diff --git a/.gitignore b/.gitignore index bcacf394c..4c4e20b40 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ bin/configlet .psc-ide-port .merlin +_opam \ No newline at end of file diff --git a/config.json b/config.json index 0206ccb3e..ea0916287 100644 --- a/config.json +++ b/config.json @@ -590,6 +590,14 @@ "practices": [], "prerequisites": [], "difficulty": 2 + }, + { + "slug": "collatz-conjecture", + "name": "Collatz Conjecture", + "uuid": "eedc9471-326b-4498-b763-5b1c8eedca88", + "practices": [], + "prerequisites": [], + "difficulty": 3 } ] }, diff --git a/exercises/practice/anagram/test.ml b/exercises/practice/anagram/test.ml index 545f17b08..4130e672f 100644 --- a/exercises/practice/anagram/test.ml +++ b/exercises/practice/anagram/test.ml @@ -10,8 +10,6 @@ let tests = [ "no matches" >:: ae [] (anagrams "diaper" ["hello"; "world"; "zombies"; "pants"]); "detects two anagrams" >:: - ae ["stream"; "maters"] (anagrams "master" ["stream"; "pigeon"; "maters"]); - "detects two anagrams" >:: ae ["lemons"; "melons"] (anagrams "solemn" ["lemons"; "cherry"; "melons"]); "does not detect anagram subsets" >:: ae [] (anagrams "good" ["dog"; "goody"]); @@ -33,8 +31,6 @@ let tests = [ ae [] (anagrams "go" ["go Go GO"]); "anagrams must use all letters exactly once" >:: ae [] (anagrams "tapper" ["patter"]); - "words are not anagrams of themselves (case-insensitive)" >:: - ae [] (anagrams "BANANA" ["BANANA"; "Banana"; "banana"]); "words are not anagrams of themselves" >:: ae [] (anagrams "BANANA" ["BANANA"]); "words are not anagrams of themselves even if letter case is partially different" >:: @@ -42,8 +38,6 @@ let tests = [ "words are not anagrams of themselves even if letter case is completely different" >:: ae [] (anagrams "BANANA" ["banana"]); "words other than themselves can be anagrams" >:: - ae ["Silent"] (anagrams "LISTEN" ["Listen"; "Silent"; "LISTEN"]); - "words other than themselves can be anagrams" >:: ae ["Silent"] (anagrams "LISTEN" ["LISTEN"; "Silent"]); ] diff --git a/exercises/practice/collatz-conjecture/.docs/instructions.md b/exercises/practice/collatz-conjecture/.docs/instructions.md new file mode 100644 index 000000000..af332a810 --- /dev/null +++ b/exercises/practice/collatz-conjecture/.docs/instructions.md @@ -0,0 +1,3 @@ +# Instructions + +Given a positive integer, return the number of steps it takes to reach 1 according to the rules of the Collatz Conjecture. diff --git a/exercises/practice/collatz-conjecture/.docs/introduction.md b/exercises/practice/collatz-conjecture/.docs/introduction.md new file mode 100644 index 000000000..c35bdeb67 --- /dev/null +++ b/exercises/practice/collatz-conjecture/.docs/introduction.md @@ -0,0 +1,28 @@ +# Introduction + +One evening, you stumbled upon an old notebook filled with cryptic scribbles, as though someone had been obsessively chasing an idea. +On one page, a single question stood out: **Can every number find its way to 1?** +It was tied to something called the **Collatz Conjecture**, a puzzle that has baffled thinkers for decades. + +The rules were deceptively simple. +Pick any positive integer. + +- If it's even, divide it by 2. +- If it's odd, multiply it by 3 and add 1. + +Then, repeat these steps with the result, continuing indefinitely. + +Curious, you picked number 12 to test and began the journey: + +12 ➜ 6 ➜ 3 ➜ 10 ➜ 5 ➜ 16 ➜ 8 ➜ 4 ➜ 2 ➜ 1 + +Counting from the second number (6), it took 9 steps to reach 1, and each time the rules repeated, the number kept changing. +At first, the sequence seemed unpredictable — jumping up, down, and all over. +Yet, the conjecture claims that no matter the starting number, we'll always end at 1. + +It was fascinating, but also puzzling. +Why does this always seem to work? +Could there be a number where the process breaks down, looping forever or escaping into infinity? +The notebook suggested solving this could reveal something profound — and with it, fame, [fortune][collatz-prize], and a place in history awaits whoever could unlock its secrets. + +[collatz-prize]: https://mathprize.net/posts/collatz-conjecture/ diff --git a/exercises/practice/collatz-conjecture/.meta/config.json b/exercises/practice/collatz-conjecture/.meta/config.json new file mode 100644 index 000000000..6359bbe8c --- /dev/null +++ b/exercises/practice/collatz-conjecture/.meta/config.json @@ -0,0 +1,22 @@ +{ + "authors": [ + "therealowenrees" + ], + "files": { + "solution": [ + "collatz_conjecture.ml" + ], + "test": [ + "test.ml" + ], + "example": [ + ".meta/example.ml" + ], + "editor": [ + "collatz_conjecture.mli" + ] + }, + "blurb": "Calculate the number of steps to reach 1 using the Collatz conjecture.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Collatz_conjecture" +} diff --git a/exercises/practice/collatz-conjecture/.meta/example.ml b/exercises/practice/collatz-conjecture/.meta/example.ml new file mode 100644 index 000000000..a2f934c2e --- /dev/null +++ b/exercises/practice/collatz-conjecture/.meta/example.ml @@ -0,0 +1,13 @@ +let collatz_conjecture n = + let rec aux n count = + match n with + | 1 -> Ok count + | _ -> + (match n mod 2 with + | 0 -> aux (n / 2) (count + 1) + | _ -> aux (n * 3 + 1) (count + 1) + ) + in + if n < 1 then Error "Only positive integers are allowed" + else aux n 0 + diff --git a/exercises/practice/collatz-conjecture/.meta/tests.toml b/exercises/practice/collatz-conjecture/.meta/tests.toml new file mode 100644 index 000000000..cc34e1684 --- /dev/null +++ b/exercises/practice/collatz-conjecture/.meta/tests.toml @@ -0,0 +1,38 @@ +# 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. + +[540a3d51-e7a6-47a5-92a3-4ad1838f0bfd] +description = "zero steps for one" + +[3d76a0a6-ea84-444a-821a-f7857c2c1859] +description = "divide if even" + +[754dea81-123c-429e-b8bc-db20b05a87b9] +description = "even and odd steps" + +[ecfd0210-6f85-44f6-8280-f65534892ff6] +description = "large number of even and odd steps" + +[7d4750e6-def9-4b86-aec7-9f7eb44f95a3] +description = "zero is an error" +include = false + +[2187673d-77d6-4543-975e-66df6c50e2da] +description = "zero is an error" +reimplements = "7d4750e6-def9-4b86-aec7-9f7eb44f95a3" + +[c6c795bf-a288-45e9-86a1-841359ad426d] +description = "negative value is an error" +include = false + +[ec11f479-56bc-47fd-a434-bcd7a31a7a2e] +description = "negative value is an error" +reimplements = "c6c795bf-a288-45e9-86a1-841359ad426d" diff --git a/exercises/practice/collatz-conjecture/Makefile b/exercises/practice/collatz-conjecture/Makefile new file mode 100644 index 000000000..b71d6af2d --- /dev/null +++ b/exercises/practice/collatz-conjecture/Makefile @@ -0,0 +1,9 @@ +default: clean test + +test: + dune runtest + +clean: + dune clean + +.PHONY: clean diff --git a/exercises/practice/collatz-conjecture/collatz_conjecture.ml b/exercises/practice/collatz-conjecture/collatz_conjecture.ml new file mode 100644 index 000000000..059e119ed --- /dev/null +++ b/exercises/practice/collatz-conjecture/collatz_conjecture.ml @@ -0,0 +1,2 @@ +let collatz_conjecture _ = + failwith "'collatz_conjecture' is missing" \ No newline at end of file diff --git a/exercises/practice/collatz-conjecture/collatz_conjecture.mli b/exercises/practice/collatz-conjecture/collatz_conjecture.mli new file mode 100644 index 000000000..5eb7c6742 --- /dev/null +++ b/exercises/practice/collatz-conjecture/collatz_conjecture.mli @@ -0,0 +1 @@ +val collatz_conjecture : int -> (int, string) result \ No newline at end of file diff --git a/exercises/practice/collatz-conjecture/dune b/exercises/practice/collatz-conjecture/dune new file mode 100644 index 000000000..9540297f2 --- /dev/null +++ b/exercises/practice/collatz-conjecture/dune @@ -0,0 +1,20 @@ +(executable + (name test) + (libraries base ounit2)) + +(alias + (name runtest) + (deps + (:x test.exe)) + (action + (run %{x}))) + +(alias + (name buildtest) + (deps + (:x test.exe))) + +(env + (dev + (flags + (:standard -warn-error -A)))) diff --git a/exercises/practice/collatz-conjecture/dune-project b/exercises/practice/collatz-conjecture/dune-project new file mode 100644 index 000000000..7655de077 --- /dev/null +++ b/exercises/practice/collatz-conjecture/dune-project @@ -0,0 +1 @@ +(lang dune 1.1) diff --git a/exercises/practice/collatz-conjecture/test.ml b/exercises/practice/collatz-conjecture/test.ml new file mode 100644 index 000000000..2155069fd --- /dev/null +++ b/exercises/practice/collatz-conjecture/test.ml @@ -0,0 +1,26 @@ +open OUnit2 +open Collatz_conjecture + +let option_printer = function + | Error m -> Printf.sprintf "Error \"%s\"" m + | Ok n -> Printf.sprintf "Ok %d" n + +let ae exp got _test_ctxt = assert_equal ~printer:option_printer exp got + +let tests = [ + "zero steps for one" >:: + ae (Ok 0) (collatz_conjecture (1)); + "divide if even" >:: + ae (Ok 4) (collatz_conjecture (16)); + "even and odd steps" >:: + ae (Ok 9) (collatz_conjecture (12)); + "large number of even and odd steps" >:: + ae (Ok 152) (collatz_conjecture (1000000)); + "zero is an error" >:: + ae (Error "Only positive integers are allowed") (collatz_conjecture (0)); + "negative value is an error" >:: + ae (Error "Only positive integers are allowed") (collatz_conjecture (-15)); +] + +let () = + run_test_tt_main ("collatz-conjecture tests" >::: tests) diff --git a/exercises/practice/etl/dune-project b/exercises/practice/etl/dune-project index ddf9710bf..d466ad7c4 100644 --- a/exercises/practice/etl/dune-project +++ b/exercises/practice/etl/dune-project @@ -1,2 +1,2 @@ (lang dune 1.1) -(version 1.0.1) +(version 1.0) diff --git a/exercises/practice/pangram/test.ml b/exercises/practice/pangram/test.ml index 08423cc6d..0f8d73052 100644 --- a/exercises/practice/pangram/test.ml +++ b/exercises/practice/pangram/test.ml @@ -22,8 +22,6 @@ let tests = [ ae false (is_pangram "7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog"); "mixed case and punctuation" >:: ae true (is_pangram "\"Five quacking Zephyrs jolt my wax bed.\""); - "case insensitive" >:: - ae false (is_pangram "the quick brown fox jumps over with lazy FX"); "a-m and A-M are 26 different characters but not a pangram" >:: ae false (is_pangram "abcdefghijklm ABCDEFGHIJKLM"); "empty sentence" >:: @@ -44,8 +42,6 @@ let tests = [ ae false (is_pangram "7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog"); "mixed case and punctuation" >:: ae true (is_pangram "\"Five quacking Zephyrs jolt my wax bed.\""); - "case insensitive" >:: - ae false (is_pangram "the quick brown fox jumps over with lazy FX"); "a-m and A-M are 26 different characters but not a pangram" >:: ae false (is_pangram "abcdefghijklm ABCDEFGHIJKLM"); "empty sentence" >:: @@ -66,8 +62,6 @@ let tests = [ ae false (is_pangram "7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog"); "mixed case and punctuation" >:: ae true (is_pangram "\"Five quacking Zephyrs jolt my wax bed.\""); - "case insensitive" >:: - ae false (is_pangram "the quick brown fox jumps over with lazy FX"); "a-m and A-M are 26 different characters but not a pangram" >:: ae false (is_pangram "abcdefghijklm ABCDEFGHIJKLM"); "empty sentence" >:: @@ -88,8 +82,6 @@ let tests = [ ae false (is_pangram "7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog"); "mixed case and punctuation" >:: ae true (is_pangram "\"Five quacking Zephyrs jolt my wax bed.\""); - "case insensitive" >:: - ae false (is_pangram "the quick brown fox jumps over with lazy FX"); "a-m and A-M are 26 different characters but not a pangram" >:: ae false (is_pangram "abcdefghijklm ABCDEFGHIJKLM"); "empty sentence" >:: @@ -110,8 +102,6 @@ let tests = [ ae false (is_pangram "7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog"); "mixed case and punctuation" >:: ae true (is_pangram "\"Five quacking Zephyrs jolt my wax bed.\""); - "case insensitive" >:: - ae false (is_pangram "the quick brown fox jumps over with lazy FX"); "a-m and A-M are 26 different characters but not a pangram" >:: ae false (is_pangram "abcdefghijklm ABCDEFGHIJKLM"); "empty sentence" >:: @@ -132,8 +122,6 @@ let tests = [ ae false (is_pangram "7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog"); "mixed case and punctuation" >:: ae true (is_pangram "\"Five quacking Zephyrs jolt my wax bed.\""); - "case insensitive" >:: - ae false (is_pangram "the quick brown fox jumps over with lazy FX"); "a-m and A-M are 26 different characters but not a pangram" >:: ae false (is_pangram "abcdefghijklm ABCDEFGHIJKLM"); "empty sentence" >:: @@ -154,8 +142,6 @@ let tests = [ ae false (is_pangram "7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog"); "mixed case and punctuation" >:: ae true (is_pangram "\"Five quacking Zephyrs jolt my wax bed.\""); - "case insensitive" >:: - ae false (is_pangram "the quick brown fox jumps over with lazy FX"); "a-m and A-M are 26 different characters but not a pangram" >:: ae false (is_pangram "abcdefghijklm ABCDEFGHIJKLM"); "empty sentence" >:: @@ -176,8 +162,6 @@ let tests = [ ae false (is_pangram "7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog"); "mixed case and punctuation" >:: ae true (is_pangram "\"Five quacking Zephyrs jolt my wax bed.\""); - "case insensitive" >:: - ae false (is_pangram "the quick brown fox jumps over with lazy FX"); "a-m and A-M are 26 different characters but not a pangram" >:: ae false (is_pangram "abcdefghijklm ABCDEFGHIJKLM"); "empty sentence" >:: @@ -198,8 +182,6 @@ let tests = [ ae false (is_pangram "7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog"); "mixed case and punctuation" >:: ae true (is_pangram "\"Five quacking Zephyrs jolt my wax bed.\""); - "case insensitive" >:: - ae false (is_pangram "the quick brown fox jumps over with lazy FX"); "a-m and A-M are 26 different characters but not a pangram" >:: ae false (is_pangram "abcdefghijklm ABCDEFGHIJKLM"); "empty sentence" >:: @@ -220,30 +202,6 @@ let tests = [ ae false (is_pangram "7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog"); "mixed case and punctuation" >:: ae true (is_pangram "\"Five quacking Zephyrs jolt my wax bed.\""); - "case insensitive" >:: - ae false (is_pangram "the quick brown fox jumps over with lazy FX"); - "a-m and A-M are 26 different characters but not a pangram" >:: - ae false (is_pangram "abcdefghijklm ABCDEFGHIJKLM"); - "empty sentence" >:: - ae false (is_pangram ""); - "perfect lower case" >:: - ae true (is_pangram "abcdefghijklmnopqrstuvwxyz"); - "only lower case" >:: - ae true (is_pangram "the quick brown fox jumps over the lazy dog"); - "missing the letter 'x'" >:: - ae false (is_pangram "a quick movement of the enemy will jeopardize five gunboats"); - "missing the letter 'h'" >:: - ae false (is_pangram "five boxing wizards jump quickly at it"); - "with underscores" >:: - ae true (is_pangram "the_quick_brown_fox_jumps_over_the_lazy_dog"); - "with numbers" >:: - ae true (is_pangram "the 1 quick brown fox jumps over the 2 lazy dogs"); - "missing letters replaced by numbers" >:: - ae false (is_pangram "7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog"); - "mixed case and punctuation" >:: - ae true (is_pangram "\"Five quacking Zephyrs jolt my wax bed.\""); - "case insensitive" >:: - ae false (is_pangram "the quick brown fox jumps over with lazy FX"); "a-m and A-M are 26 different characters but not a pangram" >:: ae false (is_pangram "abcdefghijklm ABCDEFGHIJKLM"); ] diff --git a/templates/collatz-conjecture/dune-project.tpl b/templates/collatz-conjecture/dune-project.tpl new file mode 100644 index 000000000..7655de077 --- /dev/null +++ b/templates/collatz-conjecture/dune-project.tpl @@ -0,0 +1 @@ +(lang dune 1.1) diff --git a/templates/collatz-conjecture/test.ml.tpl b/templates/collatz-conjecture/test.ml.tpl new file mode 100644 index 000000000..1464d7dad --- /dev/null +++ b/templates/collatz-conjecture/test.ml.tpl @@ -0,0 +1,18 @@ +open OUnit2 +open Collatz_conjecture + +let option_printer = function + | Error m -> Printf.sprintf "Error \"%s\"" m + | Ok n -> Printf.sprintf "Ok %d" n + +let ae exp got _test_ctxt = assert_equal ~printer:option_printer exp got + +let tests = [ + {{#cases}} + "{{description}}" >:: + ae {{#input}}{{expected}} (collatz_conjecture ({{number}})){{/input}}; + {{/cases}} +] + +let () = + run_test_tt_main ("collatz-conjecture tests" >::: tests) \ No newline at end of file diff --git a/test-generator/lib_generator/canonical_data.ml b/test-generator/lib_generator/canonical_data.ml index 0bf83c4cc..f1f035d15 100644 --- a/test-generator/lib_generator/canonical_data.ml +++ b/test-generator/lib_generator/canonical_data.ml @@ -8,6 +8,12 @@ type t = { cases: json list; } +let get_reimplemented_uuids (cases: Yojson.Basic.t list) : string list = + List.filter_map cases ~f:(fun c -> + match Yojson.Basic.Util.member "reimplements" c with + | `String s -> Some s + | _ -> None) + let of_string (s: string): t = let open Yojson.Basic in let mem = fun k -> Util.member k (from_string s) in @@ -33,12 +39,26 @@ let of_string (s: string): t = |> Option.map ~f:(fun l -> `Assoc (List.map l ~f:(fun (k, v) -> (k, `String v)))) |> Option.map ~f:(fun i -> `Assoc (("input", i) :: (Util.to_assoc c |> List.filter ~f:(fun (k, _) -> String.(k <> "input" && k <> "expected")))) ) - ) in + ) + in + + let cases = (mem "cases") |> Util.to_list |> sanitize_cases in + + (* filter original cases whose UUID is referenced in "reimplements" *) + let reimplemented_uuids = get_reimplemented_uuids cases in + let filtered_cases = + List.filter cases ~f:(fun c -> + match Util.member "uuid" c with + | `String uuid -> not (List.mem reimplemented_uuids uuid ~equal:String.equal) + | _ -> true + ) + in + { version; exercise; comments = (try (mem "comments") |> Util.to_list |> List.map ~f:Util.to_string with _ -> []); - cases = (mem "cases") |> Util.to_list |> sanitize_cases + cases = filtered_cases } let rec yo_to_ez (j: Yojson.Basic.t): Ezjsonm.value = diff --git a/test-generator/lib_generator/special_cases.ml b/test-generator/lib_generator/special_cases.ml index 9017db8c0..33c2cd876 100644 --- a/test-generator/lib_generator/special_cases.ml +++ b/test-generator/lib_generator/special_cases.ml @@ -320,6 +320,11 @@ let edit_knapsack (ps: (string * json) list): (string * string) list = | (k, v) -> (k, json_to_string v) in List.map ps ~f:edit +let edit_collatz_conjecture_expected = function + | `Int n -> "(Ok " ^ Int.to_string n ^ ")" + | `Assoc [("error", `String m)] -> "(Error \"" ^ m ^ "\")" + | x -> Yojson.Basic.to_string x + let unwrap_strings (ps: (string * json) list): (string * string) list option = let edit = function | (_, `String s) -> ("params", s) @@ -332,6 +337,7 @@ let edit_parameters ~(slug: string) (parameters: (string * json) list) = match ( | ("beer-song", ps) -> edit_expected ~f:edit_beer_song_expected ps |> Option.return | ("binary-search", ps) -> edit_binary_search ps |> Option.return | ("bowling", ps) -> edit_bowling ps |> Option.return + | ("collatz-conjecture", ps) -> edit_expected ~f:edit_collatz_conjecture_expected ps |> Option.return | ("connect", ps) -> edit_connect ps |> Option.return | ("change", ps) -> edit_change ps |> Option.return | ("darts", ps) -> edit_darts ps |> Option.return