diff --git a/bin/generate b/bin/generate index 094e717..71bd8ad 100755 --- a/bin/generate +++ b/bin/generate @@ -355,7 +355,7 @@ def generate(exercise, flags, problem_specs_source, force=False): write(path / ('%s.sml' % exercise), content) if flags & EXAMPLE: - write(path / 'example.sml', content) + write(path / '.meta/example.sml', content) shutil.copyfile( (root / 'lib/testlib.sml').as_posix(), diff --git a/config.json b/config.json index 9accbdc..36eb59e 100644 --- a/config.json +++ b/config.json @@ -617,6 +617,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "say", + "name": "Say", + "uuid": "f3bcc2f2-9752-4fb4-81f0-a790c0c2b3e6", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "transpose", "name": "Transpose", diff --git a/exercises/practice/say/.docs/instructions.md b/exercises/practice/say/.docs/instructions.md new file mode 100644 index 0000000..ad3d347 --- /dev/null +++ b/exercises/practice/say/.docs/instructions.md @@ -0,0 +1,48 @@ +# Instructions + +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. + +## 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. diff --git a/exercises/practice/say/.meta/config.json b/exercises/practice/say/.meta/config.json new file mode 100644 index 0000000..69b0000 --- /dev/null +++ b/exercises/practice/say/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "say.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "Given a number from 0 to 999,999,999,999, spell out that number in English.", + "source": "A variation on the JavaRanch CattleDrive, Assignment 4", + "source_url": "https://coderanch.com/wiki/718804" +} diff --git a/exercises/practice/say/.meta/example.sml b/exercises/practice/say/.meta/example.sml new file mode 100644 index 0000000..aa31cd4 --- /dev/null +++ b/exercises/practice/say/.meta/example.sml @@ -0,0 +1,59 @@ +val unitNames = Array.fromList [ + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + "eleven", + "twelve", + "thirteen", + "fourteen", + "fifteen", + "sixteen", + "seventeen", + "eighteen", + "nineteen" +] + +val decadeNames = Array.fromList [ + "zero", + "ten", + "twenty", + "thirty", + "forty", + "fifty", + "sixty", + "seventy", + "eighty", + "ninety" +] + +fun words (number: int): string list = + if number >= 1000000000 + then (words (number div 1000000000)) @ ("billion" :: (words (number mod 1000000000))) + else if number >= 1000000 + then (words (number div 1000000)) @ ("million" :: (words (number mod 1000000))) + else if number >= 1000 + then (words (number div 1000)) @ ("thousand" :: (words (number mod 1000))) + else if number >= 100 + then (words (number div 100)) @ ("hundred" :: (words (number mod 100))) + else if number = 0 + then [] + else if number < 20 + then [Array.sub (unitNames, number)] + else if (number mod 10) = 0 + then [Array.sub (decadeNames, number div 10)] + else [Array.sub (decadeNames, number div 10) ^ "-" ^ Array.sub (unitNames, number mod 10)] + +fun say (number: int): string = + if (number < 0) orelse (number > 999999999999) + then raise Fail "input out of range" + else if number = 0 + then "zero" + else String.concatWith " " (words number) diff --git a/exercises/practice/say/.meta/tests.toml b/exercises/practice/say/.meta/tests.toml new file mode 100644 index 0000000..a5532e9 --- /dev/null +++ b/exercises/practice/say/.meta/tests.toml @@ -0,0 +1,67 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[5d22a120-ba0c-428c-bd25-8682235d83e8] +description = "zero" + +[9b5eed77-dbf6-439d-b920-3f7eb58928f6] +description = "one" + +[7c499be1-612e-4096-a5e1-43b2f719406d] +description = "fourteen" + +[f541dd8e-f070-4329-92b4-b7ce2fcf06b4] +description = "twenty" + +[d78601eb-4a84-4bfa-bf0e-665aeb8abe94] +description = "twenty-two" + +[f010d4ca-12c9-44e9-803a-27789841adb1] +description = "thirty" + +[738ce12d-ee5c-4dfb-ad26-534753a98327] +description = "ninety-nine" + +[e417d452-129e-4056-bd5b-6eb1df334dce] +description = "one hundred" + +[d6924f30-80ba-4597-acf6-ea3f16269da8] +description = "one hundred twenty-three" + +[2f061132-54bc-4fd4-b5df-0a3b778959b9] +description = "two hundred" + +[feed6627-5387-4d38-9692-87c0dbc55c33] +description = "nine hundred ninety-nine" + +[3d83da89-a372-46d3-b10d-de0c792432b3] +description = "one thousand" + +[865af898-1d5b-495f-8ff0-2f06d3c73709] +description = "one thousand two hundred thirty-four" + +[b6a3f442-266e-47a3-835d-7f8a35f6cf7f] +description = "one million" + +[2cea9303-e77e-4212-b8ff-c39f1978fc70] +description = "one million two thousand three hundred forty-five" + +[3e240eeb-f564-4b80-9421-db123f66a38f] +description = "one billion" + +[9a43fed1-c875-4710-8286-5065d73b8a9e] +description = "a big number" + +[49a6a17b-084e-423e-994d-a87c0ecc05ef] +description = "numbers below zero are out of range" + +[4d6492eb-5853-4d16-9d34-b0f61b261fd9] +description = "numbers above 999,999,999,999 are out of range" diff --git a/exercises/practice/say/say.sml b/exercises/practice/say/say.sml new file mode 100644 index 0000000..cd97ce0 --- /dev/null +++ b/exercises/practice/say/say.sml @@ -0,0 +1,2 @@ +fun say (number: int): string = + raise Fail "'say' is not implemented" \ No newline at end of file diff --git a/exercises/practice/say/test.sml b/exercises/practice/say/test.sml new file mode 100644 index 0000000..b595869 --- /dev/null +++ b/exercises/practice/say/test.sml @@ -0,0 +1,67 @@ +use "testlib.sml"; +use "say.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "say" [ + test "zero" + (fn _ => say 0 |> Expect.equalTo "zero"), + + test "one" + (fn _ => say 1 |> Expect.equalTo "one"), + + test "fourteen" + (fn _ => say 14 |> Expect.equalTo "fourteen"), + + test "twenty" + (fn _ => say 20 |> Expect.equalTo "twenty"), + + test "twenty-two" + (fn _ => say 22 |> Expect.equalTo "twenty-two"), + + test "thirty" + (fn _ => say 30 |> Expect.equalTo "thirty"), + + test "ninety-nine" + (fn _ => say 99 |> Expect.equalTo "ninety-nine"), + + test "one hundred" + (fn _ => say 100 |> Expect.equalTo "one hundred"), + + test "one hundred twenty-three" + (fn _ => say 123 |> Expect.equalTo "one hundred twenty-three"), + + test "two hundred" + (fn _ => say 200 |> Expect.equalTo "two hundred"), + + test "nine hundred ninety-nine" + (fn _ => say 999 |> Expect.equalTo "nine hundred ninety-nine"), + + test "one thousand" + (fn _ => say 1000 |> Expect.equalTo "one thousand"), + + test "one thousand two hundred thirty-four" + (fn _ => say 1234 |> Expect.equalTo "one thousand two hundred thirty-four"), + + test "one million" + (fn _ => say 1000000 |> Expect.equalTo "one million"), + + test "one million two thousand three hundred forty-five" + (fn _ => say 1002345 |> Expect.equalTo "one million two thousand three hundred forty-five"), + + test "one billion" + (fn _ => say 1000000000 |> Expect.equalTo "one billion"), + + test "a big number" + (fn _ => say 987654321123 |> Expect.equalTo "nine hundred eighty-seven billion six hundred fifty-four million three hundred twenty-one thousand one hundred twenty-three"), + + test "numbers below zero are out of range" + (fn _ => (fn _ => say ~1) |> Expect.error (Fail "input out of range")), + + test "numbers above 999,999,999,999 are out of range" + (fn _ => (fn _ => say 1000000000000) |> Expect.error (Fail "input out of range")) + ] + +val _ = Test.run testsuite \ No newline at end of file diff --git a/exercises/practice/say/testlib.sml b/exercises/practice/say/testlib.sml new file mode 100644 index 0000000..0c8370c --- /dev/null +++ b/exercises/practice/say/testlib.sml @@ -0,0 +1,160 @@ +structure Expect = +struct + datatype expectation = Pass | Fail of string * string + + local + fun failEq b a = + Fail ("Expected: " ^ b, "Got: " ^ a) + + fun failExn b a = + Fail ("Expected: " ^ b, "Raised: " ^ a) + + fun exnName (e: exn): string = General.exnName e + in + fun truthy a = + if a + then Pass + else failEq "true" "false" + + fun falsy a = + if a + then failEq "false" "true" + else Pass + + fun equalTo b a = + if a = b + then Pass + else failEq (PolyML.makestring b) (PolyML.makestring a) + + fun nearTo delta b a = + if Real.abs (a - b) <= delta * Real.abs a orelse + Real.abs (a - b) <= delta * Real.abs b + then Pass + else failEq (Real.toString b ^ " +/- " ^ Real.toString delta) (Real.toString a) + + fun anyError f = + ( + f (); + failExn "an exception" "Nothing" + ) handle _ => Pass + + fun error e f = + ( + f (); + failExn (exnName e) "Nothing" + ) handle e' => if exnMessage e' = exnMessage e + then Pass + else failExn (exnMessage e) (exnMessage e') + end +end + +structure TermColor = +struct + datatype color = Red | Green | Yellow | Normal + + fun f Red = "\027[31m" + | f Green = "\027[32m" + | f Yellow = "\027[33m" + | f Normal = "\027[0m" + + fun colorize color s = (f color) ^ s ^ (f Normal) + + val redit = colorize Red + + val greenit = colorize Green + + val yellowit = colorize Yellow +end + +structure Test = +struct + datatype testnode = TestGroup of string * testnode list + | Test of string * (unit -> Expect.expectation) + + local + datatype evaluation = Success of string + | Failure of string * string * string + | Error of string * string + + fun indent n s = (implode (List.tabulate (n, fn _ => #" "))) ^ s + + fun fmt indentlvl ev = + let + val check = TermColor.greenit "\226\156\148 " (* ✔ *) + val cross = TermColor.redit "\226\156\150 " (* ✖ *) + val indentlvl = indentlvl * 2 + in + case ev of + Success descr => indent indentlvl (check ^ descr) + | Failure (descr, exp, got) => + String.concatWith "\n" [indent indentlvl (cross ^ descr), + indent (indentlvl + 2) exp, + indent (indentlvl + 2) got] + | Error (descr, reason) => + String.concatWith "\n" [indent indentlvl (cross ^ descr), + indent (indentlvl + 2) (TermColor.redit reason)] + end + + fun eval (TestGroup _) = raise Fail "Only a 'Test' can be evaluated" + | eval (Test (descr, thunk)) = + ( + case thunk () of + Expect.Pass => ((1, 0, 0), Success descr) + | Expect.Fail (s, s') => ((0, 1, 0), Failure (descr, s, s')) + ) + handle e => ((0, 0, 1), Error (descr, "Unexpected error: " ^ exnMessage e)) + + fun flatten depth testnode = + let + fun sum (x, y, z) (a, b, c) = (x + a, y + b, z + c) + + fun aux (t, (counter, acc)) = + let + val (counter', texts) = flatten (depth + 1) t + in + (sum counter' counter, texts :: acc) + end + in + case testnode of + TestGroup (descr, ts) => + let + val (counter, texts) = foldr aux ((0, 0, 0), []) ts + in + (counter, (indent (depth * 2) descr) :: List.concat texts) + end + | Test _ => + let + val (counter, evaluation) = eval testnode + in + (counter, [fmt depth evaluation]) + end + end + + fun println s = print (s ^ "\n") + in + fun run suite = + let + val ((succeeded, failed, errored), texts) = flatten 0 suite + + val summary = String.concatWith ", " [ + TermColor.greenit ((Int.toString succeeded) ^ " passed"), + TermColor.redit ((Int.toString failed) ^ " failed"), + TermColor.redit ((Int.toString errored) ^ " errored"), + (Int.toString (succeeded + failed + errored)) ^ " total" + ] + + val status = if failed = 0 andalso errored = 0 + then OS.Process.success + else OS.Process.failure + + in + List.app println texts; + println ""; + println ("Tests: " ^ summary); + OS.Process.exit status + end + end +end + +fun describe description tests = Test.TestGroup (description, tests) +fun test description thunk = Test.Test (description, thunk)