From ec5f981aef864a405e3294077c30c37ff153f248 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Wed, 20 Sep 2023 13:57:05 +0200 Subject: [PATCH] Sync affine-cipher with problem-specifications (#1748) --- .../affine-cipher/.docs/instructions.md | 108 +++++++------- .../practice/affine-cipher/.meta/config.json | 2 +- .../affine-cipher/.meta/test_template.tera | 18 +++ .../affine-cipher/tests/affine-cipher.rs | 140 ++++++++++-------- problem-specifications | 2 +- 5 files changed, 157 insertions(+), 113 deletions(-) create mode 100644 exercises/practice/affine-cipher/.meta/test_template.tera diff --git a/exercises/practice/affine-cipher/.docs/instructions.md b/exercises/practice/affine-cipher/.docs/instructions.md index b3ebb9c76..26ce15342 100644 --- a/exercises/practice/affine-cipher/.docs/instructions.md +++ b/exercises/practice/affine-cipher/.docs/instructions.md @@ -1,70 +1,74 @@ # Instructions -Create an implementation of the affine cipher, -an ancient encryption system created in the Middle East. +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. +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: - `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 +```text +E(x) = (ai + b) mod m +``` -The decryption function is: +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 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. - `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. +## Decryption -To find the MMI of `a`: +The decryption function is: + +```text +D(y) = (a^-1)(y - b) mod m +``` - `an mod m = 1` - - where `n` is the modular multiplicative inverse of `a mod m` +Where: -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) +- `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. -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 MMI of `a` is `x` such that the remainder after dividing `ax` by `m` is `1`: -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. +```text +ax mod m = 1 +``` -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. +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 - - 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` +- 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 index 10c809f0a..34bb0ac2b 100644 --- a/exercises/practice/affine-cipher/.meta/config.json +++ b/exercises/practice/affine-cipher/.meta/config.json @@ -24,5 +24,5 @@ }, "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" + "source_url": "https://en.wikipedia.org/wiki/Affine_cipher" } diff --git a/exercises/practice/affine-cipher/.meta/test_template.tera b/exercises/practice/affine-cipher/.meta/test_template.tera new file mode 100644 index 000000000..0c15e457a --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/test_template.tera @@ -0,0 +1,18 @@ +use affine_cipher::AffineCipherError::NotCoprime; +{% for test in cases %} +#[test] +{% if loop.index != 1 -%} +#[ignore] +{% endif -%} +fn {{ test.description | slugify | replace(from="-", to="_") }}() { + let phrase = {{ test.input.phrase | json_encode() }}; + let (a, b) = ({{ test.input.key.a }}, {{ test.input.key.b }}); + let output = {{ crate_name }}::{{ test.property }}(phrase, a, b); + let expected = {% if test.expected is object -%} + Err(NotCoprime({{ test.input.key.a }})) + {%- else -%} + Ok({{ test.expected | json_encode() }}.into()) + {%- endif %}; + assert_eq!(output, expected); +} +{% endfor -%} diff --git a/exercises/practice/affine-cipher/tests/affine-cipher.rs b/exercises/practice/affine-cipher/tests/affine-cipher.rs index 69dbf34ac..2476677a0 100644 --- a/exercises/practice/affine-cipher/tests/affine-cipher.rs +++ b/exercises/practice/affine-cipher/tests/affine-cipher.rs @@ -1,138 +1,160 @@ -use affine_cipher::*; +use affine_cipher::AffineCipherError::NotCoprime; #[test] fn encode_yes() { - assert_eq!(encode("yes", 5, 7).unwrap(), "xbt") + let phrase = "yes"; + let (a, b) = (5, 7); + let output = affine_cipher::encode(phrase, a, b); + let expected = Ok("xbt".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn encode_no() { - assert_eq!(encode("no", 15, 18).unwrap(), "fu") + let phrase = "no"; + let (a, b) = (15, 18); + let output = affine_cipher::encode(phrase, a, b); + let expected = Ok("fu".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn encode_omg() { - assert_eq!(encode("OMG", 21, 3).unwrap(), "lvz") + let phrase = "OMG"; + let (a, b) = (21, 3); + let output = affine_cipher::encode(phrase, a, b); + let expected = Ok("lvz".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn encode_o_m_g() { - assert_eq!(encode("O M G", 25, 47).unwrap(), "hjp") + let phrase = "O M G"; + let (a, b) = (25, 47); + let output = affine_cipher::encode(phrase, a, b); + let expected = Ok("hjp".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn encode_mindblowingly() { - assert_eq!(encode("mindblowingly", 11, 15).unwrap(), "rzcwa gnxzc dgt") + let phrase = "mindblowingly"; + let (a, b) = (11, 15); + let output = affine_cipher::encode(phrase, a, b); + let expected = Ok("rzcwa gnxzc dgt".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn encode_numbers() { - assert_eq!( - encode("Testing,1 2 3, testing.", 3, 4).unwrap(), - "jqgjc rw123 jqgjc rw" - ) + let phrase = "Testing,1 2 3, testing."; + let (a, b) = (3, 4); + let output = affine_cipher::encode(phrase, a, b); + let expected = Ok("jqgjc rw123 jqgjc rw".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn encode_deep_thought() { - assert_eq!( - encode("Truth is fiction", 5, 17).unwrap(), - "iynia fdqfb ifje" - ) + let phrase = "Truth is fiction."; + let (a, b) = (5, 17); + let output = affine_cipher::encode(phrase, a, b); + let expected = Ok("iynia fdqfb ifje".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn encode_all_the_letters() { - assert_eq!( - encode("The quick brown fox jumps over the lazy dog.", 17, 33).unwrap(), - "swxtj npvyk lruol iejdc blaxk swxmh qzglf" - ) + let phrase = "The quick brown fox jumps over the lazy dog."; + let (a, b) = (17, 33); + let output = affine_cipher::encode(phrase, a, b); + let expected = Ok("swxtj npvyk lruol iejdc blaxk swxmh qzglf".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn encode_with_a_not_coprime_to_m() { - const EXPECTED_ERROR: AffineCipherError = AffineCipherError::NotCoprime(6); - match encode("This is a test.", 6, 17) { - Err(EXPECTED_ERROR) => (), - Err(err) => panic!("Incorrect error: expected: {EXPECTED_ERROR:?}, actual: {err:?}."), - Ok(r) => panic!( - "Cannot encode/decode when a is coprime to m: expected: {EXPECTED_ERROR:?}, actual: {r:?}." - ), - } + let phrase = "This is a test."; + let (a, b) = (6, 17); + let output = affine_cipher::encode(phrase, a, b); + let expected = Err(NotCoprime(6)); + assert_eq!(output, expected); } #[test] #[ignore] fn decode_exercism() { - assert_eq!(decode("tytgn fjr", 3, 7).unwrap(), "exercism") + let phrase = "tytgn fjr"; + let (a, b) = (3, 7); + let output = affine_cipher::decode(phrase, a, b); + let expected = Ok("exercism".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn decode_a_sentence() { - assert_eq!( - encode("anobstacleisoftenasteppingstone", 19, 16).unwrap(), - "qdwju nqcro muwhn odqun oppmd aunwd o" - ); - assert_eq!( - decode("qdwju nqcro muwhn odqun oppmd aunwd o", 19, 16).unwrap(), - "anobstacleisoftenasteppingstone" - ) + let phrase = "qdwju nqcro muwhn odqun oppmd aunwd o"; + let (a, b) = (19, 16); + let output = affine_cipher::decode(phrase, a, b); + let expected = Ok("anobstacleisoftenasteppingstone".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn decode_numbers() { - assert_eq!( - decode("odpoz ub123 odpoz ub", 25, 7).unwrap(), - "testing123testing" - ) + let phrase = "odpoz ub123 odpoz ub"; + let (a, b) = (25, 7); + let output = affine_cipher::decode(phrase, a, b); + let expected = Ok("testing123testing".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn decode_all_the_letters() { - assert_eq!( - decode("swxtj npvyk lruol iejdc blaxk swxmh qzglf", 17, 33).unwrap(), - "thequickbrownfoxjumpsoverthelazydog" - ) + let phrase = "swxtj npvyk lruol iejdc blaxk swxmh qzglf"; + let (a, b) = (17, 33); + let output = affine_cipher::decode(phrase, a, b); + let expected = Ok("thequickbrownfoxjumpsoverthelazydog".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn decode_with_no_spaces_in_input() { - assert_eq!( - decode("swxtjnpvyklruoliejdcblaxkswxmhqzglf", 17, 33).unwrap(), - "thequickbrownfoxjumpsoverthelazydog" - ) + let phrase = "swxtjnpvyklruoliejdcblaxkswxmhqzglf"; + let (a, b) = (17, 33); + let output = affine_cipher::decode(phrase, a, b); + let expected = Ok("thequickbrownfoxjumpsoverthelazydog".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn decode_with_too_many_spaces() { - assert_eq!( - decode("vszzm cly yd cg qdp", 15, 16).unwrap(), - "jollygreengiant" - ) + let phrase = "vszzm cly yd cg qdp"; + let (a, b) = (15, 16); + let output = affine_cipher::decode(phrase, a, b); + let expected = Ok("jollygreengiant".into()); + assert_eq!(output, expected); } #[test] #[ignore] fn decode_with_a_not_coprime_to_m() { - const EXPECTED_ERROR: AffineCipherError = AffineCipherError::NotCoprime(13); - match decode("Test", 13, 11) { - Err(EXPECTED_ERROR) => (), - Err(e) => panic!("Incorrect error: expected: {EXPECTED_ERROR:?}, actual: {e:?}."), - Ok(r) => panic!( - "Cannot encode/decode when a is coprime to m: expected: {EXPECTED_ERROR:?}, actual: {r:?}." - ), - } + let phrase = "Test"; + let (a, b) = (13, 5); + let output = affine_cipher::decode(phrase, a, b); + let expected = Err(NotCoprime(13)); + assert_eq!(output, expected); } diff --git a/problem-specifications b/problem-specifications index d2229dedf..03220a9ce 160000 --- a/problem-specifications +++ b/problem-specifications @@ -1 +1 @@ -Subproject commit d2229dedfa6c6a6bb7d98dc49548d9ae06d0a848 +Subproject commit 03220a9ceb507f46cb70e317aaf1620237435144