From 55595a97638025829bcee899e312b01a1f5ecd13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Gillet?= Date: Sun, 20 Jun 2021 15:15:38 +0900 Subject: [PATCH 1/8] [#763] New practice exercise config and generated tests.toml --- bin/boostrap_practice_exercise.exs | 258 ++++++++++++++++++ config.json | 19 ++ .../affine-cipher/.docs/instructions.md | 70 +++++ .../practice/affine-cipher/.formatter.exs | 4 + .../practice/affine-cipher/.meta/config.json | 18 ++ .../practice/affine-cipher/.meta/example.ex | 67 +++++ .../practice/affine-cipher/.meta/tests.toml | 57 ++++ .../affine-cipher/lib/affine_cipher.ex | 16 ++ exercises/practice/affine-cipher/mix.exs | 28 ++ .../affine-cipher/test/affine_cipher_test.exs | 172 ++++++++++++ .../affine-cipher/test/test_helper.exs | 2 + 11 files changed, 711 insertions(+) create mode 100644 bin/boostrap_practice_exercise.exs create mode 100644 exercises/practice/affine-cipher/.docs/instructions.md create mode 100644 exercises/practice/affine-cipher/.formatter.exs create mode 100644 exercises/practice/affine-cipher/.meta/config.json create mode 100644 exercises/practice/affine-cipher/.meta/example.ex create mode 100644 exercises/practice/affine-cipher/.meta/tests.toml create mode 100644 exercises/practice/affine-cipher/lib/affine_cipher.ex create mode 100644 exercises/practice/affine-cipher/mix.exs create mode 100644 exercises/practice/affine-cipher/test/affine_cipher_test.exs create mode 100644 exercises/practice/affine-cipher/test/test_helper.exs diff --git a/bin/boostrap_practice_exercise.exs b/bin/boostrap_practice_exercise.exs new file mode 100644 index 0000000000..19e0c53bd4 --- /dev/null +++ b/bin/boostrap_practice_exercise.exs @@ -0,0 +1,258 @@ +# Generate all files required for a practice exercise. +# File content is filled as much as possible, but some, including tests, need manual input. +# +# Run the following command from the root of the repo: +# $ elixir bin/boostrap_practice_exercise.exs complex-numbers +# Pass the name of the exercise (e. g., "complex-numbers") as an argument + +defmodule Generate do + def to_snake_case(lower_camel_case) do + lower_camel_case + |> String.split(~r/(?=[A-Z\d])/) + |> Enum.map_join("_", &String.downcase/1) + end + + def get_properties(%{"cases" => cases}) when is_list(cases) do + Enum.map(cases, &Generate.get_properties/1) + |> Enum.concat() + |> Enum.uniq() + end + + def get_properties(%{"property" => property, "input" => %{} = input}), + do: [ + {property, + Enum.map(input, fn + {var, %{} = val} -> {var, Enum.map(val, fn {v, _} -> v end)} + {var, _} -> {var, nil} + end)} + ] + + def get_properties(%{"property" => property}), do: [{property, "input"}] + def get_properties(_data), do: [] + + def print_property({property, variables}) do + """ + @doc \"\"\" + insert function description here + \"\"\" + @spec #{property}(#{Enum.map_join(variables, ", ", fn + {var, nil} -> "#{var} :: String.t()" + {var, sub_vars} -> "#{var} :: {#{Enum.map_join(sub_vars, " :: String.t(), ", fn v -> v end)} :: String.t()}" + end)}) :: {:ok, String.t()} | {:error, String.t()} + def #{property}(#{Enum.map_join(variables, ", ", fn {var, _} -> var end)}) do + end + """ + end + + def print_comments(comments, do_not_print) do + comments + |> Enum.reject(fn {field, _value} -> field in do_not_print end) + |> Enum.map_join("\n", fn + {"comments", values} when is_list(values) -> + "# #{Enum.join(String.trim(values), "\n# ")}" + + {field, values} when is_list(values) -> + "#\n# --#{field} --\n# #{Enum.map_join(values, "\n# ", &inspect/1)}" + + {field, value} -> + "#\n# -- #{field} --\n# #{inspect(value)}" + end) + end + + def print_input(%{} = input) do + Enum.map_join(input, "\n", fn {variable, value} -> "#{variable} = #{inspect(value)}" end) + end + + def print_input(input), do: "input = #{inspect(input)}" + + def print_variables(%{} = input), + do: Enum.map_join(input, ", ", fn {variable, _value} -> variable end) + + def print_variables(input), do: input + + def print_expected(%{"error" => err}), do: "{:error, #{inspect(err)}}" + def print_expected(expected), do: "{:ok, #{inspect(expected)}}" + + def print_test_case(%{"description" => description, "cases" => sub_cases} = c, module) do + """ + describe \"#{description}\" do + #{Generate.print_comments(c, ["description", "cases"])} + #{Enum.map_join(sub_cases, "\n\n", &Generate.print_test_case(&1, module))} + end + """ + end + + def print_test_case( + %{ + "description" => description, + "property" => property, + "input" => input, + "expected" => expected + } = c, + module + ) do + """ + @tag :pending + test \"#{description}\" do + #{Generate.print_comments(c, ["description", "property", "input", "expected", "uuid"])} + #{print_input(input)} + output = #{module}.#{Generate.to_snake_case(property)}(#{print_variables(input)}) + expected = #{print_expected(expected)} + + assert output == expected + end + """ + end +end + +Mix.install([ + {:jason, "~> 1.2"}, + {:toml, "~> 0.6"} +]) + +[exercise] = System.argv() + +exercise_path = String.replace(exercise, "-", "_") + +module = + exercise + |> String.split("-") + |> Enum.map_join("", &String.capitalize/1) + +## Step 1: create folder structure + +Mix.Generator.create_directory("exercises/practice/#{exercise}") +Mix.Generator.create_directory("exercises/practice/#{exercise}/.docs") +Mix.Generator.create_directory("exercises/practice/#{exercise}/.meta") +Mix.Generator.create_directory("exercises/practice/#{exercise}/lib") +Mix.Generator.create_directory("exercises/practice/#{exercise}/test") + +## Step 2: add common files + +# .formatter.exs +format = """ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] +""" + +Mix.Generator.create_file("exercises/practice/#{exercise}/.formatter.exs", format) + +# mix.exs +mix = """ +defmodule #{module}.MixProject do + use Mix.Project + + def project do + [ + app: :#{exercise_path}, + version: "0.1.0", + # elixir: "~> 1.8", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end +""" + +Mix.Generator.create_file("exercises/practice/#{exercise}/mix.exs", mix) + +# test/test_helper.exs +test_helper = """ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true) +""" + +Mix.Generator.create_file("exercises/practice/#{exercise}/test/test_helper.exs", test_helper) + +## Step 3: write files that depend on problem specifications + +url = + 'https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/#{exercise}' + +:inets.start() +:ssl.start() + +# .docs/instructions.md +{:ok, {_status, _header, description}} = + :httpc.request(:get, {url ++ '/description.md', []}, [], []) + +Mix.Generator.create_file("exercises/practice/#{exercise}/.docs/instructions.md", description) + +# .meta/config.json +{:ok, {_status, _header, metadata}} = :httpc.request(:get, {url ++ '/metadata.toml', []}, [], []) + +metadata = + metadata + |> to_string + |> Toml.decode!() + +config = %{ + authors: [], + contributors: [], + files: %{ + solution: ["lib/#{exercise_path}.ex"], + test: ["test/#{exercise_path}_test.exs"], + example: [".meta/example.ex"] + } +} + +config = + Map.merge(metadata, config) + |> Jason.encode!(pretty: true) + +Mix.Generator.create_file("exercises/practice/#{exercise}/.meta/config.json", config) +IO.puts("Don't forget to add your name and the names of contributors") + +# tests and lib files +{:ok, {_status, _header, data}} = + :httpc.request(:get, {url ++ '/canonical-data.json', []}, [], []) + +data = Jason.decode!(data) + +# Generating lib file +lib_file = """ +defmodule #{module} do + +#{data |> Generate.get_properties() |> Enum.map_join("\n\n", &Generate.print_property/1)} + +end +""" + +path = "exercises/practice/#{exercise}/lib/#{exercise_path}.ex" +Mix.Generator.create_file(path, lib_file) +System.cmd("mix", ["format", path]) + +Mix.Generator.copy_file(path, "exercises/practice/#{exercise}/.meta/example.ex") + +# Generating test file +test_file = + """ + defmodule #{module}Test do + use ExUnit.Case + + #{Generate.print_comments(data, ["cases", "exercise"])} + #{Enum.map_join(data["cases"], "\n\n", &Generate.print_test_case(&1, module))} + end + """ + |> String.replace("@tag", "# @tag", global: false) + +path = "exercises/practice/#{exercise}/test/#{exercise_path}_test.exs" +Mix.Generator.create_file(path, test_file) +System.cmd("mix", ["format", path]) diff --git a/config.json b/config.json index b75dbade40..5a9e8db9ce 100644 --- a/config.json +++ b/config.json @@ -2448,6 +2448,25 @@ "multiple-clause-matching" ], "difficulty": 5 + }, + { + "slug": "affine-cipher", + "name": "Affine Cipher", + "uuid": "18d12bac-ead0-456c-bcef-ccb2fd06fe72", + "prerequisites": [ + "pattern-matching", + "atoms", + "enum", + "integers", + "if", + "strings", + "tuples" + ], + "practices": [ + "enum", + "strings" + ], + "difficulty": 5 } ], "foregone": [ diff --git a/exercises/practice/affine-cipher/.docs/instructions.md b/exercises/practice/affine-cipher/.docs/instructions.md new file mode 100644 index 0000000000..17fee4c96c --- /dev/null +++ b/exercises/practice/affine-cipher/.docs/instructions.md @@ -0,0 +1,70 @@ +# Description + +Create an implementation of the affine cipher, +an ancient encryption system created in the Middle East. + +The affine cipher is a type of monoalphabetic substitution cipher. +Each character is mapped to its numeric equivalent, encrypted with +a mathematical function and then converted to the letter relating to +its new numeric value. Although all monoalphabetic ciphers are weak, +the affine cypher is much stronger than the atbash cipher, +because it has many more keys. + +The encryption function is: + + `E(x) = (ax + b) mod m` + - where `x` is the letter's index from 0 - length of alphabet - 1 + - `m` is the length of the alphabet. For the roman alphabet `m == 26`. + - and `a` and `b` make the key + +The decryption function is: + + `D(y) = a^-1(y - b) mod m` + - where `y` is the numeric value of an encrypted letter, ie. `y = E(x)` + - it is important to note that `a^-1` is the modular multiplicative inverse + of `a mod m` + - the modular multiplicative inverse of `a` only exists if `a` and `m` are + coprime. + +To find the MMI of `a`: + + `an mod m = 1` + - where `n` is the modular multiplicative inverse of `a mod m` + +More information regarding how to find a Modular Multiplicative Inverse +and what it means can be found [here.](https://en.wikipedia.org/wiki/Modular_multiplicative_inverse) + +Because automatic decryption fails if `a` is not coprime to `m` your +program should return status 1 and `"Error: a and m must be coprime."` +if they are not. Otherwise it should encode or decode with the +provided key. + +The Caesar (shift) cipher is a simple affine cipher where `a` is 1 and +`b` as the magnitude results in a static displacement of the letters. +This is much less secure than a full implementation of the affine cipher. + +Ciphertext is written out in groups of fixed length, the traditional group +size being 5 letters, and punctuation is excluded. This is to make it +harder to guess things based on word boundaries. + +## General Examples + + - Encoding `test` gives `ybty` with the key a=5 b=7 + - Decoding `ybty` gives `test` with the key a=5 b=7 + - Decoding `ybty` gives `lqul` with the wrong key a=11 b=7 + - Decoding `kqlfd jzvgy tpaet icdhm rtwly kqlon ubstx` + - gives `thequickbrownfoxjumpsoverthelazydog` with the key a=19 b=13 + - Encoding `test` with the key a=18 b=13 + - gives `Error: a and m must be coprime.` + - because a and m are not relatively prime + +## Examples of finding a Modular Multiplicative Inverse (MMI) + + - simple example: + - `9 mod 26 = 9` + - `9 * 3 mod 26 = 27 mod 26 = 1` + - `3` is the MMI of `9 mod 26` + - a more complicated example: + - `15 mod 26 = 15` + - `15 * 7 mod 26 = 105 mod 26 = 1` + - `7` is the MMI of `15 mod 26` diff --git a/exercises/practice/affine-cipher/.formatter.exs b/exercises/practice/affine-cipher/.formatter.exs new file mode 100644 index 0000000000..d2cda26edd --- /dev/null +++ b/exercises/practice/affine-cipher/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/exercises/practice/affine-cipher/.meta/config.json b/exercises/practice/affine-cipher/.meta/config.json new file mode 100644 index 0000000000..2ea9e12e81 --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/config.json @@ -0,0 +1,18 @@ +{ + "authors": ["jiegillet"], + "contributors": [], + "files": { + "example": [ + ".meta/example.ex" + ], + "solution": [ + "lib/affine_cipher.ex" + ], + "test": [ + "test/affine_cipher_test.exs" + ] + }, + "blurb": "Create an implementation of the Affine cipher, an ancient encryption algorithm from the Middle East.", + "source": "Wikipedia", + "source_url": "http://en.wikipedia.org/wiki/Affine_cipher" +} diff --git a/exercises/practice/affine-cipher/.meta/example.ex b/exercises/practice/affine-cipher/.meta/example.ex new file mode 100644 index 0000000000..5a1cd9400e --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/example.ex @@ -0,0 +1,67 @@ +defmodule AffineCipher do + @alphabet_size 26 + @ignored ~r/[ ,.]/ + + @doc """ + Encode an encrypted message using a key + """ + @spec encode(key :: %{a: integer, b: integer}, message :: String.t()) :: + {:ok, String.t()} | {:error, String.t()} + def(encode(%{a: a, b: b}, message)) do + if Integer.gcd(a, @alphabet_size) != 1 do + {:error, "a and m must be coprime."} + else + encrypted = + message + |> String.downcase() + |> String.replace(@ignored, "") + |> to_charlist() + |> Enum.map(fn + digit when ?0 <= digit and digit <= ?9 -> digit + char -> Integer.mod(a * (char - ?a) + b, @alphabet_size) + ?a + end) + |> Enum.chunk_every(5) + |> Enum.map_join(" ", &to_string/1) + + {:ok, encrypted} + end + end + + @doc """ + Decode an encrypted message using a key + """ + @spec decode(key :: %{a: integer, b: integer}, message :: String.t()) :: + {:ok, String.t()} | {:error, String.t()} + def decode(%{a: a, b: b}, encrypted) do + if Integer.gcd(a, @alphabet_size) != 1 do + {:error, "a and m must be coprime."} + else + mmi = modular_multiplicative_inverse(a, @alphabet_size) + + message = + encrypted + |> String.replace(@ignored, "") + |> to_charlist() + |> Enum.map(fn + digit when ?0 <= digit and digit <= ?9 -> digit + char -> Integer.mod(mmi * (char - ?a - b), @alphabet_size) + ?a + end) + |> to_string + + {:ok, message} + end + end + + def modular_multiplicative_inverse(a, m) do + modular_multiplicative_inverse(a, m, 1, 0) + |> Integer.mod(m) + end + + def modular_multiplicative_inverse(0, r0, _t1, _t0) when r0 > 1, do: raise("Not invertible") + def modular_multiplicative_inverse(0, _r0, _t1, t0), do: t0 + + def modular_multiplicative_inverse(r1, r0, t1, t0) do + q = div(r0, r1) + modular_multiplicative_inverse(r0 - q * r1, r1, t0 - q * t1, t1) + end +end diff --git a/exercises/practice/affine-cipher/.meta/tests.toml b/exercises/practice/affine-cipher/.meta/tests.toml new file mode 100644 index 0000000000..7bc0eed26c --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/tests.toml @@ -0,0 +1,57 @@ +# 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. +[2ee1d9af-1c43-416c-b41b-cefd7d4d2b2a] +description = "encode -> encode yes" + +[785bade9-e98b-4d4f-a5b0-087ba3d7de4b] +description = "encode -> encode no" + +[2854851c-48fb-40d8-9bf6-8f192ed25054] +description = "encode -> encode OMG" + +[bc0c1244-b544-49dd-9777-13a770be1bad] +description = "encode -> encode O M G" + +[381a1a20-b74a-46ce-9277-3778625c9e27] +description = "encode -> encode mindblowingly" + +[6686f4e2-753b-47d4-9715-876fdc59029d] +description = "encode -> encode numbers" + +[ae23d5bd-30a8-44b6-afbe-23c8c0c7faa3] +description = "encode -> encode deep thought" + +[c93a8a4d-426c-42ef-9610-76ded6f7ef57] +description = "encode -> encode all the letters" + +[0673638a-4375-40bd-871c-fb6a2c28effb] +description = "encode -> encode with a not coprime to m" + +[3f0ac7e2-ec0e-4a79-949e-95e414953438] +description = "decode -> decode exercism" + +[241ee64d-5a47-4092-a5d7-7939d259e077] +description = "decode -> decode a sentence" + +[33fb16a1-765a-496f-907f-12e644837f5e] +description = "decode -> decode numbers" + +[20bc9dce-c5ec-4db6-a3f1-845c776bcbf7] +description = "decode -> decode all the letters" + +[623e78c0-922d-49c5-8702-227a3e8eaf81] +description = "decode -> decode with no spaces in input" + +[58fd5c2a-1fd9-4563-a80a-71cff200f26f] +description = "decode -> decode with too many spaces" + +[b004626f-c186-4af9-a3f4-58f74cdb86d5] +description = "decode -> decode with a not coprime to m" diff --git a/exercises/practice/affine-cipher/lib/affine_cipher.ex b/exercises/practice/affine-cipher/lib/affine_cipher.ex new file mode 100644 index 0000000000..51a280487a --- /dev/null +++ b/exercises/practice/affine-cipher/lib/affine_cipher.ex @@ -0,0 +1,16 @@ +defmodule AffineCipher do + @doc """ + Encode an encrypted message using a key + """ + @spec encode(key :: %{a: integer, b: integer}, message :: String.t()) :: + {:ok, String.t()} | {:error, String.t()} + def(encode(%{a: a, b: b}, message)) do + end + + @doc """ + Decode an encrypted message using a key + """ + @spec decode(key :: %{a: integer, b: integer}, message :: String.t()) :: String.t() + def decode(%{a: a, b: b}, encrypted) do + end +end diff --git a/exercises/practice/affine-cipher/mix.exs b/exercises/practice/affine-cipher/mix.exs new file mode 100644 index 0000000000..4190cda091 --- /dev/null +++ b/exercises/practice/affine-cipher/mix.exs @@ -0,0 +1,28 @@ +defmodule AffineCipher.MixProject do + use Mix.Project + + def project do + [ + app: :affine_cipher, + version: "0.1.0", + # elixir: "~> 1.8", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # {:dep_from_hexpm, "~> 0.3.0"}, + # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} + ] + end +end diff --git a/exercises/practice/affine-cipher/test/affine_cipher_test.exs b/exercises/practice/affine-cipher/test/affine_cipher_test.exs new file mode 100644 index 0000000000..c3f0cfa594 --- /dev/null +++ b/exercises/practice/affine-cipher/test/affine_cipher_test.exs @@ -0,0 +1,172 @@ +defmodule AffineCipherTest do + use ExUnit.Case + + # The test are divided into two groups: + # * Encoding from English to affine cipher + # * Decoding from affine cipher to all-lowercase-mashed-together English + describe "encode" do + # Test encoding from English to ciphertext with keys + # @tag :pending + test "encode yes" do + key = %{a: 5, b: 7} + phrase = "yes" + output = AffineCipher.encode(key, phrase) + expected = {:ok, "xbt"} + + assert output == expected + end + + @tag :pending + test "encode no" do + key = %{a: 15, b: 18} + phrase = "no" + output = AffineCipher.encode(key, phrase) + expected = {:ok, "fu"} + + assert output == expected + end + + @tag :pending + test "encode OMG" do + key = %{a: 21, b: 3} + phrase = "OMG" + output = AffineCipher.encode(key, phrase) + expected = {:ok, "lvz"} + + assert output == expected + end + + @tag :pending + test "encode O M G" do + key = %{a: 25, b: 47} + phrase = "O M G" + output = AffineCipher.encode(key, phrase) + expected = {:ok, "hjp"} + + assert output == expected + end + + @tag :pending + test "encode mindblowingly" do + key = %{a: 11, b: 15} + phrase = "mindblowingly" + output = AffineCipher.encode(key, phrase) + expected = {:ok, "rzcwa gnxzc dgt"} + + assert output == expected + end + + @tag :pending + test "encode numbers" do + key = %{a: 3, b: 4} + phrase = "Testing,1 2 3, testing." + output = AffineCipher.encode(key, phrase) + expected = {:ok, "jqgjc rw123 jqgjc rw"} + + assert output == expected + end + + @tag :pending + test "encode deep thought" do + key = %{a: 5, b: 17} + phrase = "Truth is fiction." + output = AffineCipher.encode(key, phrase) + expected = {:ok, "iynia fdqfb ifje"} + + assert output == expected + end + + @tag :pending + test "encode all the letters" do + key = %{a: 17, b: 33} + phrase = "The quick brown fox jumps over the lazy dog." + output = AffineCipher.encode(key, phrase) + expected = {:ok, "swxtj npvyk lruol iejdc blaxk swxmh qzglf"} + + assert output == expected + end + + @tag :pending + test "encode with a not coprime to m" do + key = %{a: 6, b: 17} + phrase = "This is a test." + output = AffineCipher.encode(key, phrase) + expected = {:error, "a and m must be coprime."} + + assert output == expected + end + end + + describe "decode" do + # Test decoding from ciphertext to English with keys + @tag :pending + test "decode exercism" do + key = %{a: 3, b: 7} + phrase = "tytgn fjr" + output = AffineCipher.decode(key, phrase) + expected = {:ok, "exercism"} + + assert output == expected + end + + @tag :pending + test "decode a sentence" do + key = %{a: 19, b: 16} + phrase = "qdwju nqcro muwhn odqun oppmd aunwd o" + output = AffineCipher.decode(key, phrase) + expected = {:ok, "anobstacleisoftenasteppingstone"} + + assert output == expected + end + + @tag :pending + test "decode numbers" do + key = %{a: 25, b: 7} + phrase = "odpoz ub123 odpoz ub" + output = AffineCipher.decode(key, phrase) + expected = {:ok, "testing123testing"} + + assert output == expected + end + + @tag :pending + test "decode all the letters" do + key = %{a: 17, b: 33} + phrase = "swxtj npvyk lruol iejdc blaxk swxmh qzglf" + output = AffineCipher.decode(key, phrase) + expected = {:ok, "thequickbrownfoxjumpsoverthelazydog"} + + assert output == expected + end + + @tag :pending + test "decode with no spaces in input" do + key = %{a: 17, b: 33} + phrase = "swxtjnpvyklruoliejdcblaxkswxmhqzglf" + output = AffineCipher.decode(key, phrase) + expected = {:ok, "thequickbrownfoxjumpsoverthelazydog"} + + assert output == expected + end + + @tag :pending + test "decode with too many spaces" do + key = %{a: 15, b: 16} + phrase = "vszzm cly yd cg qdp" + output = AffineCipher.decode(key, phrase) + expected = {:ok, "jollygreengiant"} + + assert output == expected + end + + @tag :pending + test "decode with a not coprime to m" do + key = %{a: 13, b: 5} + phrase = "Test" + output = AffineCipher.decode(key, phrase) + expected = {:error, "a and m must be coprime."} + + assert output == expected + end + end +end diff --git a/exercises/practice/affine-cipher/test/test_helper.exs b/exercises/practice/affine-cipher/test/test_helper.exs new file mode 100644 index 0000000000..35fc5bff82 --- /dev/null +++ b/exercises/practice/affine-cipher/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.start() +ExUnit.configure(exclude: :pending, trace: true) From a1bea74f393736d0d1365b589f988d16c80c67e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Gillet?= Date: Sun, 20 Jun 2021 16:05:58 +0900 Subject: [PATCH 2/8] Update @spec type --- exercises/practice/affine-cipher/lib/affine_cipher.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exercises/practice/affine-cipher/lib/affine_cipher.ex b/exercises/practice/affine-cipher/lib/affine_cipher.ex index 51a280487a..ed0cb22e94 100644 --- a/exercises/practice/affine-cipher/lib/affine_cipher.ex +++ b/exercises/practice/affine-cipher/lib/affine_cipher.ex @@ -10,7 +10,8 @@ defmodule AffineCipher do @doc """ Decode an encrypted message using a key """ - @spec decode(key :: %{a: integer, b: integer}, message :: String.t()) :: String.t() + @spec decode(key :: %{a: integer, b: integer}, message :: String.t()) :: + {:ok, String.t()} | {:error, String.t()} def decode(%{a: a, b: b}, encrypted) do end end From 28fbe63043c8eb31880e1c381bee95f63d79ba29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Gillet?= Date: Sun, 20 Jun 2021 16:06:45 +0900 Subject: [PATCH 3/8] Change practice concept --- config.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config.json b/config.json index 5a9e8db9ce..0e94c7e376 100644 --- a/config.json +++ b/config.json @@ -2456,6 +2456,7 @@ "prerequisites": [ "pattern-matching", "atoms", + "pipe-operator", "enum", "integers", "if", @@ -2463,7 +2464,7 @@ "tuples" ], "practices": [ - "enum", + "pipe-operator", "strings" ], "difficulty": 5 From cb2898014240512da700b05ac75865a3487b209d Mon Sep 17 00:00:00 2001 From: Jie Date: Sun, 20 Jun 2021 20:42:52 +0900 Subject: [PATCH 4/8] Delete boostrap_practice_exercise.exs --- bin/boostrap_practice_exercise.exs | 258 ----------------------------- 1 file changed, 258 deletions(-) delete mode 100644 bin/boostrap_practice_exercise.exs diff --git a/bin/boostrap_practice_exercise.exs b/bin/boostrap_practice_exercise.exs deleted file mode 100644 index 19e0c53bd4..0000000000 --- a/bin/boostrap_practice_exercise.exs +++ /dev/null @@ -1,258 +0,0 @@ -# Generate all files required for a practice exercise. -# File content is filled as much as possible, but some, including tests, need manual input. -# -# Run the following command from the root of the repo: -# $ elixir bin/boostrap_practice_exercise.exs complex-numbers -# Pass the name of the exercise (e. g., "complex-numbers") as an argument - -defmodule Generate do - def to_snake_case(lower_camel_case) do - lower_camel_case - |> String.split(~r/(?=[A-Z\d])/) - |> Enum.map_join("_", &String.downcase/1) - end - - def get_properties(%{"cases" => cases}) when is_list(cases) do - Enum.map(cases, &Generate.get_properties/1) - |> Enum.concat() - |> Enum.uniq() - end - - def get_properties(%{"property" => property, "input" => %{} = input}), - do: [ - {property, - Enum.map(input, fn - {var, %{} = val} -> {var, Enum.map(val, fn {v, _} -> v end)} - {var, _} -> {var, nil} - end)} - ] - - def get_properties(%{"property" => property}), do: [{property, "input"}] - def get_properties(_data), do: [] - - def print_property({property, variables}) do - """ - @doc \"\"\" - insert function description here - \"\"\" - @spec #{property}(#{Enum.map_join(variables, ", ", fn - {var, nil} -> "#{var} :: String.t()" - {var, sub_vars} -> "#{var} :: {#{Enum.map_join(sub_vars, " :: String.t(), ", fn v -> v end)} :: String.t()}" - end)}) :: {:ok, String.t()} | {:error, String.t()} - def #{property}(#{Enum.map_join(variables, ", ", fn {var, _} -> var end)}) do - end - """ - end - - def print_comments(comments, do_not_print) do - comments - |> Enum.reject(fn {field, _value} -> field in do_not_print end) - |> Enum.map_join("\n", fn - {"comments", values} when is_list(values) -> - "# #{Enum.join(String.trim(values), "\n# ")}" - - {field, values} when is_list(values) -> - "#\n# --#{field} --\n# #{Enum.map_join(values, "\n# ", &inspect/1)}" - - {field, value} -> - "#\n# -- #{field} --\n# #{inspect(value)}" - end) - end - - def print_input(%{} = input) do - Enum.map_join(input, "\n", fn {variable, value} -> "#{variable} = #{inspect(value)}" end) - end - - def print_input(input), do: "input = #{inspect(input)}" - - def print_variables(%{} = input), - do: Enum.map_join(input, ", ", fn {variable, _value} -> variable end) - - def print_variables(input), do: input - - def print_expected(%{"error" => err}), do: "{:error, #{inspect(err)}}" - def print_expected(expected), do: "{:ok, #{inspect(expected)}}" - - def print_test_case(%{"description" => description, "cases" => sub_cases} = c, module) do - """ - describe \"#{description}\" do - #{Generate.print_comments(c, ["description", "cases"])} - #{Enum.map_join(sub_cases, "\n\n", &Generate.print_test_case(&1, module))} - end - """ - end - - def print_test_case( - %{ - "description" => description, - "property" => property, - "input" => input, - "expected" => expected - } = c, - module - ) do - """ - @tag :pending - test \"#{description}\" do - #{Generate.print_comments(c, ["description", "property", "input", "expected", "uuid"])} - #{print_input(input)} - output = #{module}.#{Generate.to_snake_case(property)}(#{print_variables(input)}) - expected = #{print_expected(expected)} - - assert output == expected - end - """ - end -end - -Mix.install([ - {:jason, "~> 1.2"}, - {:toml, "~> 0.6"} -]) - -[exercise] = System.argv() - -exercise_path = String.replace(exercise, "-", "_") - -module = - exercise - |> String.split("-") - |> Enum.map_join("", &String.capitalize/1) - -## Step 1: create folder structure - -Mix.Generator.create_directory("exercises/practice/#{exercise}") -Mix.Generator.create_directory("exercises/practice/#{exercise}/.docs") -Mix.Generator.create_directory("exercises/practice/#{exercise}/.meta") -Mix.Generator.create_directory("exercises/practice/#{exercise}/lib") -Mix.Generator.create_directory("exercises/practice/#{exercise}/test") - -## Step 2: add common files - -# .formatter.exs -format = """ -# Used by "mix format" -[ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] -] -""" - -Mix.Generator.create_file("exercises/practice/#{exercise}/.formatter.exs", format) - -# mix.exs -mix = """ -defmodule #{module}.MixProject do - use Mix.Project - - def project do - [ - app: :#{exercise_path}, - version: "0.1.0", - # elixir: "~> 1.8", - start_permanent: Mix.env() == :prod, - deps: deps() - ] - end - - # Run "mix help compile.app" to learn about applications. - def application do - [ - extra_applications: [:logger] - ] - end - - # Run "mix help deps" to learn about dependencies. - defp deps do - [ - # {:dep_from_hexpm, "~> 0.3.0"}, - # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} - ] - end -end -""" - -Mix.Generator.create_file("exercises/practice/#{exercise}/mix.exs", mix) - -# test/test_helper.exs -test_helper = """ -ExUnit.start() -ExUnit.configure(exclude: :pending, trace: true) -""" - -Mix.Generator.create_file("exercises/practice/#{exercise}/test/test_helper.exs", test_helper) - -## Step 3: write files that depend on problem specifications - -url = - 'https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/#{exercise}' - -:inets.start() -:ssl.start() - -# .docs/instructions.md -{:ok, {_status, _header, description}} = - :httpc.request(:get, {url ++ '/description.md', []}, [], []) - -Mix.Generator.create_file("exercises/practice/#{exercise}/.docs/instructions.md", description) - -# .meta/config.json -{:ok, {_status, _header, metadata}} = :httpc.request(:get, {url ++ '/metadata.toml', []}, [], []) - -metadata = - metadata - |> to_string - |> Toml.decode!() - -config = %{ - authors: [], - contributors: [], - files: %{ - solution: ["lib/#{exercise_path}.ex"], - test: ["test/#{exercise_path}_test.exs"], - example: [".meta/example.ex"] - } -} - -config = - Map.merge(metadata, config) - |> Jason.encode!(pretty: true) - -Mix.Generator.create_file("exercises/practice/#{exercise}/.meta/config.json", config) -IO.puts("Don't forget to add your name and the names of contributors") - -# tests and lib files -{:ok, {_status, _header, data}} = - :httpc.request(:get, {url ++ '/canonical-data.json', []}, [], []) - -data = Jason.decode!(data) - -# Generating lib file -lib_file = """ -defmodule #{module} do - -#{data |> Generate.get_properties() |> Enum.map_join("\n\n", &Generate.print_property/1)} - -end -""" - -path = "exercises/practice/#{exercise}/lib/#{exercise_path}.ex" -Mix.Generator.create_file(path, lib_file) -System.cmd("mix", ["format", path]) - -Mix.Generator.copy_file(path, "exercises/practice/#{exercise}/.meta/example.ex") - -# Generating test file -test_file = - """ - defmodule #{module}Test do - use ExUnit.Case - - #{Generate.print_comments(data, ["cases", "exercise"])} - #{Enum.map_join(data["cases"], "\n\n", &Generate.print_test_case(&1, module))} - end - """ - |> String.replace("@tag", "# @tag", global: false) - -path = "exercises/practice/#{exercise}/test/#{exercise_path}_test.exs" -Mix.Generator.create_file(path, test_file) -System.cmd("mix", ["format", path]) From ae76d5a25580a9e6377ca9004001eb20fe100bbf Mon Sep 17 00:00:00 2001 From: Jie Date: Sun, 20 Jun 2021 20:43:42 +0900 Subject: [PATCH 5/8] Apply suggestions from code review Co-authored-by: Angelika Tyborska --- config.json | 9 ++++++++- exercises/practice/affine-cipher/lib/affine_cipher.ex | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/config.json b/config.json index 501e0ca14a..1cf40b8eb9 100644 --- a/config.json +++ b/config.json @@ -2471,13 +2471,20 @@ "uuid": "18d12bac-ead0-456c-bcef-ccb2fd06fe72", "prerequisites": [ "pattern-matching", + "guards", + "multiple-clause-functions", "atoms", "pipe-operator", "enum", "integers", "if", + "cond", + "case", "strings", - "tuples" + "tuples", + "maps", + "list-comprehensions", + "charlists" ], "practices": [ "pipe-operator", diff --git a/exercises/practice/affine-cipher/lib/affine_cipher.ex b/exercises/practice/affine-cipher/lib/affine_cipher.ex index ed0cb22e94..7ab808d918 100644 --- a/exercises/practice/affine-cipher/lib/affine_cipher.ex +++ b/exercises/practice/affine-cipher/lib/affine_cipher.ex @@ -4,7 +4,7 @@ defmodule AffineCipher do """ @spec encode(key :: %{a: integer, b: integer}, message :: String.t()) :: {:ok, String.t()} | {:error, String.t()} - def(encode(%{a: a, b: b}, message)) do + def encode(%{a: a, b: b}, message) do end @doc """ From ab20dbd2c385b152d13eb1a20989a59453d86a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Gillet?= Date: Sun, 20 Jun 2021 20:49:42 +0900 Subject: [PATCH 6/8] Make a type for key --- exercises/practice/affine-cipher/.meta/example.ex | 11 +++++++---- exercises/practice/affine-cipher/lib/affine_cipher.ex | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/exercises/practice/affine-cipher/.meta/example.ex b/exercises/practice/affine-cipher/.meta/example.ex index 5a1cd9400e..a45671bf60 100644 --- a/exercises/practice/affine-cipher/.meta/example.ex +++ b/exercises/practice/affine-cipher/.meta/example.ex @@ -1,12 +1,16 @@ defmodule AffineCipher do + @doctype """ + A type for the encryption key + """ + @type key() :: %{a: integer, b: integer} + @alphabet_size 26 @ignored ~r/[ ,.]/ @doc """ Encode an encrypted message using a key """ - @spec encode(key :: %{a: integer, b: integer}, message :: String.t()) :: - {:ok, String.t()} | {:error, String.t()} + @spec encode(key :: key(), message :: String.t()) :: {:ok, String.t()} | {:error, String.t()} def(encode(%{a: a, b: b}, message)) do if Integer.gcd(a, @alphabet_size) != 1 do {:error, "a and m must be coprime."} @@ -30,8 +34,7 @@ defmodule AffineCipher do @doc """ Decode an encrypted message using a key """ - @spec decode(key :: %{a: integer, b: integer}, message :: String.t()) :: - {:ok, String.t()} | {:error, String.t()} + @spec decode(key :: key(), message :: String.t()) :: {:ok, String.t()} | {:error, String.t()} def decode(%{a: a, b: b}, encrypted) do if Integer.gcd(a, @alphabet_size) != 1 do {:error, "a and m must be coprime."} diff --git a/exercises/practice/affine-cipher/lib/affine_cipher.ex b/exercises/practice/affine-cipher/lib/affine_cipher.ex index 7ab808d918..6cfabbdcd8 100644 --- a/exercises/practice/affine-cipher/lib/affine_cipher.ex +++ b/exercises/practice/affine-cipher/lib/affine_cipher.ex @@ -1,17 +1,20 @@ defmodule AffineCipher do + @doctype """ + A type for the encryption key + """ + @type key() :: %{a: integer, b: integer} + @doc """ Encode an encrypted message using a key """ - @spec encode(key :: %{a: integer, b: integer}, message :: String.t()) :: - {:ok, String.t()} | {:error, String.t()} + @spec encode(key :: key(), message :: String.t()) :: {:ok, String.t()} | {:error, String.t()} def encode(%{a: a, b: b}, message) do end @doc """ Decode an encrypted message using a key """ - @spec decode(key :: %{a: integer, b: integer}, message :: String.t()) :: - {:ok, String.t()} | {:error, String.t()} + @spec decode(key :: key(), message :: String.t()) :: {:ok, String.t()} | {:error, String.t()} def decode(%{a: a, b: b}, encrypted) do end end From c4c5ed5787464597661472a98999bbea7788729f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Gillet?= Date: Sun, 20 Jun 2021 21:03:02 +0900 Subject: [PATCH 7/8] Typo --- exercises/practice/affine-cipher/.meta/example.ex | 2 +- exercises/practice/affine-cipher/lib/affine_cipher.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exercises/practice/affine-cipher/.meta/example.ex b/exercises/practice/affine-cipher/.meta/example.ex index a45671bf60..8c297963ae 100644 --- a/exercises/practice/affine-cipher/.meta/example.ex +++ b/exercises/practice/affine-cipher/.meta/example.ex @@ -1,5 +1,5 @@ defmodule AffineCipher do - @doctype """ + @typedoc """ A type for the encryption key """ @type key() :: %{a: integer, b: integer} diff --git a/exercises/practice/affine-cipher/lib/affine_cipher.ex b/exercises/practice/affine-cipher/lib/affine_cipher.ex index 6cfabbdcd8..6b0b8c9960 100644 --- a/exercises/practice/affine-cipher/lib/affine_cipher.ex +++ b/exercises/practice/affine-cipher/lib/affine_cipher.ex @@ -1,5 +1,5 @@ defmodule AffineCipher do - @doctype """ + @typedoc""" A type for the encryption key """ @type key() :: %{a: integer, b: integer} From c9fc6923350bfad71701b1651479d917e7440256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Gillet?= Date: Sun, 20 Jun 2021 21:11:11 +0900 Subject: [PATCH 8/8] I should not rush --- exercises/practice/affine-cipher/lib/affine_cipher.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/affine-cipher/lib/affine_cipher.ex b/exercises/practice/affine-cipher/lib/affine_cipher.ex index 6b0b8c9960..f04dc5cc1e 100644 --- a/exercises/practice/affine-cipher/lib/affine_cipher.ex +++ b/exercises/practice/affine-cipher/lib/affine_cipher.ex @@ -1,5 +1,5 @@ defmodule AffineCipher do - @typedoc""" + @typedoc """ A type for the encryption key """ @type key() :: %{a: integer, b: integer}