Skip to content

Commit e7bda13

Browse files
authored
✨ Add new exercise : Affine Cipher (#887)
* ✨ Add new exercise : Affine Cipher
1 parent 6cc0907 commit e7bda13

File tree

9 files changed

+395
-0
lines changed

9 files changed

+395
-0
lines changed

config.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1141,6 +1141,19 @@
11411141
"math"
11421142
]
11431143
},
1144+
{
1145+
"slug": "affine-cipher",
1146+
"uuid": "37f26dda-6d5b-4b8a-a548-20758c5b6178",
1147+
"core": false,
1148+
"difficulty": 4,
1149+
"unlocked_by": "simple-cipher",
1150+
"topics": [
1151+
"algorithms",
1152+
"arrays",
1153+
"filtering",
1154+
"math"
1155+
]
1156+
},
11441157
{
11451158
"slug": "atbash-cipher",
11461159
"uuid": "a70e6027-eebe-43a1-84a6-763faa736169",

exercises/affine-cipher/.eslintrc

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"root": true,
3+
"parser": "babel-eslint",
4+
"parserOptions": {
5+
"ecmaVersion": 7,
6+
"sourceType": "module"
7+
},
8+
"globals": {
9+
"BigInt": true
10+
},
11+
"env": {
12+
"es6": true,
13+
"node": true,
14+
"jest": true
15+
},
16+
"extends": [
17+
"eslint:recommended",
18+
"plugin:import/errors",
19+
"plugin:import/warnings"
20+
],
21+
"rules": {
22+
"linebreak-style": "off",
23+
24+
"import/extensions": "off",
25+
"import/no-default-export": "off",
26+
"import/no-unresolved": "off",
27+
"import/prefer-default-export": "off"
28+
}
29+
}

exercises/affine-cipher/.npmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
audit=false

exercises/affine-cipher/README.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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+
## Setup
73+
74+
Go through the setup instructions for Javascript to install the necessary
75+
dependencies:
76+
77+
[https://exercism.io/tracks/javascript/installation](https://exercism.io/tracks/javascript/installation)
78+
79+
## Requirements
80+
81+
Please `cd` into exercise directory before running all below commands.
82+
83+
Install assignment dependencies:
84+
85+
```bash
86+
$ npm install
87+
```
88+
89+
## Making the test suite pass
90+
91+
Execute the tests with:
92+
93+
```bash
94+
$ npm test
95+
```
96+
97+
In the test suites all tests but the first have been skipped.
98+
99+
Once you get a test passing, you can enable the next one by changing `xtest` to
100+
`test`.
101+
102+
103+
## Submitting Solutions
104+
105+
Once you have a solution ready, you can submit it using:
106+
107+
```bash
108+
exercism submit allergies.js
109+
```
110+
111+
## Submitting Incomplete Solutions
112+
113+
It's possible to submit an incomplete solution so you can see how others have
114+
completed the exercise.
115+
116+
## Exercise Source Credits
117+
118+
Jumpstart Lab Warm-up [http://jumpstartlab.com](http://jumpstartlab.com)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const encode = (phrase, key) => {
2+
throw new Error("Remove this statement and implement this function");
3+
};
4+
5+
export const decode = (phrase, key) => {
6+
throw new Error("Remove this statement and implement this function");
7+
};
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { encode, decode } from './affine-cipher';
2+
3+
describe('Affine cipher', () => {
4+
describe('encode', () => {
5+
test('encode yes', () => {
6+
expect(encode('yes', { a: 5, b: 7 })).toBe('xbt');
7+
});
8+
9+
xtest('encode no', () => {
10+
expect(encode('no', { a: 15, b: 18 })).toBe('fu');
11+
});
12+
13+
xtest('encode OMG', () => {
14+
expect(encode('OMG', { a: 21, b: 3 })).toBe('lvz');
15+
});
16+
17+
xtest('encode O M G', () => {
18+
expect(encode('O M G', { a: 25, b: 47 })).toBe('hjp');
19+
});
20+
21+
xtest('encode mindblowingly', () => {
22+
expect(encode('mindblowingly', { a: 11, b: 15 })).toBe('rzcwa gnxzc dgt');
23+
});
24+
25+
xtest('encode numbers', () => {
26+
expect(encode('Testing,1 2 3, testing.', { a: 3, b: 4 })).toBe('jqgjc rw123 jqgjc rw');
27+
});
28+
29+
xtest('encode deep thought', () => {
30+
expect(encode('Truth is fiction.', { a: 5, b: 17 })).toBe('iynia fdqfb ifje');
31+
});
32+
33+
xtest('encode all the letters', () => {
34+
expect(encode('The quick brown fox jumps over the lazy dog.', { a: 17, b: 33 })).toBe('swxtj npvyk lruol iejdc blaxk swxmh qzglf');
35+
});
36+
37+
xtest('encode with a not coprime to m', () => {
38+
expect(() => {
39+
encode('This is a test.', { a: 6, b: 17 });
40+
}).toThrowError('a and m must be coprime.')
41+
});
42+
});
43+
describe('decode', () => {
44+
test('decode exercism', () => {
45+
expect(decode('tytgn fjr', { a: 3, b: 7 })).toBe('exercism');
46+
});
47+
48+
xtest('decode a sentence', () => {
49+
expect(decode('qdwju nqcro muwhn odqun oppmd aunwd o', { a: 19, b: 16 })).toBe('anobstacleisoftenasteppingstone');
50+
});
51+
52+
xtest('decode numbers', () => {
53+
expect(decode('odpoz ub123 odpoz ub', { a: 25, b: 7 })).toBe('testing123testing');
54+
});
55+
56+
xtest('decode all the letters', () => {
57+
expect(decode('swxtj npvyk lruol iejdc blaxk swxmh qzglf', { a: 17, b: 33 })).toBe('thequickbrownfoxjumpsoverthelazydog');
58+
});
59+
60+
xtest('decode with no spaces in input', () => {
61+
expect(decode('swxtjnpvyklruoliejdcblaxkswxmhqzglf', { a: 17, b: 33 })).toBe('thequickbrownfoxjumpsoverthelazydog');
62+
});
63+
64+
xtest('decode with too many spaces', () => {
65+
expect(decode('vszzm cly yd cg qdp', { a: 15, b: 16 })).toBe('jollygreengiant');
66+
});
67+
68+
xtest('decode with a not coprime to m', () => {
69+
expect(() => {
70+
decode('Test', { a: 13, b: 5 });
71+
}).toThrowError('a and m must be coprime.');
72+
});
73+
});
74+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports = {
2+
presets: [
3+
[
4+
"@babel/preset-env",
5+
{
6+
targets: {
7+
node: "current",
8+
},
9+
useBuiltIns: "entry",
10+
corejs: 3,
11+
},
12+
],
13+
],
14+
plugins: ["@babel/plugin-syntax-bigint"],
15+
};

exercises/affine-cipher/example.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
const ALPHABET = 'abcdefghijklmnopqrstuvwxyz';
2+
const ALPHABET_LENGTH = ALPHABET.length;
3+
4+
const areCoprimes = (a, b) => {
5+
for (let i = Math.min(a, b); i > 1; i--) {
6+
if (a % i == 0 && b % i == 0) {
7+
return false;
8+
}
9+
}
10+
11+
return true;
12+
}
13+
14+
const checkCoprime = (a, b) => {
15+
if (!areCoprimes(a, b)) {
16+
throw new Error('a and m must be coprime.');
17+
}
18+
}
19+
20+
const isNumber = (candidate) => {
21+
return !isNaN(Number(candidate));
22+
}
23+
24+
const findMMI = (a) => {
25+
let i = 1;
26+
27+
// eslint-disable-next-line no-constant-condition
28+
while (true) {
29+
i++;
30+
31+
if ((a * i - 1) % ALPHABET_LENGTH === 0) {
32+
return i;
33+
}
34+
}
35+
}
36+
37+
const positiveModulo = (a, b) => {
38+
return ((a % b) + b) % b;
39+
}
40+
41+
const groupBy = (elements, groupLength) => {
42+
const result = [[]];
43+
let i = 0;
44+
45+
elements.forEach(el => {
46+
if (result[i] && result[i].length < groupLength ) {
47+
result[i].push(el);
48+
} else {
49+
i++;
50+
result[i] = [el];
51+
}
52+
});
53+
54+
return result;
55+
}
56+
57+
export const encode = (phrase, { a, b }) => {
58+
checkCoprime(a, ALPHABET_LENGTH);
59+
60+
let encodedText = '';
61+
62+
phrase
63+
.toLowerCase()
64+
.split('')
65+
.filter(char => char !== ' ')
66+
.forEach(char => {
67+
if (ALPHABET.includes(char)) {
68+
const x = ALPHABET.indexOf(char);
69+
const encodedIndex = (a * x + b) % ALPHABET_LENGTH;
70+
71+
encodedText += ALPHABET[encodedIndex];
72+
} else if (isNumber(char)) {
73+
encodedText += char;
74+
}
75+
});
76+
77+
return groupBy(encodedText.split(''), 5)
78+
.map(group => group.join(''))
79+
.join(' ');
80+
};
81+
82+
export const decode = (phrase, { a, b }) => {
83+
checkCoprime(a, ALPHABET_LENGTH);
84+
85+
const mmi = findMMI(a);
86+
87+
return phrase
88+
.split('')
89+
.filter(char => char !== ' ')
90+
.map(char => {
91+
if (isNumber(char)) {
92+
return char;
93+
}
94+
95+
const y = ALPHABET.indexOf(char);
96+
const decodedIndex = positiveModulo(mmi * (y - b), ALPHABET_LENGTH);
97+
98+
return ALPHABET[decodedIndex];
99+
})
100+
.join('');
101+
}

0 commit comments

Comments
 (0)