 paulfioravanti's solution

to Say in the Elm Track

Published at Aug 05 2019 · 0 comments
Instructions
Test suite
Solution

Given a number from 0 to 999,999,999,999, spell out that number in English.

Step 1

Handle the basic case of 0 through 99.

If the input to the program is 22, then the output should be 'twenty-two'.

Your program should complain loudly if given a number outside the blessed range.

Some good test cases for this program are:

• 0
• 14
• 50
• 98
• -1
• 100

Extension

If you're on a Mac, shell out to Mac OS X's say program to talk out loud. If you're on Linux or Windows, eSpeakNG may be available with the command espeak.

Step 2

Implement breaking a number up into chunks of thousands.

So 1234567890 should yield a list like 1, 234, 567, and 890, while the far simpler 1000 should yield just 1 and 0.

The program must also report any values that are out of range.

Step 3

Now handle inserting the appropriate scale word between those chunks.

So 1234567890 should yield '1 billion 234 million 567 thousand 890'

The program must also report any values that are out of range. It's fine to stop at "trillion".

Step 4

Put it all together to get nothing but plain English.

12345 should give twelve thousand three hundred forty-five.

The program must also report any values that are out of range.

Extensions

Use and (correctly) when spelling out the number in English:

• 14 becomes "fourteen".
• 100 becomes "one hundred".
• 120 becomes "one hundred and twenty".
• 1002 becomes "one thousand and two".
• 1323 becomes "one thousand three hundred and twenty-three".

Elm Installation

Refer to the Installing Elm page for information about installing elm.

Writing the Code

The first time you start an exercise, you'll need to ensure you have the appropriate dependencies installed. Thankfully, Elm makes that easy for you and will install dependencies when you try to run tests or build the code.

Execute the tests with:

$elm-test Automatically run tests again when you save changes:$ elm-test --watch

As you work your way through the test suite, be sure to remove the skip <| calls from each test until you get them all passing!

Source

A variation on JavaRanch CattleDrive, exercise 4a http://www.javaranch.com/say.jsp

Submitting Incomplete Solutions

It is possible to submit an incomplete solution so you can see how others have completed the exercise.

Tests.elm

module Tests exposing (tests)

import Expect
import Say exposing (SayError(..), say)
import Test exposing (..)

tests : Test
tests =
describe "Series"
[ test "one" <|
\() ->
Expect.equal (Ok "one")
(say 1)
, skip <|
test "fourteen" <|
\() ->
Expect.equal (Ok "fourteen")
(say 14)
, skip <|
test "twenty" <|
\() ->
Expect.equal (Ok "twenty")
(say 20)
, skip <|
test "twenty-two" <|
\() ->
Expect.equal (Ok "twenty-two")
(say 22)
, skip <|
test "one hundred" <|
\() ->
Expect.equal (Ok "one hundred")
(say 100)
, skip <|
test "one hundred twenty" <|
\() ->
Expect.equal (Ok "one hundred and twenty")
(say 120)
, skip <|
test "one hundred twenty-three" <|
\() ->
Expect.equal (Ok "one hundred and twenty-three")
(say 123)
, skip <|
test "one thousand" <|
\() ->
Expect.equal (Ok "one thousand")
(say 1000)
, skip <|
test "one thousand two hundred thirty-four" <|
\() ->
Expect.equal (Ok "one thousand two hundred and thirty-four")
(say 1234)
, skip <|
test "one million" <|
\() ->
Expect.equal (Ok "one million")
(say 1000000)
, skip <|
test "one million two" <|
\() ->
Expect.equal (Ok "one million and two")
(say 1000002)
, skip <|
test "1002345" <|
\() ->
Expect.equal (Ok "one million two thousand three hundred and forty-five")
(say 1002345)
, skip <|
test "one billion" <|
\() ->
Expect.equal (Ok "one billion")
(say 1000000000)
, skip <|
test "number too large" <|
\() ->
Expect.equal (Err TooLarge)
(say 10000000000000000)
, skip <|
test "negative number" <|
\() ->
Expect.equal (Err Negative)
(say -42)
, skip <|
test "zero" <|
\() ->
Expect.equal (Ok "zero")
(say 0)
, skip <|
test "987654321123" <|
\() ->
Expect.equal
(Ok
("nine hundred and eighty-seven billion "
++ "six hundred and fifty-four million "
++ "three hundred and twenty-one thousand "
++ "one hundred and twenty-three"
)
)
(say 987654321123)
]
module Say exposing (SayError(..), say)

import Dict exposing (Dict)

type SayError
= Negative
| TooLarge

say : Int -> Result SayError String
say number =
if isTooSmall number then
Err Negative

else if isTooLarge number then
Err TooLarge

else if isZero number then
Ok "zero"

else if isUpToTwenty number then
numberWords
|> Dict.get number
|> Result.fromMaybe Negative

else if isUpToNinetyNine number then
Ok (hypenatedWord number)

else
Ok (fullWord number)

-- PRIVATE

hypenatedWord : Int -> String
hypenatedWord number =
let
onesValue =
case digits [] number of
_ :: ones :: [] ->
ones

_ ->
0

tensWord =
numberWords
|> Dict.get (number - onesValue)
|> Maybe.withDefault ""

onesWord =
numberWords
|> Dict.get onesValue
|> Maybe.withDefault ""
in
tensWord ++ "-" ++ onesWord

fullWord : Int -> String
fullWord number =
if isZero number then
""

else if isUpToTwenty number then
numberWords
|> Dict.get number
|> Maybe.withDefault ""

else if isUpToNinetyNine number then
hypenatedWord number

else if isTenThousandUpToOneMillion number then
splitListByScale 3 number

else if isTenMillionUpToOneBillion number then
splitListByScale 6 number

else if isTenBillionUpToOneTrillion number then
splitListByScale 9 number

else
constructFullWord number

constructFullWord : Int -> String
constructFullWord number =
let
( headDigit, tailDigits ) =
case digits [] number of
head :: tail ->
( head, tail )

_ ->
( 0, [] )

numberWords
|> Maybe.withDefault ""

scale =
scales
|> Dict.get (List.length tailDigits)
|> Maybe.withDefault ""

tailWords =
tailDigits
|> undigits 0
|> fullWord
|> formatTailWord tailDigits
in
headWord ++ " " ++ scale ++ tailWords

formatTailWord : List Int -> String -> String
formatTailWord tail word =
let
numRemainingNonZeroDigits =
tail
|> dropWhile (\int -> int == 0)
|> List.length
in
case numRemainingNonZeroDigits of
0 ->
""

1 ->
" and " ++ word

2 ->
" and " ++ word

_ ->
" " ++ word

splitListByScale : Int -> Int -> String
splitListByScale scale number =
let
digitList =
digits [] number

scaleChunk =
List.length digitList - scale

digitList
|> List.take scaleChunk
|> undigits 0
|> fullWord

tail =
digitList
|> List.drop scaleChunk
|> undigits 0
|> fullWord

scaleWord =
scales
|> Dict.get scale
|> Maybe.withDefault ""
in
head ++ " " ++ scaleWord ++ " " ++ tail

digits : List Int -> Int -> List Int
digits acc int =
let
base =
10
in
if abs int < base then
int :: acc

else
let
{- Integer division won't work because of this weirdness:
> 987654321123 // 10
-18815696
-}
flooredInt =
floor (toFloat int / base)
in
digits (remainderBy base int :: acc) flooredInt

undigits : Int -> List Int -> Int
undigits acc digitList =
let
base =
10
in
case digitList of
[] ->
acc

head :: tail ->
undigits (acc * base + head) tail

dropWhile : (a -> Bool) -> List a -> List a
dropWhile predicate list =
case list of
[] ->
[]

head :: tail ->
if predicate head then
dropWhile predicate tail

else
list

isTooSmall : Int -> Bool
isTooSmall number =
number < 0

isTooLarge : Int -> Bool
isTooLarge number =
number > 999999999999

isZero : Int -> Bool
isZero number =
number == 0

isUpToTwenty : Int -> Bool
isUpToTwenty number =
number < 21

isUpToNinetyNine : Int -> Bool
isUpToNinetyNine number =
number < 100

isTenThousandUpToOneMillion : Int -> Bool
isTenThousandUpToOneMillion number =
number > 9999 && number < 1000000

isTenMillionUpToOneBillion : Int -> Bool
isTenMillionUpToOneBillion number =
number > 9999999 && number < 1000000000

isTenBillionUpToOneTrillion : Int -> Bool
isTenBillionUpToOneTrillion number =
number > 9999999999 && number < 1000000000000

scales : Dict Int String
scales =
Dict.fromList
[ ( 2, "hundred" )
, ( 3, "thousand" )
, ( 6, "million" )
, ( 9, "billion" )
]

numberWords : Dict Int String
numberWords =
Dict.fromList
[ ( 1, "one" )
, ( 2, "two" )
, ( 3, "three" )
, ( 4, "four" )
, ( 5, "five" )
, ( 6, "six" )
, ( 7, "seven" )
, ( 8, "eight" )
, ( 9, "nine" )
, ( 10, "ten" )
, ( 11, "eleven" )
, ( 12, "twelve" )
, ( 13, "thirteen" )
, ( 14, "fourteen" )
, ( 15, "fifteen" )
, ( 16, "sixteen" )
, ( 17, "seventeen" )
, ( 18, "eighteen" )
, ( 19, "nineteen" )
, ( 20, "twenty" )
, ( 30, "thirty" )
, ( 40, "forty" )
, ( 50, "fifty" )
, ( 60, "sixty" )
, ( 70, "seventy" )
, ( 80, "eighty" )
, ( 90, "ninety" )
]