Greg
Greg

Reputation: 1842

How to make a closure in Swift extract two integers from a string to perform a calculation

I am currently using map property with a closure in Swift to extract linear factors from an array and calculate a list of musical frequencies spanning one octave.

    let tonic: Double   = 261.626 // middle C
    let factors         = [  1.0,   1.125, 1.25,  1.333, 1.5,   1.625,   1.875]

    let frequencies     = factors.map { $0 * tonic }
    print(frequencies)

    // [261.62599999999998, 294.32925, 327.03249999999997, 348.74745799999994, 392.43899999999996, 425.14224999999999, 490.54874999999993]

I want to do this by making the closure extract two integers from a string and divide them to form each factor. The string comes from an SCL tuning file and might look something like this:

    //                       C      D      E      F      G      A        B 

    let ratios          = [ "1/1", "9/8", "5/4", "4/3", "3/2", "27/16", "15/8"]

Can this be done ?

SOLUTION

Thankfully, yes it can. In three Swift statements tuning ratios represented as fractions since before Ptolemy can be coverted into precise frequencies. A slight modification to the accepted answer makes it possible to derive the list of frequencies. Here is the code

import UIKit

class ViewController: UIViewController {

// Diatonic scale
let ratios = [ "1/1", "9/8", "5/4", "4/3", "3/2", "27/16", "15/8"]

// Mohajira scale
// let ratios = [ "21/20", "9/8", "6/5", "49/40", "4/3", "7/5", "3/2", "8/5", "49/30", "9/5", "11/6", "2/1"]


override func viewDidLoad() {
    super.viewDidLoad()

    _ = Tuning(ratios: ratios)

    }
}

Tuning Class

import UIKit

class Tuning {

    let tonic   = 261.626       // frequency of middle C (in Hertz)

    var ratios  = [String]()

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

    let frequencies = ratios.map { s -> Double in
        let integers = s.characters.split(separator: "/").map(String.init).map({ Double($0) })
        return (integers[0]!/integers[1]!) * tonic
    }

    print("// \(frequencies)")

    }
}

And here is the list of frequencies in Hertz corresponding to notes of the diatonic scale

     C           D           E           F           G           A           B     
    [261.626007, 294.329254, 327.032501, 348.834686, 392.439026, 441.493896, 490.548767]

It works for other scales with pitches not usually found on a black-and-white-note music keyboard Mohajira scale created by Jacques Dudon

    //                     D                      F             G                                     C'
  let ratios = [ "21/20", "9/8", "6/5", "49/40", "4/3", "7/5", "3/2", "8/5", "49/30", "9/5", "11/6", "2/1"]

And here is a list of frequencies produced

    //                      D                                         F                                       G                                                                                                   C'
    // [274.70729999999998, 294.32925, 313.95119999999997, 320.49185, 348.83466666666664, 366.27639999999997, 392.43899999999996, 418.60159999999996, 427.32246666666663, 470.92679999999996, 479.64766666666662, 523.25199999999995]

Disclaimer

Currently the closure only handles rational scales. To fully comply with Scala SCL format it must also be able to distinguish between strings with fractions and strings with a decimal point and interpret the latter using cents, i.e. logarithmic rather than linear factors.

Thank you KangKang Adrian and Atem

Upvotes: 3

Views: 527

Answers (3)

Adrian Bobrowski
Adrian Bobrowski

Reputation: 2794

Convert ratios to array of double

let ratios = [ "1/1", "9/8", "5/4", "4/3", "3/2", "27/16", "15/8"]

let array = ratios.flatMap { element in
    let parts = element.components(separatedBy: "/")
    guard parts.count == 2, 
          let dividend = Double(parts[0]), 
          let divisor = Double(parts[1]), 
          divisor != 0
    else {
        return nil
    }
    return parts[0] / parts[1]
}

Upvotes: 1

Artem Novichkov
Artem Novichkov

Reputation: 2341

If I understand your question, you can do something like that:

func linearFactors(from string: String) -> Double? {
    let components = string.components(separatedBy: "/").flatMap { Double($0) }
    if let numerator = components.first, let denominator = components.last {
        return numerator / denominator
    }
    return nil
}

Upvotes: 1

halftrue
halftrue

Reputation: 149

let ratios = [ "1/1", "9/8", "5/4", "4/3", "3/2", "27/16", "15/8"]

let factors = ratios.map { s -> Float in
    let integers = s.characters.split(separator: "/").map(String.init).map({ Float($0) })
    return integers[0]!/integers[1]!
}

Upvotes: 2

Related Questions