D Smith

Posted on

# Introduction

Challenge can be found here.

# Description

Create a program that can convert time strings into text.

## Input Description

Input is an hour (0-23) followed by a colon followed by a minute (0-59)

``````00:00
01:30
12:05
14:01
20:29
21:00
``````

## Output Description

Program should output time in words, using the 12 hour format. (12:00 am)

``````It's twelve am
It's one thirty am
It's twelve oh five pm
It's two oh one pm
It's eight twenty nine pm
It's nine pm
``````

## Optional Challenge

Make the program actually talk using sound clips, like the ones found here.

# My Solution

So I have a solution for this that doesn't involve testing. However I have been trying to get into the habit of
following Test Driven Development. Also I will be using a literate programming technique to write the program
inside this article. The other thing I want to address is that I almost definately overengineered this too,
I get a little zealous when I'm writting.

## Functional Test

First things first, the following code block is the functional test for this challenge.

``````def talkingclock(time):
hour,minute = map(int, time.split(":"))
if hour >= 12:
period = "pm"
else:
period = "am"
numnames = (
'one','two','three',
'four','five','six',
'seven','eight','nine',
'ten','eleven','twelve')

if hour > 12:
hour -= 12
else:
pass

hour_string = numnames[hour-1]
minute_string = ""

onetable = ("one","two","three",
"four", "five", "six",
"seven","eight","nine")

teentable = ("ten","eleven","tweleve","thirteen",
"fourteen", "fifteen","sixteen",
"seventeen","eighteen","nineteen")

tentable = ("","ten","twenty","thirty","forty","fifty",)

if minute in range(1,10):
minute_string = "oh " + onetable[minute-1]
elif minute in range(10,20):
minute_string = teentable[minute-10]

elif minute in range(20,60):
ones = minute % 10
tens = minute - ones
if ones > 0:
minute_string = tentable[tens//10] + " " + onetable[ones-1]
else:
minute_string = tentable[tens//10]

output = "It's " + hour_string + ' '
if minute_string == '':
output += period
else:
output += minute_string + ' ' + period

return output

testtable = [i for i in zip(si.split(),so.split('\n\t'))]

results = [["Input","Expected Output","Actual Output", "Passed"]]

for test in testtable:

si,so = test
to = talkingclock(si)
results.append([si,so,to,so==to])

return results
``````

This test works by gathering the sample input and sample output as described in the challenge section, it splits them into the proper lines
and stores them as a list comprehension. I would have used tuples here, as the data never changes. However python creates generator objects
if you replace square brackets with parenthesis when defining a listcomp. At that point I would be just as well using the stand alone zip fuction.
Anyway the input string is fed into the talkingclock function and the output is compaired against the test output.
This creates the above results, and if any of them are false, we can look at which line it occures on and test that input separately.
While this is not the most elegant solution for testing, it is a rather practical test as we get a nice table out of it.

## Parsing the input

The input that the talking clock is given is in the format of (HH:MM) in 24 hour time.
To do this, we take the string and split it at the colon using the string's split method.
This produces a of the strings for hour and minute. These are then turned into ints and assigned to
the variables hour and minute respectively.

``````hour,minute = map(int, time.split(":"))
``````

To test our parser works, we need to look for three things.

1. It returns an hour value
2. It returns a minute value
3. Both hour and minute are ints

def testparser(time):
hour,minute = map(int, time.split(":"))
return hour,minute

th,tm = testparser(test)

isint = all(map(lambda x: type(x) == int, (th,tm)))
hashour = th != None
hasmin = tm != None

results=(
("Returns hour: ", hashour),
("Returns minutes: ", hasmin),
("All are ints: ", isint))

return results

Assuming when you run this test they all passed, we know that the parser is good to go.

## Converting intigers to words

Now that we have the input parsed into the correct form, we need to process it to get out the correct strings.

### Getting Hour String

Getting the hour string has a few requirements.

1. The hour string will always be between 1 and 12.
2. The hour string will be the english name for numbers (e.g) one

numnames = (
'one','two','three',
'four','five','six',
'seven','eight','nine',
'ten','eleven','twelve')

if hour > 12:
hour -= 12
else:
pass

hour_string = numnames[hour-1]

def testhourstring(hour):
numnames = (
'one','two','three',
'four','five','six',
'seven','eight','nine',
'ten','eleven','twelve')

``````if hour > 12:
hour -= 12
else:
pass

hour_string = numnames[hour-1]
return hour_string
``````

nname = (
'one','two','three',
'four','five','six',
'seven','eight','nine',
'ten','eleven','twelve')

zeroistwelve = (testhourstring(0) == 'tweleve')
oneisoneect = all([testhourstring(i) == nname[(i-1)] for i in range(1,13)])
thirteenisoneect = all([testhourstring(i) == nname[(i-1)] for i in range(1,13)])

result = (
("Test", "Passed"),
("0 yields Tweleve: ", zeroistwelve),
("1-13 yield correct name: ", oneisoneect),
("13-23 yield correct name: ", thirteenisoneect)
)

return result

### Getting Minute String

The minute string is a bit more complex than the hour string.
Effectively I have broken it up into a few parts

1. a ones table which runs from 1 to 9.
2. a teens table which runs from 10 to 19
3. a tens table which is 10 to 50

For numbers that are 1-9 the string is made by fetching the number of off the ones table and appending it to the "oh" phrase.
Because english is awful, I had to make the teen table, because the numbers that are 11-19 all have to be special and not conform to other numeric standards.
Finally for numbers that are between 20-59 the number is made by fetching the tens space, and if it has a number in the ones space, apending that to it.
The tens table also includes 10 to make some of my math easier, at the cost of making the tuple an element longer, which I think is a valid

``````minute_string = ""

onetable = ("one","two","three",
"four", "five", "six",
"seven","eight","nine")

teentable = ("ten","eleven","tweleve","thirteen",
"fourteen", "fifteen","sixteen",
"seventeen","eighteen","nineteen")

tentable = ("","ten","twenty","thirty","forty","fifty",)

if minute in range(1,10):
minute_string = "oh " + onetable[minute-1]
elif minute in range(10,20):
minute_string = teentable[minute-10]

elif minute in range(20,60):
ones = minute % 10
tens = minute - ones
if ones > 0:
minute_string = tentable[tens//10] + " " + onetable[ones-1]
else:
minute_string = tentable[tens//10]

def testminstring(minute):
minute_string = ""

onetable = ("one","two","three",
"four", "five", "six",
"seven","eight","nine")

teentable = ("ten","eleven","tweleve","thirteen",
"fourteen", "fifteen","sixteen",
"seventeen","eighteen","nineteen")

tentable = ("","ten","twenty","thirty","forty","fifty",)

if minute in range(1,10):
minute_string = "oh " + onetable[minute-1]
elif minute in range(10,20):
minute_string = teentable[minute-10]

elif minute in range(20,60):
ones = minute % 10
tens = minute - ones
if ones > 0:
minute_string = tentable[tens//10] + " " + onetable[ones-1]
else:
minute_string = tentable[tens//10]

return minute_string

singledigit = testminstring(5) == "oh five"

teens = testminstring(15) == "fifteen"

justtens = testminstring(20) == "twenty"

tensandones = testminstring(35) == "thirty five"

doesnone = testminstring(0) == ""

results = (("test","passed"),
("Does single digits: ", singledigit),
("Does teens: ", teens),
("Does just tens: ", justtens),
("Does tens: ", tensandones),
("Does zero: ", doesnone)
)

return results
``````

This test used some hard coded values in it, unlike the hours test. However I needed to test for some specific conditions, and I figured
a less robust test would work here better. Like with my hour tests, you could likely have it test all possible valid inputs with a list comp.
But in this case, I'm not sure that it would be worth the effort.

### Finding the period

To find the period, (am or pm) is a pretty simple task.
This is done by taking the hour variable, and checking if it is greater than or equal to 12.
If so the period is pm, otherwise it is am.
What is important is that this piece must be run before getting the hour string, because the hour variable is change from that operation.
This could be mitgated by storing the original value, however I think that would just be overcomplicating the issue.

``````if hour >= 12:
period = "pm"
else:
period = "am"

def testgetperiod(hour):
if hour >= 12:
period = "pm"
else:
period = "am"
return period

amworks = all([testgetperiod(t) == 'am' for t in range(0,12)])
pmworks = all([testgetperiod(t) == 'pm' for t in range(12,23)])

results = (("test","passed"),
("Am works: ",amworks),
("Pm works: ",pmworks),
)

return results
``````

With a part this simple, tests really aren't needed. But I want to stay true to the bleepings of the testing goat.

## Putting it all together

Now that we have all the dependant parts, we just need to string them together in the right order.
The main thing I look for here is if the minute string is empty or not.
If it is, there should only be one space between the hour and the period.
In one of my last attempts at this problem, I got tripped up at this stage, hense my caution.

``````def talkingclock(time):
hour,minute = map(int, time.split(":"))
if hour >= 12:
period = "pm"
else:
period = "am"
numnames = (
'one','two','three',
'four','five','six',
'seven','eight','nine',
'ten','eleven','twelve')

if hour > 12:
hour -= 12
else:
pass

hour_string = numnames[hour-1]
minute_string = ""

onetable = ("one","two","three",
"four", "five", "six",
"seven","eight","nine")

teentable = ("ten","eleven","tweleve","thirteen",
"fourteen", "fifteen","sixteen",
"seventeen","eighteen","nineteen")

tentable = ("","ten","twenty","thirty","forty","fifty",)

if minute in range(1,10):
minute_string = "oh " + onetable[minute-1]
elif minute in range(10,20):
minute_string = teentable[minute-10]

elif minute in range(20,60):
ones = minute % 10
tens = minute - ones
if ones > 0:
minute_string = tentable[tens//10] + " " + onetable[ones-1]
else:
minute_string = tentable[tens//10]

output = "It's " + hour_string + ' '
if minute_string == '':
output += period
else:
output += minute_string + ' ' + period

return output
``````

Now with this version of the program, the output string is not printed to the
stdout. I left that out for a few reasons. If I decide to do the optional challenge
in the future, printing directly to the stdout won't be super helpful.
Second, I wanted to keep the tests as simple as possible. If you notice,
the above section never import code, this includes unittest.
Unittest is a super helpful framework for more complicated tests, but I haven't
figured out how to use it to make pretty tables. I may look into using nose in the future, but I haven't quite got there yet.