Displaying phrases containing numbers

I’m in the middle of writing an app in Swift which requires an alert to appear which says something like “10 questions entered”. But of course some times it should say “1 question entered”. The traditional approach to the question/questions situation is to either test for the number and create a string accordingly or to use the rather tacky “question(s). As I needed similar phrases in a number of places, I decided that the best approach was to create a function. Although the app I’m writing is for OSX, the functions described below should work in iOS as well.

The functions can be downloaded here

A generic format

In outline I wanted the function to be of the form:

func countMessage(count:Int,message:String) ->String {...}

To meet a variety of possible cases I took the general case for message to be : “some-or-no-text-[token]-some-or-no-text” repeated an arbitrary number of times, where [token] is in one of

[count] or [singularword:pluralword]

The usage is be something like:

let amount = 1
let input = "There [is:are] [count] [question:questions] outstanding"
let output = countMessage(amount,input)
println(output)

which would produce the output:

There is 1 question outstanding

changing the value of amount to 2 produces the output:

There are two questions outstanding

Although the function doesn’t preclude [count] occurring more than once, it’s hard to think of an example where it (sensibly) would.

The function

Here is the function in its original form:

 1 func countMessage(count:Int,message:String) ->String {
 2 		var outputMessage = ""
 3 		var newArray:[String] = []
 4 		var bits = split(message,{$0 == "["},allowEmptySlices:false)
 5 		for part in bits {
 6 			var bits2 = split(part,{$0 == "]"}, allowEmptySlices:false)
 7 			for var i = 0; i < bits2.count; ++i {
 8 				if bits2[i] == "count" {
 9 						bits2[i] = "\(count)"
10 				}
11 				var bits3 = split(bits2[i],{$0 == ":"},allowEmptySlices:false)
12 				var token:String
13 				if bits3.count == 1 {
14 					token = bits3[0]
15 				} else {
16 					if count == 1 {
17 						token = bits3[0]
18 					} else {
19 						token = bits3[1]
20 					}
21 				}
22 				newArray.append(token)
23 			}
24 		}
25 		outputMessage = newArray.reduce("", +)
26 		return outputMessage
27 	}

Line 4 creates a String array containing the input string ‘message’ split at each occurrence of “[”. With input of

"There [is:are] [count] [question:questions] outstanding"

bits will contain:

["There", "is:are] ", " count] ", "question:questions] outstanding"]

Line 5 iterates through the bits array

Line 6 splits each element of the bits array at each occurrence of “]” and puts it in the bits2 array. To continue the last example, for the last element of the bits array bits2 becomes

["question:questions", " outstanding"]

Note that at this point all of the “[” and “]” will have been removed.

Line 7 iterates through the bits2 array

Lines 8, 9 and 10 replace the [count] token (which by now has become just “count”) with the value of the count variable

Line 11 splits each element of the bits2 array at each occurrence of “:” and puts it in the bits3 array. For example

"question:questions"

becomes

["question":"questions"]

Lines 12 to 21 deal with selecting the appropriate part of singular/plural pairs

Line 22 builds a new array comprising all the individual elements

Line 25 creates a string consisting of all the elements concatenated together, which is returned by line 26.

Putting it in to words

Part way through creating countMessage() it occurred to me that there would be occasions when outputting the numeric value in words would be desirable. There is a partial solution using NSNumberFormatter, which I have wrapped in a function intToWords() and enhanced to allow for the capitalisation of the first letter (if desired), which proved easy and to allow for differences in the conventional way that numbers are put into words in different countries . For example in the US 153 is rendered as “one hundred fifty-three” (this is what NSFormatter provides), whereas in the UK the same number becomes “one hundred and fifty-three”. This proved much more difficult. Finally I put in an option to replace zero with some other word.

The function

Here is the function

 1 func intToWords(integer:Int,capitaliseFirst:Bool = false,insertAnds:Bool = true,theWordForZero:String = "zero")->String{
 2 		let formatter = NSNumberFormatter()
 3 		formatter.numberStyle = NSNumberFormatterStyle.SpellOutStyle
 4 		var outString = formatter.stringFromNumber(integer)!
 5 		if insertAnds {
 6 			var myDictionary = [
 7 				"hundred o":"hundred and o",
 8 				"hundred tw":"hundred and tw",
 9 				"hundred thr":"hundred and thr",
10 				"hundred f":"hundred and f",
11 				"hundred s":"hundred and s",
12 				"hundred e":"hundred and e",
13 				"hundred n":"hundred and n",
14 				"huntdred te":"hundred and te",
15 				"hundred thi":"hundred and thi"
16 			]
17 			for (originalWord, newWord) in myDictionary {
18 				outString = outString.stringByReplacingOccurrencesOfString(originalWord, withString:newWord, options: NSStringCompareOptions.LiteralSearch, range: nil)
19 			}
20 			var zz:[String] = split(outString,{$0 == " "},allowEmptySlices:false)
21 			if zz.count>3 {
22 				if zz[zz.count-2] == "thousand" || zz[zz.count-2].hasSuffix("ion"){
23 					zz[zz.count-2] += " and"
24 				}
25 			}
26 			outString = zz.reduce("", {$0 + " " + $1})
27 			let subStart = advance(outString.startIndex, 1, outString.endIndex)
28 			outString = outString.substringWithRange(Range(start: subStart, end: outString.endIndex))
29 		}
30 		if outString == "zero" {
31 			outString = theWordForZero
32 		}
33 		if capitaliseFirst {
34 			outString.replaceRange(outString.startIndex...outString.startIndex, with: String(outString[outString.startIndex]).capitalizedString)
35 		}
36 		return outString
37 	}

Lines 2 to 4 carry out the basic number to string conversion (e.g. 153 becomes “one hundred fifty-three”)

Line 5 checks whether we want “ands”s inserted in appropriate places.

Lines 6 to 16 sets up a dictionary of the trickiest before and after cases where we want to insert “and” after “hundred”. For example we do want to turn 153 into “one hundred and fifty-three”, but we don’t want to turn 100000 into “one hundred and thousand”.

Lines 17 to 19 apply the dictionary to insert the necessary “and”s

Line 20 puts each word into a separate array element.

Lines 21 to 25 checks to see whether the second to last word is “thousand”, “million”, “billion” or any other word ending “ion” and if so adds “ and” to it.

Line 26 joins the array back into a string, putting a space between each word.

For reasons I don’t fully understand the resultant string has a spurious space at the start. Lines 27 aand 28 remove this.

Lines 30 to 32 deal with replacing zero with another word.

Lines 33 to 35 capitalise the first letter if required.

Making use of the intToWords() in compareMessage()

To make use of the intToWords function in compareMessage() a few small changes are needed to the latter. Firstly, replace the first line with:

func countMessage(count:Int,message:String,numberAsWords:Bool = false, wordForZero:String="zero",capitaliseCountWordAlways:Bool = false, capitaliseCountWordIfFirst:Bool = true, insertAnds:Bool = false)->String{

Secondly replace line 5 with this:

for (j,part) in enumerate(bits) {

This uses the variable j to keep track of which element of the array bits is under scrutiny

Lastly replace line 9 with this:

if numberAsWords {
		if j==0 || capitaliseCountWordAlways{
			bits2[i] = intToWords(count, capitaliseFirst: capitaliseCountWordIfFirst,insertAnds:insertAnds,theWordForZero:wordForZero)
		} else {
			bits2[i] = intToWords(count, capitaliseFirst: capitaliseCountWordAlways,insertAnds:insertAnds,theWordForZero:wordForZero)
		}
		
	} else {
		bits2[i] = "\(count)"
	}

This uses j to determine whether we are at the beginning of the string, checks whether we want to output count in words and calls intToWords with appropriate parameters if required. Note that the parameters of the revised function allow for capitalisation of the first letter of the stringified(!?) count to always be capitalised or only if the [count] token appears at the beginning of the message string.

The functions can be downloaded here

Posted in Programming with : Swift

Written on February 9, 2015 at 02:00