From d12d85449c1e8fe89fe85c0cf18e96dc0ccbe213 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Sat, 23 Dec 2023 22:11:14 +1100 Subject: [PATCH] Add isogram exercise (#253) --- config.json | 11 ++ .../practice/isogram/.docs/instructions.md | 14 ++ exercises/practice/isogram/.meta/config.json | 19 +++ exercises/practice/isogram/.meta/example.sml | 28 +++ exercises/practice/isogram/.meta/tests.toml | 52 ++++++ exercises/practice/isogram/isogram.sml | 2 + exercises/practice/isogram/test.sml | 54 ++++++ exercises/practice/isogram/testlib.sml | 160 ++++++++++++++++++ 8 files changed, 340 insertions(+) create mode 100644 exercises/practice/isogram/.docs/instructions.md create mode 100644 exercises/practice/isogram/.meta/config.json create mode 100644 exercises/practice/isogram/.meta/example.sml create mode 100644 exercises/practice/isogram/.meta/tests.toml create mode 100644 exercises/practice/isogram/isogram.sml create mode 100644 exercises/practice/isogram/test.sml create mode 100644 exercises/practice/isogram/testlib.sml diff --git a/config.json b/config.json index 409ecc4..15d55ed 100644 --- a/config.json +++ b/config.json @@ -313,6 +313,17 @@ "difficulty": 1, "topics": [] }, + { + "slug": "isogram", + "name": "Isogram", + "uuid": "ae8fa578-1308-44b2-8e71-d7566553379c", + "practices": [], + "prerequisites": [], + "difficulty": 3, + "topics": [ + "strings" + ] + }, { "slug": "pig-latin", "name": "Pig Latin", diff --git a/exercises/practice/isogram/.docs/instructions.md b/exercises/practice/isogram/.docs/instructions.md new file mode 100644 index 0000000..2e8df85 --- /dev/null +++ b/exercises/practice/isogram/.docs/instructions.md @@ -0,0 +1,14 @@ +# Instructions + +Determine if a word or phrase is an isogram. + +An isogram (also known as a "non-pattern word") is a word or phrase without a repeating letter, however spaces and hyphens are allowed to appear multiple times. + +Examples of isograms: + +- lumberjacks +- background +- downstream +- six-year-old + +The word _isograms_, however, is not an isogram, because the s repeats. diff --git a/exercises/practice/isogram/.meta/config.json b/exercises/practice/isogram/.meta/config.json new file mode 100644 index 0000000..56b4b6b --- /dev/null +++ b/exercises/practice/isogram/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "isogram.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "Determine if a word or phrase is an isogram.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Isogram" +} diff --git a/exercises/practice/isogram/.meta/example.sml b/exercises/practice/isogram/.meta/example.sml new file mode 100644 index 0000000..5cc03ca --- /dev/null +++ b/exercises/practice/isogram/.meta/example.sml @@ -0,0 +1,28 @@ +val isIsogram: string -> bool = + let + val a = Char.ord #"a" + + (* We represent each letter by setting a different bit, + * for example the letter c is encoded as ...00100 binary. + *) + fun letterValue (c: char): word = + Word.<<(0wx1, Word.fromInt(Char.ord (Char.toLower c) - a)) + + (* seen represents the set of letters that have been seen + * so far. For example, if we have seen the letters d and c, + * this is encoded as ...01100 binary. + * If we find a letter in l that matches a letter already + * seen, we return false. + *) + fun recurse (seen: word) (l: char list): bool = + case l of + nil => true + | c :: rest => + let + val seen' = Word.orb (seen, letterValue c) + in + seen' <> seen andalso recurse seen' rest + end + in + recurse 0wx0 o List.filter Char.isAlpha o String.explode + end diff --git a/exercises/practice/isogram/.meta/tests.toml b/exercises/practice/isogram/.meta/tests.toml new file mode 100644 index 0000000..ba04c66 --- /dev/null +++ b/exercises/practice/isogram/.meta/tests.toml @@ -0,0 +1,52 @@ +# 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. + +[a0e97d2d-669e-47c7-8134-518a1e2c4555] +description = "empty string" + +[9a001b50-f194-4143-bc29-2af5ec1ef652] +description = "isogram with only lower case characters" + +[8ddb0ca3-276e-4f8b-89da-d95d5bae78a4] +description = "word with one duplicated character" + +[6450b333-cbc2-4b24-a723-0b459b34fe18] +description = "word with one duplicated character from the end of the alphabet" + +[a15ff557-dd04-4764-99e7-02cc1a385863] +description = "longest reported english isogram" + +[f1a7f6c7-a42f-4915-91d7-35b2ea11c92e] +description = "word with duplicated character in mixed case" + +[14a4f3c1-3b47-4695-b645-53d328298942] +description = "word with duplicated character in mixed case, lowercase first" + +[423b850c-7090-4a8a-b057-97f1cadd7c42] +description = "hypothetical isogrammic word with hyphen" + +[93dbeaa0-3c5a-45c2-8b25-428b8eacd4f2] +description = "hypothetical word with duplicated character following hyphen" + +[36b30e5c-173f-49c6-a515-93a3e825553f] +description = "isogram with duplicated hyphen" + +[cdabafa0-c9f4-4c1f-b142-689c6ee17d93] +description = "made-up name that is an isogram" + +[5fc61048-d74e-48fd-bc34-abfc21552d4d] +description = "duplicated character in the middle" + +[310ac53d-8932-47bc-bbb4-b2b94f25a83e] +description = "same first and last characters" + +[0d0b8644-0a1e-4a31-a432-2b3ee270d847] +description = "word with duplicated character and with two hyphens" diff --git a/exercises/practice/isogram/isogram.sml b/exercises/practice/isogram/isogram.sml new file mode 100644 index 0000000..3b5be96 --- /dev/null +++ b/exercises/practice/isogram/isogram.sml @@ -0,0 +1,2 @@ +fun isIsogram s = + raise Fail "'isIsogram' is not implemented" diff --git a/exercises/practice/isogram/test.sml b/exercises/practice/isogram/test.sml new file mode 100644 index 0000000..0d3c1aa --- /dev/null +++ b/exercises/practice/isogram/test.sml @@ -0,0 +1,54 @@ +(* version 1.0.0 *) + +use "testlib.sml"; +use "isogram.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "isogram" [ + test "empty string" + (fn _ => isIsogram "" |> Expect.truthy), + + test "isogram with only lower case characters" + (fn _ => isIsogram "isogram" |> Expect.truthy), + + test "word with one duplicated character" + (fn _ => isIsogram "eleven" |> Expect.falsy), + + test "word with one duplicated character from the end of the alphabet" + (fn _ => isIsogram "zzyzx" |> Expect.falsy), + + test "longest reported english isogram" + (fn _ => isIsogram "subdermatoglyphic" |> Expect.truthy), + + test "word with duplicated character in mixed case" + (fn _ => isIsogram "Alphabet" |> Expect.falsy), + + test "word with duplicated character in mixed case, lowercase first" + (fn _ => isIsogram "alphAbet" |> Expect.falsy), + + test "hypothetical isogrammic word with hyphen" + (fn _ => isIsogram "thumbscrew-japingly" |> Expect.truthy), + + test "hypothetical word with duplicated character following hyphen" + (fn _ => isIsogram "thumbscrew-jappingly" |> Expect.falsy), + + test "isogram with duplicated hyphen" + (fn _ => isIsogram "six-year-old" |> Expect.truthy), + + test "made-up name that is an isogram" + (fn _ => isIsogram "Emily Jung Schwartzkopf" |> Expect.truthy), + + test "duplicated character in the middle" + (fn _ => isIsogram "accentor" |> Expect.falsy), + + test "same first and last characters" + (fn _ => isIsogram "angola" |> Expect.falsy), + + test "word with duplicated character and with two hyphens" + (fn _ => isIsogram "up-to-date" |> Expect.falsy) + ] + +val _ = Test.run testsuite diff --git a/exercises/practice/isogram/testlib.sml b/exercises/practice/isogram/testlib.sml new file mode 100644 index 0000000..0c8370c --- /dev/null +++ b/exercises/practice/isogram/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)