Make App Pie

Training for Developers and Artists

Parsing Strings from Time and Fractions to Doubles

This week’s lesson covers a topic the I left off on last week’s lesson, but can be used outside that context. I thus wanted to cover it separately. Last week I showed you how to use a UIPickerView to input numbers with a lot less validation. We looked at doubles, fractions and time intervals.

2016-12-12_07-33-06  2016-12-12_07-35-25     2016-12-12_07-34-30

You can go here to read the article on how I set that up. I left the article with one deficiency: the values returned from the picker view are strings, not numbers. In this lesson, I’ll show you how to convert the string to the much more useful Double and its alias TimeInerval. Since that’s  something you can use outside the application from last week, I’m presenting it separately. Those who want this  in the picker view should be able to cut and paste these functions into the code from last week.

For this Lesson I’m going to use a swift playground. You can use one in Xcode or on your iPad by downloading this file and loading it into playgrounds: .  I’ve heavily commented that file to explain what’s going on here.

Converting a String to Double.

The easiest of the conversion cases will also be the building block of all the rest: converting a string to a double. In a new playground, add this:

let numberString = "3.1415926"
if let number = Double(numberString){
    print(number)
}

The constructor for type Double has a converter built-in for converting strings to doubles. All you need is Double("3.14") to convert the string to a number – almost. Double returns an optional value of Double if you convert a string, where nil is an invalid string for conversion. for 3.l4l59@6 instead of 3.1415926. returns nil. Before you use the value, you’ll need to unwrap it. For these conversions,  I use either if let or guard to do that, so I can return nil if there is any reason the string is invalid for conversion. Here, I ignore any nil case, but that will change shortly.

Converting Minutes and Seconds to TimeInterval

TimeInterval is a commonly used type, which is really an alias for Double. TimeInterval is different from the other time type Date you’ll often see in cod . TimeInterval is a measure of seconds, independent of a starting and ending time.  Date is a  time based on a reference date. Both have their uses. Apple has the DateFormatter classes and its dateFromString method for handling dates, so I’m not concerned with those as much as TimeInterval. Unlike Date which has a lot of localization issues, almost everyone expresses the measures involved in time intervals constantly  to  hh:mm:ss.ss. The only variations are leaving off the hours or adding days. That means writing a straightforward function is relatively easy. I’ll start even easier and convert a string of minutes and seconds only. Make a new function in the playground of this:

func minutesSecondsInterval(_ timeString:String)->TimeInterval!{
}

We’ll assume that the string timeString looks like mm:ss.ss. If that’s the case, then there’s a very handy string function components(separatedBy:) change the function to this:

func minutesSecondsInterval(_ timeString:String)->TimeInterval!{
    let time = timeString.components(separatedBy: ":")
}

The components(separatedBy:) function creates a string array separated by a colon. if timeString was “12:34.56” time becomes [“12″,”34.56”]. I can take the components of that array and after checking for a non-nil value, add them together to find the number of seconds.

func minutesSecondsInterval(_ timeString:String)->TimeInterval!{
    var time = timeString.components(separatedBy: ":")
    if let seconds = Double(time[1]){
        if let minutes = Double(time[0]){
            return (seconds + (minutes * 60.0))
        }
    }
    return nil
}

If minutes and seconds are true values, I’ll add the seconds to minutes multiplied to 60 seconds. If either are nil, I drop out of the if and return nil.

A Flexible TimeInverval Converter

That’s okay, but not great. If I want hours, I’d need to write a new function. A string with more than one colon gives wrong results. If I put 1:10:12.5, the time array would be [“1″,”10″,”12.5”], adding 1 minute time 60 for 60 seconds and 10 seconds together for 70 seconds, which is wrong. This should be a lot more robust.

Think this out. Where hours, minutes and seconds appear in the array changes depending on the string. Reverse the array elements though, and if they exist, the time component is always in the same position in the array. 10:12.15 is [“12.15”,”10] reversed and 1:10:12.15 is [“12.15″,”10″,”1”] reversed. Seconds is always index 0, minutes index 1, hours index 2, if it exists. I multiply the number of seconds in a minute (60) and the number of seconds in an hour (3600) to the component before I add it to the result. If I have a matching array of those component multipliers, I could do that and put the whole thing in a loop that loops only the length of the array. If I have two components only do minute and seconds, If I have three, do all three. If I find five, return nil, because that’s an error. That all becomes this function:

func timeInterval(_ timeString:String)->TimeInterval!{
    let timeMultipilers = [1.0,60.0,3600.0] //seconds for unit
    var time = 0.0
    var timeComponents = timeString.components(separatedBy: ":")
    timeComponents.reverse()
    if timeComponents.count > timeMultipilers.count{return nil}
    for index in 0..<timeComponents.count{
        guard let timeComponent = Double(timeComponents[index]) else { return nil}
        time += timeComponent * timeMultipilers[index]
    }
    return time
}

I have a constant array timeMultipliers with the multiplier value for the component. This could be expanded to days, if I add another element of 86400.00, but I rarely need that for time intervals. I initialize a value time where I’ll add the components together. I break apart the timeString argument into an array then reverse it with the reverse() method of Array. I check the array if there are more components than I have multipliers for. If there is, it’s an invalid string, and I return nil.

There’s a loop from 0 to the last value in the timeComponents array. I use guard to convert the element in timeComponents to a Double, making sure the value is non-nil. If nil, I return nil. If not nil, I multiply by the multiplier, and add that result to time. When the loop is over, I return the time.

This will work with a value in seconds, seconds and minutes, and hours, seconds, and minutes, returning nil for any invalid answer.

Converting Fractions.

In the picker view, I made input for fractions. Fractions have three components: a whole number, a numerator and a denominator. The double value is the whole number added to the numerator divided by the denominator. In the picker view, I picked a format of w n/d, so thirty-three and a third is a string 33 1/3. This has two separators, a space and a slash instead of the single separator of the time interval. The String method you’ve used so far uses a single character. It also can use a character set. Add this function to your code:

func double(fractionString:String)->Double!{
    let separators = CharacterSet(charactersIn: " /")
    let components = fractionString.components(separatedBy: separators)
}

Before breaking the string apart to an array, you make a list of separators as a CharacterSet, in our case a space and a slash. This breaks the array into three components ["w","n","d"]. So the string “33 1/3” becomes ["33',"1","3"]. This never has a change of format, so I can directly use these values, and assume there are only three components, so check for a count of 3 in the array for validity. Get the components, then do the math to get the double.

func double(fractionString:String)->Double!{
    let seperators = CharacterSet(charactersIn: " /")
    let components = fractionString.components(separatedBy: seperators)
    if components.count == 3{
        if let wholeNumber = Double(components[0]){
            if let numerator = Double(components[1]){
                if let denominator = Double(components[2]){
                    return wholeNumber + (numerator/denominator)
                }
            }
        }
    }
    return nil //failure case
}

Try this out and you’ll get some doubles

One more bug

However, there’s a problem. Try this one:

double(fractionString: "12 0/5")

You should get 12.0 back. You get nil instead.
In cases where we don’t have three components, this doesn’t work. If I had two or one component, I’d like to return just the whole number and ignore whatever is wrong with the fraction. The if let optional chaining presents a problem though. All my calls are local, and make it hard to return just the whole number. This is the beauty of guard. I’ll change this code to use guard, check for the proper number of components and act accordingly.

Make a new function like the first but chage the parameter to (fraction fractionString:String) so we can use it in the playground without duplication complaints from the compiler.

func double(fraction fractionString:String)->Double!{
    let separators = CharacterSet(charactersIn: " /")
    let components = fractionString.components(separatedBy: separators)
}

I’m breaking this into two steps instead of one. I’ll check for components to be in the range of 1 to 3. of it isn’t we have an invalid string and will return nil. I’ll use guard to get a constant number. However since this is within the if clause it’s local, so if successful, I’ll assign to a variable wholeNumber the value of number

var wholeNumber:Double = 0.0
if components.count <= 3 && components.count > 0 {
     guard let number = Double(components[0]) else{
          return nil //invalid whole number
     }
     wholeNumber = number
} else {
     return nil // wrong number of components
}

Anything that survives that first if clause is a valid whole number and there are 1, 2, or 3 elements in the array. If I have 3 elements, as I did in the previous example, I have a mixed fraction, and can find the value of the numerator and denominator once again using guard, return the whole number if the value is invalid. Then I can return the value of the fraction, like I did in the last example.

if components.count == 3{
     guard  let numerator = Double(components[1]) else {return wholeNumber}
     guard  let denominator = Double(components[2]) else {return wholeNumber}
     if denominator != 0{
          return wholeNumber + (numerator/denominator)
     } else {return wholeNumber} //division by zero will result in zero for the fraction
}
return wholeNumber

You’ll notice my other paranoid thing I did. I prevented division by zero, returning the whole number if denominator is zero. I also return wholeNumber if I have only one or two components.

Test this out:

double(fraction: "33 1/3")
double(fraction: "33 0/3")
double(fraction: "33 1/0")
double(fraction: "33")

You get an extra added feature. Since the code converts everything to Double, this works:

double(fraction: "33.1234")

And so does this.

double(fraction: "33.1234 1.1/1.1")

Since it doesn’t harm anything and might be useful in a few places where I might be converting just decimals in one string and fractions in another, I’m leaving this the way it is.

Adding to Last Week’s Project

The rest of this is for those working through last weeks lesson. If you didn’t, you can skip this. If you worked through last week’s post and are wondering how to use this in that code, copy the double(fraction fractionString:String) and timeInterval(_ timeString:String) into the ViewController class of that project:

func double(fraction fractionString:String)->Double!{
        let separators = CharacterSet(charactersIn: " /")
        let components = fractionString.components(separatedBy: separators)
        print (components)
        var wholeNumber:Double = 0.0
        if components.count <= 3 && components.count > 0 {
            guard let number = Double(components[0]) else{
                return nil //invalid whole number
            }
            wholeNumber = number
        } else {
            return nil // wrong number of components
        }
        if components.count == 3{
            guard  let numerator = Double(components[1]) else {return wholeNumber}
            guard  let denominator = Double(components[2]) else {return wholeNumber}
            if denominator != 0{
                return wholeNumber + (numerator/denominator)
            } else {return wholeNumber} //division by zero will result in zero
        }
        return wholeNumber
    }

    func timeInterval(_ timeString:String)->TimeInterval!{
        let timeMultipiler = [1.0,60.0,3600.0] //seconds for unit
        var time = 0.0
        var timeComponents = timeString.components(separatedBy: ":")
        if timeComponents.count > timeMultipiler.count{
            return nil
        }
        timeComponents.reverse()
        for index in 0..<timeComponents.count{
            guard let timeComponent = Double(timeComponents[index]) else { return nil}
            time += timeComponent * timeMultipiler[index]
        }
        return time
    }

In the pickerView:didSelectRow: delegate, change the display to the label to this:

func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        resultString = ""
        for index in 0..<components.count{ let digit = components[index][pickerView.selectedRow(inComponent: index)] if digit.characters.count > 1 {//add space if more than one character
                resultString += " " //add space if more than one character
            }
            resultString += digit
        }
        
//--- New Code for displaying doubles ----
        // display results as a string and as a double
        var value:Double! = 0.0
        if segmentedControl.selectedSegmentIndex == 2{
            value = timeInterval(resultString) //time
        }else{
            value = double(fraction: resultString) //fraction or decimal
        }
        displayLabel.text = "\(resultString) is \(value)"

    }

I got sneaky here. I only needed two conversion functions and not three because of that extra added feature of double(fraction:). Time calls the timeInterval function, everything else will call  the double(fraction:) function. Instead of unwrapping the value I used string interpolation to present the value. The resulting string will tell me this is an optional value. For a real app you’ll be doing some more unwrapping of course.

Build and run. For a decimal value you get this:
2016-12-20_08-03-05

For a time you get this:
2016-12-20_08-02-48

Unfortunately, for a string of 3 for the fraction you get this

2016-12-20_08-07-58

But it does work in this case.
2016-12-20_08-09-11

This is a problem with the picker. In the numberPickerComponent function the first element of the x case, "0" is a simple number character with no delimiter, and the string does not get broken:

case "x":
   return ["0","1/16","1/8","3/16",
           "1/4","5/16","3/8","7/16",
          "1/2","9/16","5/8","11/16",
           "3/4","13/16","7/8","15/16"]

Changing "0" to " 0" by adding a space in front is a cheap and easy way to solve that problem.

case x:            
return [" 0","1/16","1/8","3/16",
        "1/4","5/16","3/8","7/16",
        "1/2","9/16","5/8","11/16",
        "3/4","13/16","7/8","15/16"]

Build and run. Now it works.

2016-12-20_08-14-41

 

The process for converting any string into a double is the same. Find the characters that separate components. divide the string, check that the components are valid numbers, then add them to for your final result. As I’ve shown here, that might be different for the format you are using, but the general principles are the same.

The Whole Code

 

Here’s the week’s lesson as a Swift playground, formatted for the iPad playgrounds. Copy and paste into a playground on Xcode or iPad Playgrounds.   You can download and unzip the file here as well: numberstringparser-playground

import UIKit
//: A playground for converting Strings to Doubles (TimeInterval)

/*: #Case 1: A String that looks like number
 Double converts to an optional, where `nil` is the unconvertable value. */

let numberString = "3.1415926"
if let number = Double(numberString){
    print(number)
}

/*: # Case 2: A String that looks like a *mm:ss.ss* time
  - Break into components, using `components(separatedBy:)`
  - Unwrap each component, and add together
  - If anything goes wrong, return `nil` */
func minutesSecondsInterval(_ timeString:String)->TimeInterval!{
    var time = timeString.components(separatedBy: ":")
    if let seconds = Double(time[1]){
        if let minutes = Double(time[0]){
            return (seconds + (minutes * 60.0))
        }
    }
    return nil
}
//: Try this out
minutesSecondsInterval("10:13.6")

/*: # Case 3: A Flexible TimeInterval converter.
 - This has a constant array `timeMultiplier` holding a mutiplier for the number of seconds for the component
 - The function reverses the array with the `reverse()` method so components are alwys in the same position.
 - The function uses a loop to access the correct component and multiply by `timeMultiplier` before adding together.
 */
func timeInterval(_ timeString:String)->TimeInterval!{
    let timeMultipiler = [1.0,60.0,3600.0] //seconds for unit
    var time = 0.0
    var timeComponents = timeString.components(separatedBy: ":")
    if timeComponents.count > timeMultipiler.count{
        return nil
    }
    timeComponents.reverse()
    for index in 0..<timeComponents.count{ guard let timeComponent = Double(timeComponents[index]) else { return nil} time += timeComponent * timeMultipiler[index] } return time } //: Try it out: timeInterval("1:10:13.6") /*: Case 4: Fractions using a slash. - Fractions are strings like **33 1/3** or **w n/h** - Formula is `wholeNumber + (numerator/denominator)` - There are two separators, a space and a slash. Use the `components(separatedBy: separators)` function for a character set, creating a CharaterSet of the separators by `CharacterSet(charactersIn:)` */ func double(fractionString:String)->Double!{
    let separators = CharacterSet(charactersIn: "_/")
    let components = fractionString.components(separatedBy: separators)
       if components.count == 3{
        if let wholeNumber = Double(components[0]){
            if let numerator = Double(components[1]){
                if let denominator = Double(components[2]){
                    return wholeNumber + (numerator/denominator)
                } else {return wholeNumber}//no or incomplete fraction
            } else {return wholeNumber} //no or incomplete fraction.
        }
    }
    return nil //failure case
}
//: Try it out:
double(fractionString: "12 0/0")


/*: # Case 5: A better fraction converter
 - Deals with the bug of a fraction of zero case 3 doesn't.
 - Returns the whole number part if numerator or denominator invalid value. Nil for invalid whole number.
 - All values are doubles so *22.5 10.2/2.5* will return a correct decimal value of 26.58
 */
func double(fraction fractionString:String)->Double!{
    let separators = CharacterSet(charactersIn: " /")
    let components = fractionString.components(separatedBy: separators)
    var wholeNumber:Double = 0.0
    if components.count <= 3 && components.count > 0 {
        guard let number = Double(components[0]) else{
            return nil //invalid whole number
        }
        wholeNumber = number
    } else {
        return nil // wrong number of components
    }
    if components.count == 3{
        guard  let numerator = Double(components[1]) else {return wholeNumber}
        guard  let denominator = Double(components[2]) else {return wholeNumber}
        if denominator != 0{
            return wholeNumber + (numerator/denominator)
        } else {return wholeNumber} //division by zero will result in zero
    }
    return wholeNumber
}
double(fraction: "33 1/3")
double(fraction: "33 0/3")
double(fraction: "33 1/0")
double(fraction: "33")
double(fraction: "33.1234")
double(fraction: "33.1234 1.1/1.1")

11 responses to “Parsing Strings from Time and Fractions to Doubles”

  1. […] week, I’m writing on the website about converting strings into doubles. For some cases this is easy, but if the string is a time or a fraction, it’s not so simple. […]

  2. Thank you very much, that will work for me.
    One question though.
    func timeInterval(_ timeString:String)->TimeInterval!{
    has _ timeString:String

    func double(fractionString:String)->Double!{
    does not have the _ before fractionString:String
    To call the function requires (fractionString ” xx xx/xx”)

    If I add the _ before fractionString:String
    To call the function only takes (xx xx/xx) like timeInterval

    Are you just showing that functions may be done either way?

    1. It’s also context. timeInterval is clearly something that you’ll put a string in, and doesn’t have one in its own class. Double already has a Double(_ string:String)-> Double! as a factory method. For documentation and prevent confusion, double(fractionString:string) -> Double! tells the developer and anyone reading the code specifically what gets converted to Double.

  3. Going back to your 12/12/16 Post
    DATA ENTRY WITH UIPICKERVIEW

    I changed the first index in the “x” array from “0” to “0/16” or “”
    Without a space between the whole number and the fraction it looks like a larger number.
    Since the displayLabel starts out at 000
    Picking 23 and leaving the fraction at 0 the displayLabel looks like 230
    even though the segmented selector says 99x
    0/16 inserts a space and shows 23 0/16
    “” leaves the third digit out of displayLabel which looks good.
    23
    23 1/16, etc
    BUT
    In todays post if a fraction is not entered it returns nil.
    If 0/16 is there it works, even if it is kinda ugly
    Example
    double(fractionString:”24 “) returns nil
    double(fractionString:”24 0″) returns nil
    double(fractionString:”24 0/16″) returns 24

    Also the first number (wholeNumber) can be a decimal
    double(fractionString:”23.5678 2/3”) returns 24.23446666666667

    1. In the playground.
      A decimal number could not be entered in the picker.

      1. Not sure what you are saying here. I didn’t implement a picker in the playground.

      2. A decimal whole number can be tried in a playground when there is not a picker involved.

      3. This works on any string, yes. that’s the whole point. If you are talking about what I think your are talking about, check the updated post for clarification.

    2. Yes there’s a bug, thought the case of doubles will remain.
      Here’s an entirely revamped version that does solve the bug.

      /*: # Case 5: A better fraction converter
       - Deals with the bug of a fraction of zero case 3 doesn't.
       - Returns the whole number part if numerator or denominator invalid value, nil if whole number invalid.
       - All values are doubles so *22.5 10.2/2.5* will return a correct decimal value of 26.58
       */
      func double(fraction fractionString:String)->Double!{
          let separators = CharacterSet(charactersIn: " /")
          let components = fractionString.components(separatedBy: separators)
          print (components)
          var wholeNumber:Double = 0.0
          if components.count <= 3 && components.count > 0 {
              guard let number = Double(components[0]) else{
                  return nil //invalid whole number
              }
              wholeNumber = number
          } else {
              return nil // wrong number of components
          }
          if components.count == 3{
              guard  let numerator = Double(components[1]) else {return wholeNumber}
              guard  let denominator = Double(components[2]) else {return wholeNumber}
              if denominator != 0{
                  return wholeNumber + (numerator/denominator)
              } else {return wholeNumber} //division by zero will result in zero
          }
          return wholeNumber
      }
      
      
      1. Thank you again. I thought the solution would be in the components .count but the couple of things I tried did not work. This is really helping me understand functions better. I told someone before that I was getting dangerous because I was gaining knowledge without understanding. I can write functions, or modify other people’s functions, but I don’t fully understand what I am doing. I am getting more understanding as I get more experience. Practice makes perfect. But your solutions show me what to do without all my trashing in the dark. It is easier to learn the correct way instead of trying a lot of ways that do not work.

        Especially when it is very close to exactly what I want to do.

        >

      2. Well if there’s learning happening, I’m happy to help.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: