From 376725716fb04347323391ed9ded75f727d1c1b4 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Wed, 8 Apr 2026 17:21:16 -0400 Subject: [PATCH] Add affine-cipher --- config.json | 8 ++ exercises/practice/affine-cipher/.busted | 5 ++ .../affine-cipher/.docs/instructions.md | 74 +++++++++++++++++ .../practice/affine-cipher/.meta/config.json | 19 +++++ .../practice/affine-cipher/.meta/example.moon | 39 +++++++++ .../affine-cipher/.meta/spec_generator.moon | 18 ++++ .../practice/affine-cipher/.meta/tests.toml | 58 +++++++++++++ .../practice/affine-cipher/affine_cipher.moon | 7 ++ .../affine-cipher/affine_cipher_spec.moon | 82 +++++++++++++++++++ 9 files changed, 310 insertions(+) create mode 100644 exercises/practice/affine-cipher/.busted create mode 100644 exercises/practice/affine-cipher/.docs/instructions.md create mode 100644 exercises/practice/affine-cipher/.meta/config.json create mode 100644 exercises/practice/affine-cipher/.meta/example.moon create mode 100644 exercises/practice/affine-cipher/.meta/spec_generator.moon create mode 100644 exercises/practice/affine-cipher/.meta/tests.toml create mode 100644 exercises/practice/affine-cipher/affine_cipher.moon create mode 100644 exercises/practice/affine-cipher/affine_cipher_spec.moon diff --git a/config.json b/config.json index 45d5020..717c659 100644 --- a/config.json +++ b/config.json @@ -618,6 +618,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "affine-cipher", + "name": "Affine Cipher", + "uuid": "b9d2eda3-0e01-4f08-b8e3-b5fd19759d5e", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "binary-search-tree", "name": "Binary Search Tree", diff --git a/exercises/practice/affine-cipher/.busted b/exercises/practice/affine-cipher/.busted new file mode 100644 index 0000000..86b84e7 --- /dev/null +++ b/exercises/practice/affine-cipher/.busted @@ -0,0 +1,5 @@ +return { + default = { + ROOT = { '.' } + } +} diff --git a/exercises/practice/affine-cipher/.docs/instructions.md b/exercises/practice/affine-cipher/.docs/instructions.md new file mode 100644 index 0000000..1603dbb --- /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 Latin 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/config.json b/exercises/practice/affine-cipher/.meta/config.json new file mode 100644 index 0000000..1be8ecd --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "affine_cipher.moon" + ], + "test": [ + "affine_cipher_spec.moon" + ], + "example": [ + ".meta/example.moon" + ] + }, + "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/example.moon b/exercises/practice/affine-cipher/.meta/example.moon new file mode 100644 index 0000000..f06cb89 --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/example.moon @@ -0,0 +1,39 @@ +M = 26 -- size of alphabet + +gcd = (a, b) -> + while b != 0 + a, b = b, a % b + a + +mmi = (a, m) -> + for x = 1, m + return x if (a * x) % m == 1 + error 'should not happen: cannot determine MMI of #{a} and #{m}' + +ord = (letter) -> letter\byte! +chr = (number) -> string.char number +A = ord 'a' + +validate = (a, m) -> + assert gcd(a, m) == 1, 'a and m must be coprime.' + +add_spaces = (str, n = 5) -> + str\gsub('.'\rep(n), '%0 ')\gsub(' $', '') + +encipher = (text, func) -> + encipherer = (c) -> c\match('%d') and c or chr(A + func(ord(c) - A)) + table.concat [encipherer c for c in text\lower!\gmatch '%w'] + + +{ + encode: (phrase, key) -> + validate key.a, M + encoder = (x) -> (key.a * x + key.b) % M + add_spaces encipher(phrase, encoder) + + decode: (phrase, key) -> + validate key.a, M + a_prime = mmi key.a, M + decoder = (y) -> (a_prime * (y - key.b)) % M + encipher(phrase, decoder) +} diff --git a/exercises/practice/affine-cipher/.meta/spec_generator.moon b/exercises/practice/affine-cipher/.meta/spec_generator.moon new file mode 100644 index 0000000..2ecfb8a --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/spec_generator.moon @@ -0,0 +1,18 @@ +{ + module_imports: {'encode', 'decode'}, + + generate_test: (case, level) -> + local lines + if type(case.expected) == 'string' + lines = { + "result = #{case.property} #{quote case.input.phrase}, {a: #{case.input.key.a}, b: #{case.input.key.b}}", + "expected = #{quote case.expected}", + "assert.are.equal expected, result" + } + else + lines = { + "f = -> #{case.property} #{quote case.input.phrase}, {a: #{case.input.key.a}, b: #{case.input.key.b}}", + "assert.has.error f, #{quote case.expected.error}" + } + table.concat [indent line, level for line in *lines], '\n' +} 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/affine_cipher.moon b/exercises/practice/affine-cipher/affine_cipher.moon new file mode 100644 index 0000000..f94f769 --- /dev/null +++ b/exercises/practice/affine-cipher/affine_cipher.moon @@ -0,0 +1,7 @@ +{ + encode: (phrase, key) -> + error 'Implement the encode function' + + decode: (phrase, key) -> + error 'Implement the decode function' +} diff --git a/exercises/practice/affine-cipher/affine_cipher_spec.moon b/exercises/practice/affine-cipher/affine_cipher_spec.moon new file mode 100644 index 0000000..4cf188a --- /dev/null +++ b/exercises/practice/affine-cipher/affine_cipher_spec.moon @@ -0,0 +1,82 @@ +import encode, decode from require 'affine_cipher' + +describe 'affine-cipher', -> + describe 'encode', -> + it 'encode yes', -> + result = encode 'yes', {a: 5, b: 7} + expected = 'xbt' + assert.are.equal expected, result + + pending 'encode no', -> + result = encode 'no', {a: 15, b: 18} + expected = 'fu' + assert.are.equal expected, result + + pending 'encode OMG', -> + result = encode 'OMG', {a: 21, b: 3} + expected = 'lvz' + assert.are.equal expected, result + + pending 'encode O M G', -> + result = encode 'O M G', {a: 25, b: 47} + expected = 'hjp' + assert.are.equal expected, result + + pending 'encode mindblowingly', -> + result = encode 'mindblowingly', {a: 11, b: 15} + expected = 'rzcwa gnxzc dgt' + assert.are.equal expected, result + + pending 'encode numbers', -> + result = encode 'Testing,1 2 3, testing.', {a: 3, b: 4} + expected = 'jqgjc rw123 jqgjc rw' + assert.are.equal expected, result + + pending 'encode deep thought', -> + result = encode 'Truth is fiction.', {a: 5, b: 17} + expected = 'iynia fdqfb ifje' + assert.are.equal expected, result + + pending 'encode all the letters', -> + result = encode 'The quick brown fox jumps over the lazy dog.', {a: 17, b: 33} + expected = 'swxtj npvyk lruol iejdc blaxk swxmh qzglf' + assert.are.equal expected, result + + pending 'encode with a not coprime to m', -> + f = -> encode 'This is a test.', {a: 6, b: 17} + assert.has.error f, 'a and m must be coprime.' + + describe 'decode', -> + pending 'decode exercism', -> + result = decode 'tytgn fjr', {a: 3, b: 7} + expected = 'exercism' + assert.are.equal expected, result + + pending 'decode a sentence', -> + result = decode 'qdwju nqcro muwhn odqun oppmd aunwd o', {a: 19, b: 16} + expected = 'anobstacleisoftenasteppingstone' + assert.are.equal expected, result + + pending 'decode numbers', -> + result = decode 'odpoz ub123 odpoz ub', {a: 25, b: 7} + expected = 'testing123testing' + assert.are.equal expected, result + + pending 'decode all the letters', -> + result = decode 'swxtj npvyk lruol iejdc blaxk swxmh qzglf', {a: 17, b: 33} + expected = 'thequickbrownfoxjumpsoverthelazydog' + assert.are.equal expected, result + + pending 'decode with no spaces in input', -> + result = decode 'swxtjnpvyklruoliejdcblaxkswxmhqzglf', {a: 17, b: 33} + expected = 'thequickbrownfoxjumpsoverthelazydog' + assert.are.equal expected, result + + pending 'decode with too many spaces', -> + result = decode 'vszzm cly yd cg qdp', {a: 15, b: 16} + expected = 'jollygreengiant' + assert.are.equal expected, result + + pending 'decode with a not coprime to m', -> + f = -> decode 'Test', {a: 13, b: 5} + assert.has.error f, 'a and m must be coprime.'