Skip to content

Commit

Permalink
Add isogram exercise (#253)
Browse files Browse the repository at this point in the history
  • Loading branch information
keiravillekode authored Dec 23, 2023
1 parent 23cfd80 commit d12d854
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 0 deletions.
11 changes: 11 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
14 changes: 14 additions & 0 deletions exercises/practice/isogram/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions exercises/practice/isogram/.meta/config.json
Original file line number Diff line number Diff line change
@@ -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"
}
28 changes: 28 additions & 0 deletions exercises/practice/isogram/.meta/example.sml
Original file line number Diff line number Diff line change
@@ -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
52 changes: 52 additions & 0 deletions exercises/practice/isogram/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 2 additions & 0 deletions exercises/practice/isogram/isogram.sml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
fun isIsogram s =
raise Fail "'isIsogram' is not implemented"
54 changes: 54 additions & 0 deletions exercises/practice/isogram/test.sml
Original file line number Diff line number Diff line change
@@ -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
160 changes: 160 additions & 0 deletions exercises/practice/isogram/testlib.sml
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit d12d854

Please sign in to comment.