How to Split a Numeric String Using Multiple Separators in a Swift Closure

can I split a numeric string using multiple separators in a Swift closure?

If your intention is to create floating point numbers from
either a decimal representation or a fraction, then there is no
need to split the string at the decimal point.

You can try to convert the string with Double(string),
and if that fails, split it at the slash and convert numerator
and denominator separately:

func doubleFromDecimalOrFraction(s: String) -> Double? {
// Try to convert from decimal representation:
if let value = Double(s) {
return value
}
// Try to convert from fractional format:
if let range = s.range(of: "/"),
let num = Double(s.substring(to: range.lowerBound)),
let den = Double(s.substring(from: range.upperBound)) {
return num/den
}
// Invalid format
return nil
}

(Instead of returning nil for invalid input you might also
consider to throw an error, to abort the execution with
fatalError(), or to return some default value.)

This "utility function" can then be applied each array element:

let strings = [ "0.0", "193.16", "5/4", "503.42", "696.58", "25/16", "1082.89", "2/1"]
let values = strings.flatMap(doubleFromDecimalOrFraction)

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

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]!
}

How to use Optionals to stop a compiler error in Swift]

  1. fraction is String not String? so you don't have to use fraction?
  2. components return [] not []?, so you can use fractionArray without unwrap anything
  3. the only thing you have to unwrap is numerator and denominator, their type is Double?
  4. Thanks @OOPer, should check denominator != 0
  5. Thanks @Martin R, should check fractionArray.count == 2

so I'll refactor to the following code:

let fraction = "15/8"
let fractionArray = fraction.components(separatedBy: "/")
guard let numerator = Double(fractionArray[0]),
let denominator = Double(fractionArray[1]),
denominator != 0,
fractionArray.count == 2 else {
print("Invalid. Re-enter fraction, or denominator == 0, or fractionArray.count != 2")
return
}
let linearFactor = numerator / denominator
print(numerator, "/", denominator, " = ", linearFactor)

How to use Optionals to stop a compiler error in Swift]

  1. fraction is String not String? so you don't have to use fraction?
  2. components return [] not []?, so you can use fractionArray without unwrap anything
  3. the only thing you have to unwrap is numerator and denominator, their type is Double?
  4. Thanks @OOPer, should check denominator != 0
  5. Thanks @Martin R, should check fractionArray.count == 2

so I'll refactor to the following code:

let fraction = "15/8"
let fractionArray = fraction.components(separatedBy: "/")
guard let numerator = Double(fractionArray[0]),
let denominator = Double(fractionArray[1]),
denominator != 0,
fractionArray.count == 2 else {
print("Invalid. Re-enter fraction, or denominator == 0, or fractionArray.count != 2")
return
}
let linearFactor = numerator / denominator
print(numerator, "/", denominator, " = ", linearFactor)

optional chaining in Swift 3: why does one example work and not the other?

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.

Find NSRanges of all substrings splitted by new lines

In the closure create an NSRange from Range<String.Index>

let string = "hello\nhello"
let rangesSplittedByLines: [NSRange] = string.split(separator: "\n").map {
return NSRange(string.range(of: $0)!, in: string)
}

But there is another problem. The code returns [{0, 5}, {0, 5}] because range(of: returns always the range of the first match.

This fixes the problem, it slices the string by moving the start index to the position after the most recent match

let string = "hello\nhello"
var startIndex = string.startIndex
let rangesSplittedByLines: [NSRange] = string.split(separator: "\n").map {
let subString = string[startIndex...]
let swiftRange = subString.range(of: $0)!
let nsRange = NSRange(swiftRange, in: string)
startIndex = swiftRange.upperBound
return nsRange
}

And there is no reason to bridge String to NSString explicitly.

But as you want nsranges anyway Regular Expression like in Martin’s answer is much more efficient.

swift string separation but include the

You can enumerate your substrings in range using .byWords options, append the substring to your words array, get the substring range upperBound and the enclosed range upperBound, remove the white spaces on the resulting substring and append it to the words array:


import Foundation

let sentence = "What is your name?"
var words: [String] = []
sentence.enumerateSubstrings(in: sentence.startIndex..., options: .byWords) { substring, range, enclosedRange, _ in
words.append(substring!)
let start = range.upperBound
let end = enclosedRange.upperBound
words += sentence[start..<end]
.split{$0.isWhitespace}
.map(String.init)
}

print(words) // "["What", "is", "your", "name", "?"]\n"

You can also use a regular expression to replace the punctuation by the same punctuation preceded by a space before splitting your words by whitespaces:

let sentence = "What is your name?"
let words = sentence
.replacingOccurrences(of: "[.,;:?!]",
with: " $0",
options: .regularExpression)
.split{$0.isWhitespace}

print(words) // "["What", "is", "your", "name", "?"]\n"

Swift native approach:

var sentence = "What is your name?"
for index in sentence
.indices
.filter({ sentence[$0].isPunctuation })
.reversed() {
sentence.insert(" ", at: index)
}
let words = sentence.split { $0.isWhitespace }
words.forEach { print($0) }

This will print:

What

is

your

name

?

case insensitive splitting swift 4?

You can write something like this:

import Foundation

let str1 = "Hello There!"
let str2 = "hello there!"

extension String {
func caseInsensitiveSplit(separator: String) -> [String] {
//Thanks for Carpsen90. Please see comments below.
if separator.isEmpty {
return [self] //generates the same output as `.components(separatedBy: "")`
}
let pattern = NSRegularExpression.escapedPattern(for: separator)
let regex = try! NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let matches = regex.matches(in: self, options: [], range: NSRange(0..<self.utf16.count))
let ranges = (0..<matches.count+1).map { (i: Int)->NSRange in
let start = i == 0 ? 0 : matches[i-1].range.location + matches[i-1].range.length
let end = i == matches.count ? self.utf16.count: matches[i].range.location
return NSRange(location: start, length: end-start)
}
return ranges.map {String(self[Range($0, in: self)!])}
}
}

print( str1.caseInsensitiveSplit(separator: "th") ) //->["Hello ", "ere!"]
print( str2.caseInsensitiveSplit(separator: "th") ) //->["hello ", "ere!"]

But I wonder what you want to do with "hello " and "ere!".
(You lose case-info of the separator, if it matched with "th", "tH", "Th" or "TH".)

If you can explain what you really want to do, someone would show you a better solution for it.

why does a Swift function using .flatMap return `Double` but not `tuple'?

The function currently works fine returning Double and already gives the variable I need, namely frequency. It is better not to change it. I will create a new class to add further functionality (i.e. mapping frequencies to keys of a MIDI keyboard). Thank you Rob and jtbandes for your input.

EDIT

It was more sensible to solve this without broadening the problem space. I found my solution once I questioned the need to return an argument to the function and asked instead how data could be presented from inside the function using only the available arguments. The solution addresses a related problem as identified by the accepted answer to another post and unwraps optional values without causing runtime errors as recommended on several other posts (here, here and here.)

Values are unwrapped using optional chaining and nil coalescing instead of forced unwrapping. Numbers that are valid according to these rules are converted to frequencies and mapped to a MIDI keyboard. Invalid tuning values and note strings marked with an ‘x’ (as specified by these rules) will generate a frequency of 0.0 Hz.

The solution allows Optional scale values that are read by the function to be compared more easily with frequencies returned as other functions are called.

e.g.

    Octave map

0 Optional("x")
0 : 0.0
1 Optional("35/32")
1 : 286.1534375
2 Optional("x")
2 : 0.0
3 Optional("x")
3 : 0.0
4 Optional("5/4")
4 : 327.0325
5 Optional("21/16")
5 : 343.384125
6 Optional("x")
6 : 0.0
7 Optional("3/2")
7 : 392.439
8 Optional("x")
8 : 0.0
9 Optional("x")
9 : 0.0
10 Optional("7/4")
10 : 457.8455
11 Optional("15/8")
11 : 490.54875

It also helps with reading frequencies in other octaves

    MIDI map

Octave 0
0 0 0.0
1 1 8.942294921875
2 2 0.0
3 3 0.0
4 4 10.219765625
5 5 10.73075390625
6 6 0.0
7 7 12.26371875
8 8 0.0
9 9 0.0
10 10 14.307671875
11 11 15.3296484375

Octave 1
12 0 0.0
13 1 17.88458984375
14 2 0.0
15 3 0.0
16 4 20.43953125
17 5 21.4615078125
18 6 0.0
19 7 24.5274375
20 8 0.0
21 9 0.0
22 10 28.61534375
23 11 30.659296875

Octave 2
24 0 0.0
25 1 35.7691796875
26 2 0.0

etc

Octave 9
108 0 0.0
109 1 4578.455
110 2 0.0
111 3 0.0
112 4 5232.52
113 5 5494.146
114 6 0.0
115 7 6279.024
116 8 0.0
117 9 0.0
118 10 7325.528
119 11 7848.78

Octave 10
120 0 0.0
121 1 9156.91
122 2 0.0
123 3 0.0
124 4 10465.04
125 5 10988.292
126 6 0.0

Developers of music apps may find this useful because it shows how to create a retuned MIDI map. The solution lets me unwrap numeric strings consisting of both fractions and decimals that specify the tuning of notes in musical scales that lie beyond the scope of a standard music keyboard. Its significance will not be lost on anyone who visits this site.

Here is the code

Tuner.swift

import UIKit

class Tuner {

var tuning = [String]() // .scl
var pitchClassFrequency = Double() // .scl
let centsPerOctave: Double = 1200.0 // .scl mandated by Scala tuning file format

let formalOctave: Double = 2.0 // .kbm/.scl Double for stretched-octave tunings
var octaveMap = [Double]() // .kbm/.scl
var midiMap = [Double]() // .kbm

let sizeOfMap = 12 // .kbm
let firstMIDIKey = 0 // .kbm
let lastMIDIKey = 127 // .kbm

let referenceMIDIKey = 60 // .kbm
let referenceFrequency: Double = 261.626 // .kbm frequency of middle C

var indexMIDIKeys = Int()
var indexOctaveKeys = Int()
var currentKeyOctave = Int()
var index: Int = 0

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

// SCL file format - create frequency map of notes for one octave
print("Octave map")
print("")
let _ = tuning.flatMap(scaleToFrequencies)

// KBM file format - create frequency map of MIDI keys 0-127
print("")
print("MIDI map")
let _ = createMIDIMap()

}

func createMIDIMap() {

indexOctaveKeys = firstMIDIKey // set indexOctaveKeys to pitchClass 0
currentKeyOctave = firstMIDIKey // set currentOctave to octave 0

for indexMIDIKeys in firstMIDIKey...lastMIDIKey {
let indexOctaveKeys = indexMIDIKeys % sizeOfMap

currentKeyOctave = Int((indexMIDIKeys) / sizeOfMap)
let frequency = octaveMap[indexOctaveKeys] * 2**Double(currentKeyOctave)

// midiMap[i] = octaveMap[indexMIDIKeys] * 2**Double(currentKeyOctave)
if indexOctaveKeys == 0 {
print("")
print("Octave \(currentKeyOctave)")
}
print(indexMIDIKeys, indexOctaveKeys, frequency)
}
}

func scaleToFrequencies(s: String?) {

var frequency: Double = 0.0

// first process non-numerics.
let numericString = zapAllButNumbersSlashDotAndX(s: s)

print(index, numericString as Any) // eavesdrop on String?

// then process numerics.
frequency = (processNumericsAndMap(numericString: numericString)) / Double(2)**Double(referenceMIDIKey / sizeOfMap)
octaveMap.append(frequency)
print(index,":",frequency * 2**Double(referenceMIDIKey / sizeOfMap))
index += 1
}

func processNumericsAndMap(numericString: String?) -> Double {
guard let slashToken = ((numericString?.contains("/")) ?? nil),
let dotToken = ((numericString?.contains(".")) ?? nil),
let xToken = ((numericString?.contains("x")) ?? nil),
slashToken == false,
dotToken == true,
xToken == false
else {
guard let dotToken = ((numericString?.contains(".")) ?? nil),
let xToken = ((numericString?.contains("x")) ?? nil),
dotToken == false,
xToken == false
else {
guard let xToken = ((numericString?.contains("x")) ?? nil),
xToken == false
else {
// then it must be mapping.
let frequency = 0.0
// print("[x] \(frequency) Hz")
return frequency
}
// then process integer.
let frequency = processInteger(s: numericString)
return frequency
}
// then process fractional.
let frequency = processFractional(s: numericString)
return frequency
}
// process decimal.
let frequency = processDecimal(s: numericString)
return frequency
}

func processFractional(s: String?) -> Double {
let parts = s?.components(separatedBy: "/")
guard parts?.count == 2,
let numerator = Double((parts?[0])?.digits ?? "failNumerator"),
let dividend = Double((parts?[1])?.digits ?? "failDenominator"),
dividend != 0
else {
let frequency = 0.0
print("invalid ratio: frequency now being set to \(frequency) Hz")
return frequency
}
let frequency = referenceFrequency * (numerator / dividend)
return frequency
}

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

let parts = s?.components(separatedBy: ".")
guard parts?.count == 2,
let intervalValue = Double(s ?? "failInterval"),
let _ = Double((parts?[0])?.digits ?? "failDecimal")
else {
let frequency = 0.0
print("invalid cents value: frequency now being forced to \(frequency) Hz ")
return frequency
}
let power = intervalValue/centsPerOctave // value with explicit remainder
let frequency = referenceFrequency * (formalOctave**power)
return frequency
}

func processInteger(s: String?) -> Double {
let frequency = 0.0
print("not cents, not ratio : frequency now being set to \(frequency) Hz ")
return frequency
}

func zapAllButNumbersSlashDotAndX(s: String?) -> String? {

var mixedString = s
if mixedString != nil {
mixedString = mixedString!
}
guard var _ = mixedString?.contains("/"),
var _ = mixedString?.contains(".")
else {
let numberToken = mixedString
return numberToken
}
guard let xToken = mixedString?.contains("x"),
xToken == false
else {
let xToken = "x"
return xToken
}
let notNumberCharacters = NSCharacterSet.decimalDigits.inverted
let numericString = s?.trimmingCharacters(in: notNumberCharacters) ?? "orElse"
return numericString.stringByRemovingWhitespaces
}

}

extension String {
var stringByRemovingWhitespaces: String {
return components(separatedBy: .whitespaces).joined(separator: "")
}
}

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)
}

func pitchClass(pitchClass: Int, _ frequency: Double) -> Int {
return pitchClass
}

func frequency(pitchClass: Int, _ frequency: Double) -> Double {
return frequency
}

ViewController.swift

import UIKit

class ViewController: UIViewController {

// Hexany: 6-note scale of Erv Wilson

let tuning = ["x", "35/32", "x", "x", "5/4", "21/16", "x", "3/2", "x", "x", "7/4", "15/8"]

// 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"]

override func viewDidLoad() {
super.viewDidLoad()

_ = Tuner(tuning: tuning)

}
}

Swift version of componentsSeparatedByString

If you want to split a string by a given character then you can use the
built-in split() method, without needing Foundation:

let str = "Today is so hot"
let arr = split(str, { $0 == " "}, maxSplit: Int.max, allowEmptySlices: false)
println(arr) // [Today, is, so, hot]

Update for Swift 1.2: The order of the parameters changed with Swift 1.2 (Xcode 6.3), compare split now complains about missing "isSeparator":

let str = "Today is so hot"
let arr = split(str, maxSplit: Int.max, allowEmptySlices: false, isSeparator: { $0 == " "} )
println(arr) // [Today, is, so, hot]

Update for Swift 2: See Stuart's answer.

Update for Swift 3:

let str = "Today is so hot"
let arr = str.characters.split(separator: " ").map(String.init)
print(arr)


Related Topics



Leave a reply



Submit