diff --git a/config.json b/config.json index 957be6f..7b52802 100644 --- a/config.json +++ b/config.json @@ -547,6 +547,14 @@ "prerequisites": [], "difficulty": 3 }, + { + "slug": "affine-cipher", + "name": "Affine Cipher", + "uuid": "54da632d-ab6a-4a6f-8a06-cb40b84e60fe", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "anagram", "name": "Anagram", diff --git a/exercises/practice/affine-cipher/.docs/instructions.md b/exercises/practice/affine-cipher/.docs/instructions.md new file mode 100644 index 0000000..4eff918 --- /dev/null +++ b/exercises/practice/affine-cipher/.docs/instructions.md @@ -0,0 +1,74 @@ +# Instructions + +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 cipher is much stronger than the atbash cipher, because it has many more keys. + +[//]: # " monoalphabetic as spelled by Merriam-Webster, compare to polyalphabetic " + +## Encryption + +The encryption function is: + +```text +E(x) = (ai + b) mod m +``` + +Where: + +- `i` is the letter's index from `0` to the length of the alphabet - 1. +- `m` is the length of the alphabet. + For the Roman alphabet `m` is `26`. +- `a` and `b` are integers which make up the encryption key. + +Values `a` and `m` must be _coprime_ (or, _relatively prime_) for automatic decryption to succeed, i.e., they have number `1` as their only common factor (more information can be found in the [Wikipedia article about coprime integers][coprime-integers]). +In case `a` is not coprime to `m`, your program should indicate that this is an error. +Otherwise it should encrypt or decrypt with the provided key. + +For the purpose of this exercise, digits are valid input but they are not encrypted. +Spaces and punctuation characters are excluded. +Ciphertext is written out in groups of fixed length separated by space, the traditional group size being `5` letters. +This is to make it harder to guess encrypted text based on word boundaries. + +## Decryption + +The decryption function is: + +```text +D(y) = (a^-1)(y - b) mod m +``` + +Where: + +- `y` is the numeric value of an encrypted letter, i.e., `y = E(x)` +- it is important to note that `a^-1` is the modular multiplicative inverse (MMI) of `a mod m` +- the modular multiplicative inverse only exists if `a` and `m` are coprime. + +The MMI of `a` is `x` such that the remainder after dividing `ax` by `m` is `1`: + +```text +ax mod m = 1 +``` + +More information regarding how to find a Modular Multiplicative Inverse and what it means can be found in the [related Wikipedia article][mmi]. + +## General Examples + +- Encrypting `"test"` gives `"ybty"` with the key `a = 5`, `b = 7` +- Decrypting `"ybty"` gives `"test"` with the key `a = 5`, `b = 7` +- Decrypting `"ybty"` gives `"lqul"` with the wrong key `a = 11`, `b = 7` +- Decrypting `"kqlfd jzvgy tpaet icdhm rtwly kqlon ubstx"` gives `"thequickbrownfoxjumpsoverthelazydog"` with the key `a = 19`, `b = 13` +- Encrypting `"test"` with the key `a = 18`, `b = 13` is an error because `18` and `26` are not coprime + +## Example of finding a Modular Multiplicative Inverse (MMI) + +Finding MMI for `a = 15`: + +- `(15 * x) mod 26 = 1` +- `(15 * 7) mod 26 = 1`, ie. `105 mod 26 = 1` +- `7` is the MMI of `15 mod 26` + +[mmi]: https://en.wikipedia.org/wiki/Modular_multiplicative_inverse +[coprime-integers]: https://en.wikipedia.org/wiki/Coprime_integers diff --git a/exercises/practice/affine-cipher/.meta/Example.roc b/exercises/practice/affine-cipher/.meta/Example.roc new file mode 100644 index 0000000..8d27cbc --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/Example.roc @@ -0,0 +1,72 @@ +module [encode, decode] + +alphabetSize : U64 +alphabetSize = 26 + +groupLength : U64 +groupLength = 5 + +encode : Str, { a : U64, b : U64 } -> Result Str [InvalidKey, BadUtf8 _ _] +encode = \phrase, key -> + alphabet = encodedAlphabet? key + phrase + |> Str.toUtf8 + |> List.joinMap \char -> + if char >= '0' && char <= '9' then + [char] + else + + charLower = if char >= 'A' && char <= 'Z' then char - 'A' + 'a' else char + if charLower >= 'a' && charLower <= 'z' then + index = charLower - 'a' |> Num.toU64 + when alphabet |> List.get index is + Ok encodedChar -> [encodedChar] + Err OutOfBounds -> crash "Unreachable: index cannot be out of bounds here" + else + [] + |> List.chunksOf groupLength + |> List.intersperse [' '] + |> List.join + |> Str.fromUtf8 + +encodedAlphabet : { a : U64, b : U64 } -> Result (List U8) [InvalidKey] +encodedAlphabet = \{ a, b } -> + encoded = + List.range { start: At 'a', end: At 'z' } + |> List.map \char -> + num = (char - 'a') |> Num.toU64 + index = (a * num + b) % alphabetSize + 'a' + Num.toU8 index + if (encoded |> Set.fromList |> Set.len) < alphabetSize then + Err InvalidKey + else + Ok encoded + +decodedAlphabet : { a : U64, b : U64 } -> Result (List U8) [InvalidKey] +decodedAlphabet = \key -> + encodedAlphabet? key + |> List.mapWithIndex \encoded, decodedIndex -> { encoded, decodedIndex } + |> List.sortWith \{ encoded: encoded1 }, { encoded: encoded2 } -> + Num.compare encoded1 encoded2 + |> List.map \pair -> Num.toU8 pair.decodedIndex + 'a' + |> Ok + +decode : Str, { a : U64, b : U64 } -> Result Str [InvalidKey, BadUtf8 _ _, InvalidCharacter] +decode = \phrase, key -> + alphabet = decodedAlphabet? key + phrase + |> Str.toUtf8 + |> List.mapTry? \char -> + if char == ' ' then + Ok [] + else if char >= '0' && char <= '9' then + Ok [char] + else if char >= 'a' && char <= 'z' then + index = char - 'a' |> Num.toU64 + when alphabet |> List.get index is + Ok decodedChar -> Ok [decodedChar] + Err OutOfBounds -> crash "Unreachable: index cannot be out of bounds here" + else + Err InvalidCharacter + |> List.join + |> Str.fromUtf8 diff --git a/exercises/practice/affine-cipher/.meta/config.json b/exercises/practice/affine-cipher/.meta/config.json new file mode 100644 index 0000000..5287fea --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "ageron" + ], + "files": { + "solution": [ + "AffineCipher.roc" + ], + "test": [ + "affine-cipher-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "blurb": "Create an implementation of the Affine cipher, an ancient encryption algorithm from the Middle East.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Affine_cipher" +} diff --git a/exercises/practice/affine-cipher/.meta/template.j2 b/exercises/practice/affine-cipher/.meta/template.j2 new file mode 100644 index 0000000..9690a49 --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/template.j2 @@ -0,0 +1,26 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header() }} + +import {{ exercise | to_pascal }} exposing [encode, decode] + +{% for supercase in cases %} +## +## {{ supercase["description"] }} +## + +{% for case in supercase["cases"] -%} +# {{ case["description"] }} +expect + phrase = {{ case["input"]["phrase"] | to_roc }} + key = {a: {{ case["input"]["key"]["a"] }}, b: {{ case["input"]["key"]["b"] }}} + result = {{ case["property"] | to_camel }} phrase key + {%- if case["expected"]["error"] %} + result |> Result.isErr + {%- else %} + expected = Ok {{ case["expected"] | to_roc }} + result == expected + {%- endif %} + +{% endfor %} +{% endfor %} diff --git a/exercises/practice/affine-cipher/.meta/tests.toml b/exercises/practice/affine-cipher/.meta/tests.toml new file mode 100644 index 0000000..07cce7c --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/tests.toml @@ -0,0 +1,58 @@ +# 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/AffineCipher.roc b/exercises/practice/affine-cipher/AffineCipher.roc new file mode 100644 index 0000000..e93e906 --- /dev/null +++ b/exercises/practice/affine-cipher/AffineCipher.roc @@ -0,0 +1,9 @@ +module [encode, decode] + +encode : Str, { a : U64, b : U64 } -> Result Str _ +encode = \phrase, key -> + crash "Please implement the 'encode' function" + +decode : Str, { a : U64, b : U64 } -> Result Str _ +decode = \phrase, key -> + crash "Please implement the 'decode' function" diff --git a/exercises/practice/affine-cipher/affine-cipher-test.roc b/exercises/practice/affine-cipher/affine-cipher-test.roc new file mode 100644 index 0000000..8cebcf8 --- /dev/null +++ b/exercises/practice/affine-cipher/affine-cipher-test.roc @@ -0,0 +1,146 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/affine-cipher/canonical-data.json +# File last updated on 2024-10-23 +app [main] { + pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br", +} + +main = + Task.ok {} + +import AffineCipher exposing [encode, decode] + +## +## encode +## + +# encode yes +expect + phrase = "yes" + key = { a: 5, b: 7 } + result = encode phrase key + expected = Ok "xbt" + result == expected + +# encode no +expect + phrase = "no" + key = { a: 15, b: 18 } + result = encode phrase key + expected = Ok "fu" + result == expected + +# encode OMG +expect + phrase = "OMG" + key = { a: 21, b: 3 } + result = encode phrase key + expected = Ok "lvz" + result == expected + +# encode O M G +expect + phrase = "O M G" + key = { a: 25, b: 47 } + result = encode phrase key + expected = Ok "hjp" + result == expected + +# encode mindblowingly +expect + phrase = "mindblowingly" + key = { a: 11, b: 15 } + result = encode phrase key + expected = Ok "rzcwa gnxzc dgt" + result == expected + +# encode numbers +expect + phrase = "Testing,1 2 3, testing." + key = { a: 3, b: 4 } + result = encode phrase key + expected = Ok "jqgjc rw123 jqgjc rw" + result == expected + +# encode deep thought +expect + phrase = "Truth is fiction." + key = { a: 5, b: 17 } + result = encode phrase key + expected = Ok "iynia fdqfb ifje" + result == expected + +# encode all the letters +expect + phrase = "The quick brown fox jumps over the lazy dog." + key = { a: 17, b: 33 } + result = encode phrase key + expected = Ok "swxtj npvyk lruol iejdc blaxk swxmh qzglf" + result == expected + +# encode with a not coprime to m +expect + phrase = "This is a test." + key = { a: 6, b: 17 } + result = encode phrase key + result |> Result.isErr + +## +## decode +## + +# decode exercism +expect + phrase = "tytgn fjr" + key = { a: 3, b: 7 } + result = decode phrase key + expected = Ok "exercism" + result == expected + +# decode a sentence +expect + phrase = "qdwju nqcro muwhn odqun oppmd aunwd o" + key = { a: 19, b: 16 } + result = decode phrase key + expected = Ok "anobstacleisoftenasteppingstone" + result == expected + +# decode numbers +expect + phrase = "odpoz ub123 odpoz ub" + key = { a: 25, b: 7 } + result = decode phrase key + expected = Ok "testing123testing" + result == expected + +# decode all the letters +expect + phrase = "swxtj npvyk lruol iejdc blaxk swxmh qzglf" + key = { a: 17, b: 33 } + result = decode phrase key + expected = Ok "thequickbrownfoxjumpsoverthelazydog" + result == expected + +# decode with no spaces in input +expect + phrase = "swxtjnpvyklruoliejdcblaxkswxmhqzglf" + key = { a: 17, b: 33 } + result = decode phrase key + expected = Ok "thequickbrownfoxjumpsoverthelazydog" + result == expected + +# decode with too many spaces +expect + phrase = "vszzm cly yd cg qdp" + key = { a: 15, b: 16 } + result = decode phrase key + expected = Ok "jollygreengiant" + result == expected + +# decode with a not coprime to m +expect + phrase = "Test" + key = { a: 13, b: 5 } + result = decode phrase key + result |> Result.isErr +