DEV Community

D Smith
D Smith

Posted on

Talking Clock Challenge

Table of Contents

  1. Introduction
  2. Description
    1. Input Description
    2. Output Description
    3. Optional Challenge
  3. My Solution
    1. Functional Test
    2. Parsing the input
    3. Converting intigers to words
      1. Getting Hour String
      2. Getting Minute String
      3. Finding the period
    4. Putting it all together

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
trade off.

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.

Top comments (0)