Skip to content

Commit d2ff42e

Browse files
jackattack24cmccandless
authored andcommitted
affine-cipher: implement exercise (exercism#1501)
* create empty files for implementation * implement exercise and tests * add exercise entry to config.json * generate README * fix formatting issues * update unlocked_by to core exercise * edit topics and change tests
1 parent de00314 commit d2ff42e

File tree

5 files changed

+261
-0
lines changed

5 files changed

+261
-0
lines changed

config.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,6 +1368,18 @@
13681368
"strings"
13691369
]
13701370
},
1371+
{
1372+
"slug": "affine-cipher",
1373+
"uuid": "02bf6783-fc74-47e9-854f-44d22eb1b6f8",
1374+
"core": false,
1375+
"unlocked_by": "grade-school",
1376+
"difficulty": 5,
1377+
"topics": [
1378+
"algorithms",
1379+
"cryptography",
1380+
"strings"
1381+
]
1382+
},
13711383
{
13721384
"slug": "accumulate",
13731385
"uuid": "e7351e8e-d3ff-4621-b818-cd55cf05bffd",

exercises/affine-cipher/README.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Affine Cipher
2+
3+
Create an implementation of the affine cipher,
4+
an ancient encryption system created in the Middle East.
5+
6+
The affine cipher is a type of monoalphabetic substitution cipher.
7+
Each character is mapped to its numeric equivalent, encrypted with
8+
a mathematical function and then converted to the letter relating to
9+
its new numeric value. Although all monoalphabetic ciphers are weak,
10+
the affine cypher is much stronger than the atbash cipher,
11+
because it has many more keys.
12+
13+
the encryption function is:
14+
15+
`E(x) = (ax + b) mod m`
16+
- where `x` is the letter's index from 0 - length of alphabet - 1
17+
- `m` is the length of the alphabet. For the roman alphabet `m == 26`.
18+
- and `a` and `b` make the key
19+
20+
the decryption function is:
21+
22+
`D(y) = a^-1(y - b) mod m`
23+
- where `y` is the numeric value of an encrypted letter, ie. `y = E(x)`
24+
- it is important to note that `a^-1` is the modular multiplicative inverse
25+
of `a mod m`
26+
- the modular multiplicative inverse of `a` only exists if `a` and `m` are
27+
coprime.
28+
29+
To find the MMI of `a`:
30+
31+
`an mod m = 1`
32+
- where `n` is the modular multiplicative inverse of `a mod m`
33+
34+
More information regarding how to find a Modular Multiplicative Inverse
35+
and what it means can be found [here.](https://en.wikipedia.org/wiki/Modular_multiplicative_inverse)
36+
37+
Because automatic decryption fails if `a` is not coprime to `m` your
38+
program should return status 1 and `"Error: a and m must be coprime."`
39+
if they are not. Otherwise it should encode or decode with the
40+
provided key.
41+
42+
The Caesar (shift) cipher is a simple affine cipher where `a` is 1 and
43+
`b` as the magnitude results in a static displacement of the letters.
44+
This is much less secure than a full implementation of the affine cipher.
45+
46+
Ciphertext is written out in groups of fixed length, the traditional group
47+
size being 5 letters, and punctuation is excluded. This is to make it
48+
harder to guess things based on word boundaries.
49+
50+
## Examples
51+
52+
- Encoding `test` gives `ybty` with the key a=5 b=7
53+
- Decoding `ybty` gives `test` with the key a=5 b=7
54+
- Decoding `ybty` gives `lqul` with the wrong key a=11 b=7
55+
- Decoding `kqlfd jzvgy tpaet icdhm rtwly kqlon ubstx`
56+
- gives `thequickbrownfoxjumpsoverthelazydog` with the key a=19 b=13
57+
- Encoding `test` with the key a=18 b=13
58+
- gives `Error: a and m must be coprime.`
59+
- because a and m are not relatively prime
60+
61+
### Examples of finding a Modular Multiplicative Inverse (MMI)
62+
63+
- simple example:
64+
- `9 mod 26 = 9`
65+
- `9 * 3 mod 26 = 27 mod 26 = 1`
66+
- `3` is the MMI of `9 mod 26`
67+
- a more complicated example:
68+
- `15 mod 26 = 15`
69+
- `15 * 7 mod 26 = 105 mod 26 = 1`
70+
- `7` is the MMI of `15 mod 26`
71+
72+
## Exception messages
73+
74+
Sometimes it is necessary to raise an exception. When you do this, you should include a meaningful error message to
75+
indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. Not
76+
every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include
77+
a message.
78+
79+
To raise a message with an exception, just write it as an argument to the exception type. For example, instead of
80+
`raise Exception`, you should write:
81+
82+
```python
83+
raise Exception("Meaningful message indicating the source of the error")
84+
```
85+
86+
## Running the tests
87+
88+
To run the tests, run the appropriate command below ([why they are different](https://github.com/pytest-dev/pytest/issues/1629#issue-161422224)):
89+
90+
- Python 2.7: `py.test affine_cipher_test.py`
91+
- Python 3.4+: `pytest affine_cipher_test.py`
92+
93+
Alternatively, you can tell Python to run the pytest module (allowing the same command to be used regardless of Python version):
94+
`python -m pytest affine_cipher_test.py`
95+
96+
### Common `pytest` options
97+
98+
- `-v` : enable verbose output
99+
- `-x` : stop running tests on first failure
100+
- `--ff` : run failures from previous test before running other test cases
101+
102+
For other options, see `python -m pytest -h`
103+
104+
## Submitting Exercises
105+
106+
Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/affine-cipher` directory.
107+
108+
You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`.
109+
110+
For more detailed information about running tests, code style and linting,
111+
please see [Running the Tests](http://exercism.io/tracks/python/tests).
112+
113+
## Source
114+
115+
Wikipedia [http://en.wikipedia.org/wiki/Affine_cipher](http://en.wikipedia.org/wiki/Affine_cipher)
116+
117+
## Submitting Incomplete Solutions
118+
119+
It's possible to submit an incomplete solution so you can see how others have completed the exercise.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
def encode(plain_text, a, b):
2+
pass
3+
4+
5+
def decode(ciphered_text, a, b):
6+
pass
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import unittest
2+
3+
from affine_cipher import decode, encode
4+
5+
6+
# Tests adapted from `problem-specifications//canonical-data.json` @ v2.0.0
7+
8+
class AffineCipherTest(unittest.TestCase):
9+
def test_encode_yes(self):
10+
self.assertEqual(encode("yes", 5, 7), "xbt")
11+
12+
def test_encode_no(self):
13+
self.assertEqual(encode("no", 15, 18), "fu")
14+
15+
def test_encode_OMG(self):
16+
self.assertEqual(encode("OMG", 21, 3), "lvz")
17+
18+
def test_encode_O_M_G(self):
19+
self.assertEqual(encode("O M G", 25, 47), "hjp")
20+
21+
def test_encode_mindblowingly(self):
22+
self.assertEqual(encode("mindblowingly", 11, 15), "rzcwa gnxzc dgt")
23+
24+
def test_encode_numbers(self):
25+
self.assertEqual(encode("Testing,1 2 3, testing.", 3, 4),
26+
"jqgjc rw123 jqgjc rw")
27+
28+
def test_encode_deep_thought(self):
29+
self.assertEqual(encode("Truth is fiction.", 5, 17),
30+
"iynia fdqfb ifje")
31+
32+
def test_encode_all_the_letters(self):
33+
self.assertEqual(
34+
encode("The quick brown fox jumps over the lazy dog.", 17, 33),
35+
"swxtj npvyk lruol iejdc blaxk swxmh qzglf")
36+
37+
def test_encode_raises_meaningful_exception(self):
38+
with self.assertRaisesWithMessage(ValueError):
39+
encode("This is a test", 6, 17)
40+
41+
def test_decode_exercism(self):
42+
self.assertEqual(decode("tytgn fjr", 3, 7), "exercism")
43+
44+
def test_decode_sentence(self):
45+
self.assertEqual(
46+
decode("qdwju nqcro muwhn odqun oppmd aunwd o", 19, 16),
47+
"anobstacleisoftenasteppingstone")
48+
49+
def test_decode_numbers(self):
50+
self.assertEqual(decode("odpoz ub123 odpoz ub", 25, 7),
51+
"testing123testing")
52+
53+
def test_decode_all_the_letters(self):
54+
self.assertEqual(
55+
decode("swxtj npvyk lruol iejdc blaxk swxmh qzglf", 17, 33),
56+
"thequickbrownfoxjumpsoverthelazydog")
57+
58+
def test_decode_with_no_spaces(self):
59+
self.assertEqual(
60+
decode("swxtjnpvyklruoliejdcblaxkswxmhqzglf", 17, 33),
61+
"thequickbrownfoxjumpsoverthelazydog")
62+
63+
def test_decode_with_too_many_spaces(self):
64+
self.assertEqual(
65+
decode("vszzm cly yd cg qdp", 15, 16), "jollygreengiant")
66+
67+
def test_decode_raises_meaningful_exception(self):
68+
with self.assertRaisesWithMessage(ValueError):
69+
decode("Test", 13, 5)
70+
71+
# Utility functions
72+
def setUp(self):
73+
try:
74+
self.assertRaisesRegex
75+
except AttributeError:
76+
self.assertRaisesRegex = self.assertRaisesRegexp
77+
78+
def assertRaisesWithMessage(self, exception):
79+
return self.assertRaisesRegex(exception, r".+")
80+
81+
82+
if __name__ == '__main__':
83+
unittest.main()

exercises/affine-cipher/example.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
BLKSZ = 5
2+
ALPHSZ = 26
3+
4+
5+
def modInverse(a, ALPHSZ):
6+
a = a % ALPHSZ
7+
for x in range(1, ALPHSZ):
8+
if ((a * x) % ALPHSZ == 1):
9+
return x
10+
return 1
11+
12+
13+
def translate(text, a, b, mode):
14+
inv = modInverse(a, ALPHSZ)
15+
if inv == 1:
16+
raise ValueError("a and alphabet size must be coprime.")
17+
18+
chars = []
19+
for c in text:
20+
if c.isalnum():
21+
orig = ord(c.lower()) - 97
22+
if orig < 0:
23+
chars.append(c)
24+
continue
25+
if mode == 0:
26+
new = (a * orig + b) % ALPHSZ
27+
elif mode == 1:
28+
new = (inv * (orig - b)) % ALPHSZ
29+
chars.append(chr(new + 97))
30+
31+
return ''.join(chars)
32+
33+
34+
def encode(plain, a, b):
35+
cipher = translate(plain, a, b, 0)
36+
return " ".join([cipher[i:i + BLKSZ]
37+
for i in range(0, len(cipher), BLKSZ)])
38+
39+
40+
def decode(ciphered, a, b):
41+
return translate(ciphered, a, b, 1)

0 commit comments

Comments
 (0)