Avatar of paulfioravanti

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, [] )

        headWord =
            numberWords
                |> Dict.get headDigit
                |> 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

        head =
            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" )
        ]

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?