15
\$\begingroup\$

I wrote a converter for English, then tried to adapt the code to work for French.

I'm wondering mainly whether any simplifications are possible.

In English

English is relatively straightforward:

english :: Int -> String
english n
  | n == 0    = "zero"
  | n < 0     = "negative " ++ (english (-n))
  | otherwise = kScale 0 $ map wordsTo1k $ splitk $ littleEndianDigits n
    where
      littleEndianDigits n
        | n <  0    = error "Negative"
        | n < 10    = [n]
        | otherwise = (n `mod` 10) : (littleEndianDigits (n `div` 10))

      splitk []           = []
      splitk (i:[])       = [(0, 0, i)]
      splitk (i:x:[])     = [(0, x, i)]
      splitk (i:x:c:n)    = (c, x, i) : (splitk n)

      wordsTo1k (0, 0, i) = digits !! i
      wordsTo1k (0, x, 0) = tens   !! x
      wordsTo1k (0, 1, i) = teens  !! i
      wordsTo1k (0, x, i) = (wordsTo1k (0, x, 0)) ++ "-" ++ (wordsTo1k (0, 0, i))
      wordsTo1k (c, 0, 0) = (digits !! c) ++ " hundred"
      wordsTo1k (c, x, i) = (wordsTo1k (c, 0, 0)) ++ " " ++ (wordsTo1k (0, x, i))

      kScale s []  = []
      kScale s ("zero":gs) = kScale (s + 1) gs
      kScale s (g:gs)      = unwords' [ kScale (s + 1) gs, g, kScales !! s ]

      unwords' (a:[])     = a
      unwords' (a:[""])   = a
      unwords' ([]:b)     = unwords' b
      unwords' (a:b)      = a ++ " " ++ unwords' b

      digits = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
      tens   = [error "zero", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"]
      teens  = [error "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"]
      kScales = [[], "thousand", "million", "billion", "trillion", "quadrillion", "quintillion"]

En français

I've tried to stick to "standard" French, using the style guide from Le Figaro and these rules for large numbers.

Relative to English, complications include:

  • Numbers 61 to 99
  • Pluralization inflections: 100 is cent, 200 is deux cents, but 201 loses the "s" (deux cent un). "Thousand" is mille, and it is invariable — 2000 is deux mille.
  • "One hundred" is just cent, "one thousand" is mille, but "one million" is un million.
  • Large numbers use the long scale.

I hope I have interpreted the rules correctly!

french :: Int -> String
french n
  | n == 0    = "zéro"
  | n < 0     = "moins " ++ (french (-n))
  | otherwise = mScale 0 $ map wordsTo1M $ group2 $ splitk $ littleEndianDigits n
    where
      littleEndianDigits n
        | n <  0    = error "Negative"
        | n < 10    = [n]
        | otherwise = (n `mod` 10) : (littleEndianDigits (n `div` 10))

      splitk []           = []
      splitk (i:[])       = [(0, 0, i)]
      splitk (i:x:[])     = [(0, x, i)]
      splitk (i:x:c:n)    = (c, x, i) : (splitk n)

      wordsTo1k (0, 0, i) = digits !! i
      wordsTo1k (0, 8, 0) = (tens  !! 8) ++ "s"
      wordsTo1k (0, x, 0) = tens   !! x
      wordsTo1k (0, 1, i) = teens  !! i

      wordsTo1k (0, 7, 1) = (tens !! 6) ++ "-et-" ++ (wordsTo1k (0, 1, 1))
      wordsTo1k (0, 7, i) = (tens !! 6) ++ "-"    ++ (wordsTo1k (0, 1, i))
      wordsTo1k (0, 8, i) = (tens !! 8) ++ "-"    ++ (wordsTo1k (0, 0, i))
      wordsTo1k (0, 9, i) = (tens !! 8) ++ "-"    ++ (wordsTo1k (0, 1, i))

      wordsTo1k (0, x, 1) = (wordsTo1k (0, x, 0)) ++ "-et-" ++ (wordsTo1k (0, 0, 1))
      wordsTo1k (0, x, i) = (wordsTo1k (0, x, 0)) ++ "-"    ++ (wordsTo1k (0, 0, i))

      wordsTo1k (1, 0, 0) = "cent"
      wordsTo1k (1, x, i) = "cent " ++ (wordsTo1k (0, x, i))
      wordsTo1k (c, 0, 0) = (digits !! c) ++ " cents"
      wordsTo1k (c, x, i) = (digits !! c) ++ " cent " ++ (wordsTo1k (0, x, i))

      group2 []         = []
      group2 (k0:[])    = ((0, 0, 0), k0) : []
      group2 (k0:k1:ks) = (k1, k0) : group2 ks

      wordsTo1M ((0, 0, 0), ones)      = wordsTo1k ones
      wordsTo1M ((0, 0, 1), (0, 0, 0)) = "mille"
      wordsTo1M ((0, 0, 1), ones)      = "mille " ++ (wordsTo1k ones)
      wordsTo1M (thousands, (0, 0, 0)) = (wordsTo1k thousands) ++ " mille"
      wordsTo1M (thousands, ones)      = (wordsTo1k thousands) ++ " mille " ++ (wordsTo1k ones)

      mScale s []  = []
      mScale s ("zéro":gs) = mScale (s + 1) gs
      mScale s (g:gs)
        | s == 0 || g == "un"  = unwords' [ mScale (s + 1) gs, g, mScales !! s ]
        | otherwise            = unwords' [ mScale (s + 1) gs, g, mScales !! s ++ "s" ]

      unwords' (a:[])     = a
      unwords' (a:[""])   = a
      unwords' ([]:b)     = unwords' b
      unwords' (a:b)      = a ++ " " ++ unwords' b

      digits = ["zéro", "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf"]
      tens   = [error "zero", "dix", "vingt", "trente", "quarante", "cinquante", "soixante", "soixante-dix", "quatre-vingt", "quatre-vingt-dix"]
      teens  = [error "ten", "onze", "douze", "treize", "quatorze", "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf"]
      mScales = [[], "million", "billion", "trillion"]

Sample output

*Main> map (\n -> (n, english n)) [1,2,10,11,100,101,1000,1001,2000,100000,1000000,2000000,2000001,1000000000,1000600003,2001000003000,-123456789,maxBound::Int]
[(1,"one"),
 (2,"two"),
 (10,"ten"),
 (11,"eleven"),
 (100,"one hundred"),
 (101,"one hundred one"),
 (1000,"one thousand"),
 (1001,"one thousand one"),
 (2000,"two thousand"),
 (100000,"one hundred thousand"),
 (1000000,"one million"),
 (2000000,"two million"),
 (2000001,"two million one"),
 (1000000000,"one billion"),
 (1000600003,"one billion six hundred thousand three"),
 (2001000003000,"two trillion one billion three thousand"),
 (-123456789,"negative one hundred twenty-three million four hundred fifty-six thousand seven hundred eighty-nine"),
 (9223372036854775807,"nine quintillion two hundred twenty-three quadrillion three hundred seventy-two trillion thirty-six billion eight hundred fifty-four million seven hundred seventy-five thousand eight hundred seven")]
*Main> map (\n -> (n, french n)) [1,2,10,11,100,101,1000,1001,2000,100000,1000000,2000000,2000001,1000000000,1000600003,2001000003000,-123456789,maxBound::Int]
[(1,"un"),
 (2,"deux"),
 (10,"dix"),
 (11,"onze"),
 (100,"cent"),
 (101,"cent un"),
 (1000,"mille"),
 (1001,"mille un"),
 (2000,"deux mille"),
 (100000,"cent mille"),
 (1000000,"un million"),
 (2000000,"deux millions"),
 (2000001,"deux millions un"),
 (1000000000,"mille millions"),
 (1000600003,"mille millions six cents mille trois"),
 (2001000003000,"deux billions mille millions trois mille"),
 (-123456789,"moins cent vingt-trois millions quatre cent cinquante-six mille sept cent quatre-vingt-neuf"),
 (9223372036854775807,"neuf trillions deux cent vingt-trois mille trois cent soixante-douze billions trente-six mille huit cent cinquante-quatre millions sept cent soixante-quinze mille huit cent sept")]
\$\endgroup\$
  • \$\begingroup\$ What's the output for 87 and 97? For 87, it should be 4 * 20 + 7 and for 97 it should be 4 * 20 + 17. According to my memory and Google, 87 is quatre vingt sept and quatre vingt dix-sept for 97. \$\endgroup\$ – Ismael Miguel Sep 10 '15 at 9:13
  • \$\begingroup\$ @IsmaelMiguel The function outputs "quatre-vingt-sept" and "quatre-vingt-dix-sept". \$\endgroup\$ – 200_success Sep 10 '15 at 9:19
  • \$\begingroup\$ Nice! Question: Where's the double-upvote button? You really deserve it! You have a whole back-study on this! \$\endgroup\$ – Ismael Miguel Sep 10 '15 at 9:26
  • \$\begingroup\$ Does this handle 21, 31, 41 and so? For example, 21 is vingt et un but 22 is vingt-deux. \$\endgroup\$ – Ismael Miguel Sep 10 '15 at 9:34
  • \$\begingroup\$ @IsmaelMiguel The code carries an implicit claim to give the correct output for all Ints from minBound to maxBound. If you find a counterexample, please write an answer. \$\endgroup\$ – 200_success Sep 10 '15 at 9:50
9
\$\begingroup\$

I would move all this functions to the global level:

splitk []           = []
splitk (i:[])       = [(0, 0, i)]
splitk (i:x:[])     = [(0, x, i)]
splitk (i:x:c:n)    = (c, x, i) : (splitk n)

wordsTo1k (0, 0, i) = digits !! i
wordsTo1k (0, x, 0) = tens   !! x
wordsTo1k (0, 1, i) = teens  !! i
wordsTo1k (0, x, i) = (wordsTo1k (0, x, 0)) ++ "-" ++ (wordsTo1k (0, 0, i))
wordsTo1k (c, 0, 0) = (digits !! c) ++ " hundred"
wordsTo1k (c, x, i) = (wordsTo1k (c, 0, 0)) ++ " " ++ (wordsTo1k (0, x, i))

kScale s []  = []
kScale s ("zero":gs) = kScale (s + 1) gs
kScale s (g:gs)      = unwords' [ kScale (s + 1) gs, g, kScales !! s ]

unwords' (a:[])     = a
unwords' (a:[""])   = a
unwords' ([]:b)     = unwords' b
unwords' (a:b)      = a ++ " " ++ unwords' b

While it is true that these functions are related to numbers_to_words, putting them at global level increases testability.

Non-standard pattern-matching

splitk (i:[])       = [(0, 0, i)]
splitk (i:x:[])     = [(0, x, i)]

Is non standard, I suggest

splitk [i] = [(0, 0, i)]
splitk [i, x] = [(0, x, i)]

That is more readable.

Use the built-ins

You define your own unwords but there already exists one. Using it may cause extraneous spaces at the edges of the words, but trim is all it takes to remove those, like this:

trim :: String -> String
trim = f . f
   where f = reverse . dropWhile isSpace

And then:

kScale s (g:gs)      = trim $ unwords [ kScale (s + 1) gs, g, kScales !! s ]

Now you can safely delete your home-made unwords' and profit from the code shortening.

Always write explicit types

The Haskell compiler can infer (deduce) types for you. But I encourage you to write explicit typing all the time, as it:

  • Increases efficiency (inferred types are more generic and more slow).
  • Takes very little time (it took me less than a minute to write types for your functions).
  • Checks that you and the compiler think the same about the same piece of code.
  • Simplifies reading, mainly with curried definitions.

Use everyday names whenever possible

littleEndianDigits vaguely recalls me something about bit stored in a computer to represent integers, something like right to left or left to right. I was not sure about the order of the digits until I executed it. Instead if you name it reversedDigits people will understand that for example reversedDigits 123 == [3, 2, 1] in the blink of an eye.

Do not give names differing in one char only

tens  and teens

kScale and kScales

If you make a typo and switch one for the other debugging will be a nightmare. Chose an equally descriptive synonym.

Avoid abbreviations as often as possible

The meaning of kScales is not immediately clear as it is not the meaning of splitk. Using Thousands in full is sure more typing, but I think think that nobody would argue that it increases readibility.

\$\endgroup\$
2
\$\begingroup\$

Small error

This is not so much a code issue as a linguistic overlook (French numbers are quite tricky) but the "billion" part in this list should instead be "milliard" such that it translates as "un milliard" (one billion), "deux milliards" (two billion), etc.

 mScales = [[], "million", "billion", "trillion"]
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.