0

Here is a detailed account of a problem following my previous post on Swift optionals.

Thanks to leads given here, here and here, I am able to read fractions (for harmonic ratios) or decimals (for cents) from a string array to calculate the frequencies of notes in musical scales.

Each element in the string array is first tested to see if it contains a / or a . One of two functions then identifies input errors using optional chaining so both fractional and decimal numbers conform to rules outlined in this tuning file format.

Example 1 and 1a shows what happens with correctly entered data in both formats.

  1. Scale with a mixture of fractions and decimals

                    C      D            E      F            G            Ab       B             C’      
    let tuning = [ "1/1", "193.15686", "5/4", "503.42157", "696.57843", "25/16", "1082.89214", "2/1"]
    

the column in the debug area shows input data (top down), row shows output frequencies (l-to-r).

    Optional("1/1")
    Optional("193.15686")
    Optional("5/4")
    Optional("503.42157")
    Optional("696.57843")
    Optional("25/16")
    Optional("1082.89214")
    Optional("2/1")
    [261.62599999999998, 292.50676085897425, 327.03249999999997, 349.91970174951047, 391.22212058238728, 408.79062499999998, 489.02764963627084, 523.25199999999995]

Examples 2 & 3 show how both functions react to bad input (i.e. wrongly entered data).

  1. bad fractions are reported (e.g. missing denominator prints a message)

    Optional("1/1")
    Optional("5/")
    User input error - invalid fraction: frequency now being set to 0.0 Hertz 
    Optional("500.0")
    Optional("700.0")
    Optional("2/1")
    [261.62599999999998, 0.0, 349.22881168708938, 391.99608729493866, 523.25199999999995]
    
  2. bad decimals are not reported (e.g. after 700 there is no .0 - this should produce a message)

    Optional("1/1")
    Optional("5/4")
    Optional("500.0")
    Optional("700")
    Optional("2/1")
    [261.62599999999998, 327.03249999999997, 349.22881168708938, 0.0, 523.25199999999995]
    

NOTE: In addition to the report 0.0 (Hz) appears in the row when an optional is nil. This was inserted elsewhere in the code (where it is explained in context with a comment.)

The problem in a nutshell ? the function for fractions reports a fault whereas the function for decimal numbers fails to detect bad input.

Both functions use optional chaining with a guard statement. This works for faulty fractions but nothing I do will make the function report a faulty input condition for decimals. After checking the code thoroughly I’m convinced the problem lies in the conditions I’ve set for the guard statement. But I just can’t get this right. Can anyone please explain what I did wrong ?

Tuner.swift

import UIKit

class Tuner {

    var tuning                      = [String]()
    let tonic: Double               = 261.626   // frequency of middle C
    var index                       = -1
    let centsPerOctave: Double      = 1200.0    // mandated by Scala tuning file format
    let formalOctave: Double        = 2.0       // Double for stretched-octave tunings

init(tuning: [String]) {
    self.tuning                     = tuning

    let frequency                   = tuning.flatMap(doubleFromDecimalOrFraction)
    print(frequency)

}


func doubleFromDecimalOrFraction(s: String?) -> Double {

    index                           += 1
    let whichNumericStringType      = s
    print(whichNumericStringType as Any)        // eavesdrop on String?

    var possibleFrequency: Double?

    //  first process decimal.
    if (whichNumericStringType?.contains("."))!             {
        possibleFrequency           = processDecimal(s: s)
    }

    //  then process fractional.
    if (whichNumericStringType?.contains("/"))!             {
        possibleFrequency           = processFractional(s: s)
    }

    // Insert "0.0" marker. Remove when processDecimal works
    let noteFrequency               = possibleFrequency
    let zeroFrequency               = 0.0
    // when noteFrequency? is nil, possibleFrequency is set to zeroFrequency
    let frequency                   = noteFrequency ?? zeroFrequency

    return frequency    // TO DO let note: (index: Int, frequency: Double)

    }


func processFractional(s: String?) -> Double?   {

    var fractionArray               = s?.components(separatedBy: "/")

    guard let numerator             = Double((fractionArray?[0])!.digits),
        let denominator             = Double((fractionArray?[1])!.digits),
        numerator                   > 0,
        denominator                 != 0,
        fractionArray?.count        == 2
        else
    {
        let possibleFrequency       = 0.0
        print("User input error - invalid fraction: frequency now being set to \(possibleFrequency) Hertz ")
        return possibleFrequency
        }
    let possibleFrequency           = tonic * (numerator / denominator)
    return possibleFrequency
        }


func processDecimal(s: String?) -> Double?      {

    let decimalArray                = s?.components(separatedBy: ".")
    guard let _                     = s,
        decimalArray?.count         == 2
        else
    {
        let denominator             = 1
        let possibleFrequency       = 0.0
        print("User input error (value read as \(s!.digits)/\(denominator) - see SCL format, http://www.huygens-fokker.org/scala/scl_format.html): frequency now being forced to \(possibleFrequency) Hertz ")
        return possibleFrequency
        }
    let power                       = Double(s!)!/centsPerOctave
    let possibleFrequency           = tonic * (formalOctave**power)
    return possibleFrequency
        }
    }


extension String {

    var digits: String {
    return components(separatedBy: CharacterSet.decimalDigits.inverted).joined()
        }
    }


precedencegroup Exponentiative {

    associativity: left
    higherThan: MultiplicationPrecedence

    }


infix operator ** : Exponentiative

func ** (num: Double, power: Double) -> Double{
    return pow(num, power)
    }

ViewController.swift

import UIKit

class ViewController: UIViewController {

    // test pitches: rational fractions and decimal numbers (currently 'good')
    let tuning = ["1/1", "5/4", "500.0", "700.0", "2/1"]

    // Diatonic scale: rational fractions
    //       let tuning = [ "1/1", "9/8", "5/4", "4/3", "3/2", "27/16", "15/8", "2/1"]

    // Mohajira: rational fractions
    //    let tuning = [ "21/20", "9/8", "6/5", "49/40", "4/3", "7/5", "3/2", "8/5", "49/30", "9/5", "11/6", "2/1"]

    // Diatonic scale: 12-tET
    //    let tuning = [ "0.0", "200.0", "400.0", "500", "700.0", "900.0", "1100.0", "1200.0"]

    // Diatonic scale: mixed 12-tET and rational fractions
    //    let tuning = [ "0.0", "9/8", "400.0", "4/3", "700.0", "27/16", "1100.0", "2/1"]

    // Diatonic scale: 19-tET
    //     let tuning = [ "0.0", "189.48", "315.8", "505.28", "694.76", "884.24", "1073.72", "1200.0"]

    // Diatonic 1/4-comma meantone scale. Pietro Aaron's temperament (1523) : mixed cents and rational fractions
    //    let tuning = [ "1/1", "193.15686", "5/4", "503.42157", "696.57843", "25/16", "1082.89214", "2/1"]

override func viewDidLoad() {
    super.viewDidLoad()

    _ = Tuner(tuning: tuning)

    }
}
Community
  • 1
  • 1
Greg
  • 1,750
  • 2
  • 29
  • 56
  • Have you debugged your processDecimal func for when your string = "700"? I would interested in know what the decimalArray.count is. – Benjamin Lowry Dec 31 '16 at 09:19
  • 1
    There's a lot of explicit forced unwrapping above (`!`), e.g. in `processFractional`: this should generally be avoided as it can lead to runtime crashes. E.g. for the `guard` in `processFractional(...)`, `Double((fractionArray?[0])!.digits)`, you could make this safe with optional chaining and the `nil` coalescing operator, e.g. `Double((fractionArray?[0])?.digits ?? "failMe")`. – dfrib Dec 31 '16 at 09:37
  • All three answers http://stackoverflow.com/a/41337042/2348597, http://stackoverflow.com/a/41337068/2348597, http://stackoverflow.com/a/41359262/2348597 to your previous questions solve the task without any forced unwrapping. – Martin R Dec 31 '16 at 10:04
  • @Martin R, can you clarify one thing please - `let numerator = Double((parts?[0])!.digits),` - forced unwrapping ? or a guard condition to catch nil ? I understood this to be the latter. – Greg Dec 31 '16 at 13:33
  • `optionalExpr!` is a forced unwrapping and causes the program to crash if `optionalExpr` is `nil`. `fractionArray?[0]` in your code is *optional chaining*. But why did you make the parameter of `func doubleFromDecimalOrFraction(s: String?)` an optional at all? That seems unnecessary to me. – I don't want to sound rude, but you already got 3 good answers solving exactly your task. Please try to understand them. Ask for clarification if necessary. – Martin R Dec 31 '16 at 13:44
  • @Martin R, I appreciate your comments (I don't find them rude). I'll work more with these answers. – Greg Dec 31 '16 at 13:58

1 Answers1

3

The problem in a nutshell ? the function for fractions reports a fault whereas the function for decimal numbers fails to detect bad input.

The function for decimal numbers does detect “bad” input. However, "700" does not contain ".", and you only call processDecimal(s:) if the string does contain ".". If the string doesn't contain "." and also doesn't contain "/", doubleFromDecimalOrFraction(s:) doesn't call any function to parse the string.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Good spot, missed that. – Benjamin Lowry Dec 31 '16 at 09:58
  • @Martin R, @robmayoff, this has to be the reason I had such trouble with decimal and not fraction. I know how to introduce a third function to process strings without `“.”` or `“/“` Or will it be better with a reworked version of both functions “without any forced unwrapping” (which I need to get on top of) ? – Greg Dec 31 '16 at 11:26
  • @rob mayor, accepted. You pin-pointed it! I'll edit the post with my solution first thing next year! – Greg Dec 31 '16 at 14:46