Skip to content

✨ Add new exercise : Affine Cipher #887

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,19 @@
"math"
]
},
{
"slug": "affine-cipher",
"uuid": "37f26dda-6d5b-4b8a-a548-20758c5b6178",
"core": false,
"difficulty": 4,
"unlocked_by": "simple-cipher",
"topics": [
"algorithms",
"arrays",
"filtering",
"math"
]
},
{
"slug": "atbash-cipher",
"uuid": "a70e6027-eebe-43a1-84a6-763faa736169",
Expand Down
29 changes: 29 additions & 0 deletions exercises/affine-cipher/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"root": true,
"parser": "babel-eslint",
"parserOptions": {
"ecmaVersion": 7,
"sourceType": "module"
},
"globals": {
"BigInt": true
},
"env": {
"es6": true,
"node": true,
"jest": true
},
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings"
],
"rules": {
"linebreak-style": "off",

"import/extensions": "off",
"import/no-default-export": "off",
"import/no-unresolved": "off",
"import/prefer-default-export": "off"
}
}
1 change: 1 addition & 0 deletions exercises/affine-cipher/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
audit=false
118 changes: 118 additions & 0 deletions exercises/affine-cipher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
## Affine Cipher

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.

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

the decryption function is:

`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.

To find the MMI of `a`:

`an mod m = 1`
- where `n` is the modular multiplicative inverse of `a mod m`

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)

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 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.

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.

## 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`

## Setup

Go through the setup instructions for Javascript to install the necessary
dependencies:

[https://exercism.io/tracks/javascript/installation](https://exercism.io/tracks/javascript/installation)

## Requirements

Please `cd` into exercise directory before running all below commands.

Install assignment dependencies:

```bash
$ npm install
```

## Making the test suite pass

Execute the tests with:

```bash
$ npm test
```

In the test suites all tests but the first have been skipped.

Once you get a test passing, you can enable the next one by changing `xtest` to
`test`.


## Submitting Solutions

Once you have a solution ready, you can submit it using:

```bash
exercism submit allergies.js
```

## Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have
completed the exercise.

## Exercise Source Credits

Jumpstart Lab Warm-up [http://jumpstartlab.com](http://jumpstartlab.com)
7 changes: 7 additions & 0 deletions exercises/affine-cipher/affine-cipher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const encode = (phrase, key) => {
throw new Error("Remove this statement and implement this function");
};

export const decode = (phrase, key) => {
throw new Error("Remove this statement and implement this function");
};
74 changes: 74 additions & 0 deletions exercises/affine-cipher/affine-cipher.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { encode, decode } from './affine-cipher';

describe('Affine cipher', () => {
describe('encode', () => {
test('encode yes', () => {
expect(encode('yes', { a: 5, b: 7 })).toBe('xbt');
});

xtest('encode no', () => {
expect(encode('no', { a: 15, b: 18 })).toBe('fu');
});

xtest('encode OMG', () => {
expect(encode('OMG', { a: 21, b: 3 })).toBe('lvz');
});

xtest('encode O M G', () => {
expect(encode('O M G', { a: 25, b: 47 })).toBe('hjp');
});

xtest('encode mindblowingly', () => {
expect(encode('mindblowingly', { a: 11, b: 15 })).toBe('rzcwa gnxzc dgt');
});

xtest('encode numbers', () => {
expect(encode('Testing,1 2 3, testing.', { a: 3, b: 4 })).toBe('jqgjc rw123 jqgjc rw');
});

xtest('encode deep thought', () => {
expect(encode('Truth is fiction.', { a: 5, b: 17 })).toBe('iynia fdqfb ifje');
});

xtest('encode all the letters', () => {
expect(encode('The quick brown fox jumps over the lazy dog.', { a: 17, b: 33 })).toBe('swxtj npvyk lruol iejdc blaxk swxmh qzglf');
});

xtest('encode with a not coprime to m', () => {
expect(() => {
encode('This is a test.', { a: 6, b: 17 });
}).toThrowError('a and m must be coprime.')
});
});
describe('decode', () => {
test('decode exercism', () => {
expect(decode('tytgn fjr', { a: 3, b: 7 })).toBe('exercism');
});

xtest('decode a sentence', () => {
expect(decode('qdwju nqcro muwhn odqun oppmd aunwd o', { a: 19, b: 16 })).toBe('anobstacleisoftenasteppingstone');
});

xtest('decode numbers', () => {
expect(decode('odpoz ub123 odpoz ub', { a: 25, b: 7 })).toBe('testing123testing');
});

xtest('decode all the letters', () => {
expect(decode('swxtj npvyk lruol iejdc blaxk swxmh qzglf', { a: 17, b: 33 })).toBe('thequickbrownfoxjumpsoverthelazydog');
});

xtest('decode with no spaces in input', () => {
expect(decode('swxtjnpvyklruoliejdcblaxkswxmhqzglf', { a: 17, b: 33 })).toBe('thequickbrownfoxjumpsoverthelazydog');
});

xtest('decode with too many spaces', () => {
expect(decode('vszzm cly yd cg qdp', { a: 15, b: 16 })).toBe('jollygreengiant');
});

xtest('decode with a not coprime to m', () => {
expect(() => {
decode('Test', { a: 13, b: 5 });
}).toThrowError('a and m must be coprime.');
});
});
});
15 changes: 15 additions & 0 deletions exercises/affine-cipher/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current",
},
useBuiltIns: "entry",
corejs: 3,
},
],
],
plugins: ["@babel/plugin-syntax-bigint"],
};
101 changes: 101 additions & 0 deletions exercises/affine-cipher/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const ALPHABET = 'abcdefghijklmnopqrstuvwxyz';
const ALPHABET_LENGTH = ALPHABET.length;

const areCoprimes = (a, b) => {
for (let i = Math.min(a, b); i > 1; i--) {
if (a % i == 0 && b % i == 0) {
return false;
}
}

return true;
}

const checkCoprime = (a, b) => {
if (!areCoprimes(a, b)) {
throw new Error('a and m must be coprime.');
}
}

const isNumber = (candidate) => {
return !isNaN(Number(candidate));
}

const findMMI = (a) => {
let i = 1;

// eslint-disable-next-line no-constant-condition
while (true) {
i++;

if ((a * i - 1) % ALPHABET_LENGTH === 0) {
return i;
}
}
}

const positiveModulo = (a, b) => {
return ((a % b) + b) % b;
}

const groupBy = (elements, groupLength) => {
const result = [[]];
let i = 0;

elements.forEach(el => {
if (result[i] && result[i].length < groupLength ) {
result[i].push(el);
} else {
i++;
result[i] = [el];
}
});

return result;
}

export const encode = (phrase, { a, b }) => {
checkCoprime(a, ALPHABET_LENGTH);

let encodedText = '';

phrase
.toLowerCase()
.split('')
.filter(char => char !== ' ')
.forEach(char => {
if (ALPHABET.includes(char)) {
const x = ALPHABET.indexOf(char);
const encodedIndex = (a * x + b) % ALPHABET_LENGTH;

encodedText += ALPHABET[encodedIndex];
} else if (isNumber(char)) {
encodedText += char;
}
});

return groupBy(encodedText.split(''), 5)
.map(group => group.join(''))
.join(' ');
};

export const decode = (phrase, { a, b }) => {
checkCoprime(a, ALPHABET_LENGTH);

const mmi = findMMI(a);

return phrase
.split('')
.filter(char => char !== ' ')
.map(char => {
if (isNumber(char)) {
return char;
}

const y = ALPHABET.indexOf(char);
const decodedIndex = positiveModulo(mmi * (y - b), ALPHABET_LENGTH);

return ALPHABET[decodedIndex];
})
.join('');
}
Loading