diff --git a/config.json b/config.json index 27265e5e..9c092ca8 100644 --- a/config.json +++ b/config.json @@ -897,6 +897,14 @@ "practices": [], "prerequisites": [], "difficulty": 1 + }, + { + "slug": "affine-cipher", + "name": "Affine Cipher", + "uuid": "dc1161ae-d7c3-47e8-b3d0-3fd458eb6488", + "practices": [], + "prerequisites": [], + "difficulty": 1 } ] }, diff --git a/exercises/practice/affine-cipher/.docs/instructions.md b/exercises/practice/affine-cipher/.docs/instructions.md new file mode 100644 index 00000000..f6329db9 --- /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/config.json b/exercises/practice/affine-cipher/.meta/config.json new file mode 100644 index 00000000..0e89d002 --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "JohnMertz" + ], + "files": { + "solution": [ + "lib/AffineCipher.pm" + ], + "test": [ + "t/affine-cipher.t" + ], + "example": [ + ".meta/solutions/lib/AffineCipher.pm" + ] + }, + "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/solutions/lib/AffineCipher.pm b/exercises/practice/affine-cipher/.meta/solutions/lib/AffineCipher.pm new file mode 100644 index 00000000..b148c4da --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/solutions/lib/AffineCipher.pm @@ -0,0 +1,47 @@ +package AffineCipher; + +use strict; +use warnings; +use experimental qw; + +use Exporter qw; +our @EXPORT_OK = qw; + +sub encode ( $phrase, $a, $b ) { + die "a and m must be coprime." unless _coprime($a); + my $ret = ''; + foreach ( split( //, $phrase ) ) { + next if $_ eq ' '; + $ret .= ' ' if ( $_ =~ /[[:alpha:]\d]/ && !( ( length($ret) + 1 ) % 6 ) ); + if ( ord( lc($_) ) < 97 ) { + $ret .= $_ if ( $_ =~ /\d/ ); + } else { + $ret .= chr( ( ( $a * ( ord( lc($_) ) - 97 ) + $b ) % 26 ) + 97 ); + } + } + return $ret; +} + +sub decode ( $phrase, $a, $b ) { + die "a and m must be coprime." unless _coprime($a); + my ( $mmi, $ret ) = ( 0, '' ); + $mmi++ until ( ( $a * $mmi ) % 26 == 1 ); + foreach ( split( //, $phrase ) ) { + next if $_ eq ' '; + if ( ord( lc($_) ) < 97 ) { + $ret .= $_ if ( $_ =~ /\d/ ); + } else { + $ret .= chr( ( $mmi * ( ( ord( lc($_) ) - 97 ) - $b ) % 26 ) + 97 ); + } + } + return $ret; +} + +sub _coprime ($a) { + for ( 2 .. 26 ) { + return 0 unless ( $a % $_ + 26 % $_ ); + } + return 1; +} + +1; diff --git a/exercises/practice/affine-cipher/.meta/solutions/t/affine-cipher.t b/exercises/practice/affine-cipher/.meta/solutions/t/affine-cipher.t new file mode 120000 index 00000000..ab304e99 --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/solutions/t/affine-cipher.t @@ -0,0 +1 @@ +../../../t/affine-cipher.t \ No newline at end of file diff --git a/exercises/practice/affine-cipher/.meta/template-data.yaml b/exercises/practice/affine-cipher/.meta/template-data.yaml new file mode 100644 index 00000000..c908d724 --- /dev/null +++ b/exercises/practice/affine-cipher/.meta/template-data.yaml @@ -0,0 +1,90 @@ +subs: encode decode + +properties: + encode: + test: |- + use Data::Dmp; + if (ref $case->{expected} eq ref {}) { + sprintf(<<~'END', map {dmp($_)} $case->{input}{phrase}, $case->{input}{key}{a}, $case->{input}{key}{b}, qr/$case->{expected}{error}/, $case->{description}); + like( + dies( sub { encode(%s, %s, %s) } ), + %s, + %s, + ); + END + } else { + sprintf(<<~'END', dmp($case->{input}{phrase}), $case->{input}{key}{a}, $case->{input}{key}{b}, dmp($case->{expected}), dmp($case->{description})); + is( + encode(%s, %s, %s), + %s, + %s, + ); + END + } + decode: + test: |- + use Data::Dmp; + if (ref $case->{expected} eq ref {}) { + sprintf(<<~'END', map {dmp($_)} $case->{input}{phrase}, $case->{input}{key}{a}, $case->{input}{key}{b}, qr/$case->{expected}{error}/, $case->{description}); + like( + dies( sub { decode(%s, %s, %s) } ), + %s, + %s, + ); + END + } else { + sprintf(<<~'END', dmp($case->{input}{phrase}), $case->{input}{key}{a}, $case->{input}{key}{b}, dmp($case->{expected}), dmp($case->{description})); + is( + decode(%s, %s, %s), + %s, + %s, + ); + END + } + +stub: |- + sub encode ($phrase, $a, $b) { + return undef; + } + + sub decode ($phrase, $a, $b) { + return undef; + } + +example: |- + sub encode ($phrase, $a, $b) { + die "a and m must be coprime." unless _coprime($a); + my $ret = ''; + foreach (split(//, $phrase)) { + next if $_ eq ' '; + $ret .= ' ' if ($_ =~ /[[:alpha:]\d]/ && !((length($ret)+1) % 6)); + if (ord(lc($_)) < 97) { + $ret .= $_ if ($_ =~ /\d/); + } else { + $ret .= chr((($a*(ord(lc($_))-97) + $b) % 26)+97); + } + } + return $ret; + } + + sub decode ($phrase, $a, $b) { + die "a and m must be coprime." unless _coprime($a); + my ($mmi, $ret) = (0, ''); + $mmi++ until ( ($a * $mmi) % 26 == 1 ); + foreach (split(//, $phrase)) { + next if $_ eq ' '; + if (ord(lc($_)) < 97) { + $ret .= $_ if ($_ =~ /\d/); + } else { + $ret .= chr(($mmi*((ord(lc($_))-97)-$b) % 26)+97); + } + } + return $ret; + } + + sub _coprime ($a) { + for (2..26) { + return 0 unless ($a % $_ + 26 % $_); + } + return 1; + } diff --git a/exercises/practice/affine-cipher/.meta/tests.toml b/exercises/practice/affine-cipher/.meta/tests.toml new file mode 100644 index 00000000..07cce7c7 --- /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/lib/AffineCipher.pm b/exercises/practice/affine-cipher/lib/AffineCipher.pm new file mode 100644 index 00000000..f93724e2 --- /dev/null +++ b/exercises/practice/affine-cipher/lib/AffineCipher.pm @@ -0,0 +1,16 @@ +package AffineCipher; + +use v5.40; + +use Exporter qw; +our @EXPORT_OK = qw; + +sub encode ( $phrase, $a, $b ) { + return undef; +} + +sub decode ( $phrase, $a, $b ) { + return undef; +} + +1; diff --git a/exercises/practice/affine-cipher/t/affine-cipher.t b/exercises/practice/affine-cipher/t/affine-cipher.t new file mode 100755 index 00000000..8e84857a --- /dev/null +++ b/exercises/practice/affine-cipher/t/affine-cipher.t @@ -0,0 +1,105 @@ +#!/usr/bin/env perl +use Test2::V0; + +use FindBin qw<$Bin>; +use lib "$Bin/../lib", "$Bin/../local/lib/perl5"; + +use AffineCipher qw; + +is( # begin: 2ee1d9af-1c43-416c-b41b-cefd7d4d2b2a + encode( "yes", 5, 7 ), + "xbt", + "encode: encode yes", +); # end: 2ee1d9af-1c43-416c-b41b-cefd7d4d2b2a + +is( # begin: 785bade9-e98b-4d4f-a5b0-087ba3d7de4b + encode( "no", 15, 18 ), + "fu", + "encode: encode no", +); # end: 785bade9-e98b-4d4f-a5b0-087ba3d7de4b + +is( # begin: 2854851c-48fb-40d8-9bf6-8f192ed25054 + encode( "OMG", 21, 3 ), + "lvz", + "encode: encode OMG", +); # end: 2854851c-48fb-40d8-9bf6-8f192ed25054 + +is( # begin: bc0c1244-b544-49dd-9777-13a770be1bad + encode( "O M G", 25, 47 ), + "hjp", + "encode: encode O M G", +); # end: bc0c1244-b544-49dd-9777-13a770be1bad + +is( # begin: 381a1a20-b74a-46ce-9277-3778625c9e27 + encode( "mindblowingly", 11, 15 ), + "rzcwa gnxzc dgt", + "encode: encode mindblowingly", +); # end: 381a1a20-b74a-46ce-9277-3778625c9e27 + +is( # begin: 6686f4e2-753b-47d4-9715-876fdc59029d + encode( "Testing,1 2 3, testing.", 3, 4 ), + "jqgjc rw123 jqgjc rw", + "encode: encode numbers", +); # end: 6686f4e2-753b-47d4-9715-876fdc59029d + +is( # begin: ae23d5bd-30a8-44b6-afbe-23c8c0c7faa3 + encode( "Truth is fiction.", 5, 17 ), + "iynia fdqfb ifje", + "encode: encode deep thought", +); # end: ae23d5bd-30a8-44b6-afbe-23c8c0c7faa3 + +is( # begin: c93a8a4d-426c-42ef-9610-76ded6f7ef57 + encode( "The quick brown fox jumps over the lazy dog.", 17, 33 ), + "swxtj npvyk lruol iejdc blaxk swxmh qzglf", + "encode: encode all the letters", +); # end: c93a8a4d-426c-42ef-9610-76ded6f7ef57 + +like( # begin: 0673638a-4375-40bd-871c-fb6a2c28effb + dies( sub { encode( "This is a test.", 6, 17 ) } ), + qr(a and m must be coprime.), + "encode: encode with a not coprime to m", +); # end: 0673638a-4375-40bd-871c-fb6a2c28effb + +is( # begin: 3f0ac7e2-ec0e-4a79-949e-95e414953438 + decode( "tytgn fjr", 3, 7 ), + "exercism", + "decode: decode exercism", +); # end: 3f0ac7e2-ec0e-4a79-949e-95e414953438 + +is( # begin: 241ee64d-5a47-4092-a5d7-7939d259e077 + decode( "qdwju nqcro muwhn odqun oppmd aunwd o", 19, 16 ), + "anobstacleisoftenasteppingstone", + "decode: decode a sentence", +); # end: 241ee64d-5a47-4092-a5d7-7939d259e077 + +is( # begin: 33fb16a1-765a-496f-907f-12e644837f5e + decode( "odpoz ub123 odpoz ub", 25, 7 ), + "testing123testing", + "decode: decode numbers", +); # end: 33fb16a1-765a-496f-907f-12e644837f5e + +is( # begin: 20bc9dce-c5ec-4db6-a3f1-845c776bcbf7 + decode( "swxtj npvyk lruol iejdc blaxk swxmh qzglf", 17, 33 ), + "thequickbrownfoxjumpsoverthelazydog", + "decode: decode all the letters", +); # end: 20bc9dce-c5ec-4db6-a3f1-845c776bcbf7 + +is( # begin: 623e78c0-922d-49c5-8702-227a3e8eaf81 + decode( "swxtjnpvyklruoliejdcblaxkswxmhqzglf", 17, 33 ), + "thequickbrownfoxjumpsoverthelazydog", + "decode: decode with no spaces in input", +); # end: 623e78c0-922d-49c5-8702-227a3e8eaf81 + +is( # begin: 58fd5c2a-1fd9-4563-a80a-71cff200f26f + decode( "vszzm cly yd cg qdp", 15, 16 ), + "jollygreengiant", + "decode: decode with too many spaces", +); # end: 58fd5c2a-1fd9-4563-a80a-71cff200f26f + +like( # begin: b004626f-c186-4af9-a3f4-58f74cdb86d5 + dies( sub { decode( "Test", 13, 5 ) } ), + qr(a and m must be coprime.), + "decode: decode with a not coprime to m", +); # end: b004626f-c186-4af9-a3f4-58f74cdb86d5 + +done_testing;