15
\$\begingroup\$

This code was inspired by this question: Number to Words

import Foundation

extension Int {
    func plainEnglish (negativeSign: String = "negative") -> String {
        func singleNumberName (number: Int) -> String {
            switch (number) {
            case 1: return "one"
            case 2: return "two"
            case 3: return "three"
            case 4: return "four"
            case 5: return "five"
            case 6: return "six"
            case 7: return "seven"
            case 8: return "eight"
            case 9: return "nine"
            default:return ""
            }
        }

        func tensNumberName (number: Int) -> String {
            switch (number) {
            case 2: return "twenty"
            case 3: return "thirty"
            case 4: return "fourty"
            case 5: return "fifty"
            case 6: return "sixty"
            case 7: return "seventy"
            case 8: return "eighty"
            case 9: return "ninety"
            default:return ""
            }
        }

        func teensNumberName (number: Int) -> String {
            switch (number) {
            case 0: return "ten"
            case 1: return "eleven"
            case 2: return "twelve"
            case 3: return "thirteen"
            case 5: return "fifteen"
            default:return singleNumberName(number) + "teen"
            }
        }

        var number = self
        var digits: [Int] = []

        while number != 0 {
            digits.append(abs(number%10))
            number /= 10
        }

        var plainEnglish: [String] = []

        var isTeen = false
        let totalPlaces = digits.count
        var currentPlace = 0

        for (index, digit) in enumerate(digits.reverse()) {
            currentPlace = totalPlaces - index
            if currentPlace % 3 == 0 && digit > 0 {
                plainEnglish.append(singleNumberName(digit) + " hundred")
            } else if (currentPlace + 1) % 3 == 0 {
                if digit == 1 {
                    isTeen = true
                    continue;
                } else {
                    isTeen = false
                    plainEnglish.append(tensNumberName(digit))
                }
            } else {
                if isTeen {
                    plainEnglish.append(teensNumberName(digit))
                } else {
                    plainEnglish.append(singleNumberName(digit))
                }
            }

            if currentPlace == 4 {
                plainEnglish.append("thousand")
            } else if currentPlace == 7 {
                plainEnglish.append("million")
            } else if currentPlace == 10 {
                plainEnglish.append("billion")
            }           
        }

        let initial = (self < 0) ? negativeSign : ""

        func combine (first: String, second: String) -> String {
            return first + " " + second
        }

        let finalString = plainEnglish.reduce(initial, combine: combine)

        return finalString.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
    }
}

This is implemented as an extension on the Int data type, so you can use it via dot notation on any integer (including literals). Example usage:

let payamount = 98327
let dollars = payamount / 100
let cents = payamount % 100
let checkString = dollars.plainEnglish() + " dollars and " + cents.plainEnglish() + " cents"

Which results in a string that reads:

"nine hundred eighty three dollars and twenty seven cents"
\$\endgroup\$
2
  • \$\begingroup\$ I think you've forgotten to use the negativeSign argument for anything. Near the bottom of the function (let initial = …), you just have a hard-coded "negative". (By the way negativePrefix might be a more precise name for the argument) \$\endgroup\$
    – Flambino
    Commented Aug 3, 2014 at 11:21
  • \$\begingroup\$ Yeah whoops. I originally had that right but it was embedded in the let finalString ... line. I messed it up when I moved it out of there. \$\endgroup\$
    – nhgrif
    Commented Aug 3, 2014 at 12:02

4 Answers 4

18
\$\begingroup\$

So Apple already has an implementation for this, using NSNumberFormatter. A bit easier to implement than what you have now ;).

var numberFormatter:NSNumberFormatter = NSNumberFormatter()
numberFormatter.numberStyle = NSNumberFormatterStyle.SpellOutStyle
var string = numberFormatter.stringFromNumber(100)
\$\endgroup\$
2
  • 2
    \$\begingroup\$ You can drop the :NSNumberFormatter from line 1 and drop NSNumberFormatterStyle from line 2. \$\endgroup\$
    – vacawama
    Commented Aug 9, 2014 at 21:17
  • 2
    \$\begingroup\$ If you only need to translate one, you can use this one-liner: var string = NSNumberFormatter.localizedStringFromNumber(100, numberStyle: .SpellOutStyle) \$\endgroup\$
    – vacawama
    Commented Aug 9, 2014 at 21:28
7
\$\begingroup\$
if currentPlace == 4 {
    plainEnglish.append("thousand")
} else if currentPlace == 7 {
    plainEnglish.append("million")
} else if currentPlace == 10 {
    plainEnglish.append("billion")
} 

First of all, this doesn't cover all of the cases. Swift Ints are 64 bit, so you need to cover everything up to "quintillion".

Second of all, why aren't we using a function for this like we used for other names?

Let's make a function that returns an optional string:

func bigNumberName (place: Int) -> String? {
    switch (place) {
    case 4: return "thousand"
    case 7: return "million"
    case 10:return "billion"
    case 13:return "trillion"
    case 16:return "quadrillion"
    case 19:return "quintillion"
    default:return nil
    }
}

And now let's replace that if-else structure that needed 3 more branches to it with a single if:

if let bigName = bigNumberName(currentPlace) {
    plainEnglish.append(bigName)
}

The same logic could and probably should be applied to the singleNumberName and tensNumberName functions, where they return nil rather than empty strings for the default case. Doing this will probably help clean up the forin loop a little bit.

\$\endgroup\$
5
\$\begingroup\$

Seems fine to me. Here's some thoughts:

  • You will need to put some sort of limit on the int, or provide all the words (thousand, million, billion, etc) up to the int's natural limit. Or return "A really big number greater than a quadrillion" or something like that if there's no limit.
  • With very similar code, you can add an ordinal number output: "First, second, three-hundred and twenty-first"
  • You can easily add "negative" or "minus" to the test suite
\$\endgroup\$
5
  • \$\begingroup\$ I don't understand your third bullet point. As for your first one, the original code posted would've covered everything that fits in a 32-bit integer, which I assumed was what Swift ints are, but they are actually 64-bit integers, so we need to cover up to quintillion. \$\endgroup\$
    – nhgrif
    Commented Aug 3, 2014 at 14:10
  • \$\begingroup\$ Oh sorry, I meant add it to the test suite \$\endgroup\$
    – Carlos
    Commented Aug 3, 2014 at 14:12
  • \$\begingroup\$ You mean in my "example usage" I provided below? \$\endgroup\$
    – nhgrif
    Commented Aug 3, 2014 at 14:14
  • \$\begingroup\$ Yeah, that's right. I like to make sure the tests/examples cover any possibilities. \$\endgroup\$
    – Carlos
    Commented Aug 3, 2014 at 14:15
  • 1
    \$\begingroup\$ Ah okay. I tested these things--just didn't include examples. \$\endgroup\$
    – nhgrif
    Commented Aug 3, 2014 at 14:16
4
\$\begingroup\$
return finalString.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())

This is ugly. And it only exists because if the Int is not negative, you end up with a space at the front of the string because of the way your combine works.

What's more, because of the way combine works, if you end up with a 0 in the hundreds place (for example, 1,034 or 2,044,562), you end up with a double space there and stringByTrimming... doesn't fix the embedded double space. So let's improve the combine function.

func combine (first: String, second: String) -> String {
    let joiner = (first.utf16Count > 0 && second.utf16Count > 0) ? " " : ""
    return first + joiner + second
}

Now the last few lines look like this and never produce and unnecessary spaces:

let initial = (self < 0) ? negativeSign : ""

func combine (first: String, second: String) -> String {
    let joiner = (first.utf16Count > 0 && second.utf16Count > 0) ? " " : ""
    return first + joiner + second
}

let finalString = plainEnglish.reduce(initial, combine: combine)

return finalString
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.