Add rail fence cipher (#516)

* Add rail fence cipher encoder & decoder

* Add functions to encode & decode strings using the rail fence cipher method
* Add unit tests covering empty strings, pair & odd number of characters in the input string, n=3 & n=4
* Add a README.md for the algorithm
* Update root README.md to link to the new algorithm

* Rename the CI workflow file.

Co-authored-by: Oleksii Trekhleb <trehleb@gmail.com>
This commit is contained in:
João Pedro Raskopf 2020-12-20 14:40:22 -03:00 committed by GitHub
parent c755110eb1
commit 9641940fd7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 262 additions and 0 deletions

View File

@ -143,6 +143,7 @@ a set of rules that precisely define a sequence of operations.
* `A` [Travelling Salesman Problem](src/algorithms/graph/travelling-salesman) - shortest possible route that visits each city and returns to the origin city
* **Cryptography**
* `B` [Polynomial Hash](src/algorithms/cryptography/polynomial-hash) - rolling hash function based on polynomial
* `B` [Rail Fence Cypher](src/algorithms/cryptography/rail-fence-cipher) - a transposition cipher algorithm for encoding messages
* `B` [Caesar Cipher](src/algorithms/cryptography/caesar-cipher) - simple substitution cipher
* `B` [Hill Cipher](src/algorithms/cryptography/hill-cipher) - substitution cipher based on linear algebra
* **Machine Learning**

View File

@ -0,0 +1,25 @@
# Rail fence Cipher
This is a [transposition cipher](https://en.wikipedia.org/wiki/Transposition_cipher) in which the message is split accross a set of rails on a fence for encoding. The fence is populated with the message's characters, starting at the top left and adding a character on each position, traversing them diagonally to the bottom. Upon reaching the last rail, the direction should then turn diagonal and upwards up to the very first rail in a zig-zag motion. Rinse and repeat until the message is fully disposed across the fence. The encoded message is the result of concatenating the text in each rail, from top to bottom.
From [wikipedia](https://en.wikipedia.org/wiki/Rail_fence_cipher), this is what the message `WE ARE DISCOVERED. FLEE AT ONCE` looks like on a 3-rail fence:
```
W . . . E . . . C . . . R . . . L . . . T . . . E
. E . R . D . S . O . E . E . F . E . A . O . C .
. . A . . . I . . . V . . . D . . . E . . . N . .
-------------------------------------------------
ECRLTEERDSOEEFEAOCAIVDEN
```
The message can then be decoded by re-creating the encode fence, with the same traversal pattern, except characters should only be added on one rail at a time. To ilustrate that, a dash can be added on the rails that are not supposed to be poupated yet. This is what the fence would look like after populating the first rail, the dashes represent positions that were visited but not populated.
```
W . . . E . . . C . . . R . . . L . . . T . . . E
. - . - . - . - . - . - . - . - . - . - . - . - .
. . - . . . - . . . - . . . - . . . - . . . - . .
```
It's time to start populating the next rail once the number of visited fence positions is equal to the number of characters in the message.
[Learn more](https://crypto.interactive-maths.com/rail-fence-cipher.html)

View File

@ -0,0 +1,26 @@
import encodeRailFenceCipher from '../encodeRailFence';
import decodeRailFenceCipher from '../decodeRailFence';
describe('rail fence cipher', () => {
it('encodes a string correctly for base=3', () => {
expect(encodeRailFenceCipher('', 3)).toBe('');
expect(encodeRailFenceCipher('WEAREDISCOVEREDFLEEATONCE', 3)).toBe('WECRLTEERDSOEEFEAOCAIVDEN');
expect(encodeRailFenceCipher('Hello, World!', 3)).toBe('Hoo!el,Wrdl l');
});
it('decodes a string correctly for base=3', () => {
expect(decodeRailFenceCipher('', 3)).toBe('');
expect(decodeRailFenceCipher('WECRLTEERDSOEEFEAOCAIVDEN', 3)).toBe('WEAREDISCOVEREDFLEEATONCE');
expect(decodeRailFenceCipher('Hoo!el,Wrdl l', 3)).toBe('Hello, World!');
});
it('encodes a string correctly for base=4', () => {
expect(encodeRailFenceCipher('', 4)).toBe('');
expect(encodeRailFenceCipher('THEYAREATTACKINGFROMTHENORTH', 4)).toBe('TEKOOHRACIRMNREATANFTETYTGHH');
});
it('decodes a string correctly for base=4', () => {
expect(decodeRailFenceCipher('', 4)).toBe('');
expect(decodeRailFenceCipher('TEKOOHRACIRMNREATANFTETYTGHH', 4)).toBe('THEYAREATTACKINGFROMTHENORTH');
});
});

View File

@ -0,0 +1,108 @@
import {
addChar,
buildFence,
DIRECTIONS,
getNextDirection,
} from './railFenceCipher';
/**
* @param {object} params
* @param {number} params.railCount
* @param {number} params.strLen
* @param {Array} params.string
* @param {Array} params.fence
* @param {number} params.targetRail
* @param {number} params.direction
* @param {Array} params.coords
*
* @returns {Array}
*/
const fillDecodeFence = ({
railCount, strLen, string, fence, targetRail, direction, coords,
}) => {
if (string.length === 0) return fence;
const [currentRail, currentColumn] = coords;
const shouldGoNextRail = currentColumn === strLen - 1;
const nextDirection = shouldGoNextRail
? DIRECTIONS.DOWN
: getNextDirection({ railCount, currentRail, direction });
const nextRail = shouldGoNextRail ? targetRail + 1 : targetRail;
const nextCoords = [
shouldGoNextRail ? 0 : currentRail + nextDirection,
shouldGoNextRail ? 0 : currentColumn + 1,
];
const shouldAddChar = currentRail === targetRail;
const [currentChar, ...remainderString] = string;
const nextString = shouldAddChar ? remainderString : string;
const nextFence = shouldAddChar ? fence.map(addChar(currentRail, currentChar)) : fence;
return fillDecodeFence({
railCount,
strLen,
string: nextString,
fence: nextFence,
targetRail: nextRail,
direction: nextDirection,
coords: nextCoords,
});
};
/**
* @param {object} params
* @param {number} params.railCount
* @param {number} params.strLen
* @param {Array} params.fence
* @param {number} params.currentRail
* @param {number} params.direction
* @param {Array} params.code
*
* @returns {string}
*/
const decodeFence = ({
railCount, strLen, fence, currentRail, direction, code,
}) => {
if (code.length === strLen) return code.join('');
const [currentChar, ...nextRail] = fence[currentRail];
const nextDirection = getNextDirection({ railCount, currentRail, direction });
return decodeFence({
railCount,
strLen,
currentRail: currentRail + nextDirection,
direction: nextDirection,
code: [...code, currentChar],
fence: fence.map((rail, idx) => (idx === currentRail ? nextRail : rail)),
});
};
/**
* @param {string} string
* @param {number} railCount
*
* @returns {string}
*/
export default function decodeRailFenceCipher(string, railCount) {
const strLen = string.length;
const emptyFence = buildFence(railCount);
const filledFence = fillDecodeFence({
railCount,
strLen,
string: string.split(''),
fence: emptyFence,
targetRail: 0,
direction: DIRECTIONS.DOWN,
coords: [0, 0],
});
return decodeFence({
railCount,
strLen,
fence: filledFence,
currentRail: 0,
direction: DIRECTIONS.DOWN,
code: [],
});
}

View File

@ -0,0 +1,52 @@
import {
addChar,
buildFence,
DIRECTIONS,
getNextDirection,
} from './railFenceCipher';
/**
* @param {object} params
* @param {number} params.railCount
* @param {number} params.currentRail
* @param {number} params.direction
* @param {Array} params.string
*
* @returns {Array}
*/
const fillEncodeFence = ({
railCount, fence, currentRail, direction, string,
}) => {
if (string.length === 0) return fence;
const [letter, ...nextString] = string;
const nextDirection = getNextDirection({ railCount, currentRail, direction });
return fillEncodeFence({
railCount,
fence: fence.map(addChar(currentRail, letter)),
currentRail: currentRail + nextDirection,
direction: nextDirection,
string: nextString,
});
};
/**
* @param {string} string
* @param {number} railCount
*
* @returns {string}
*/
export default function encodeRailFenceCipher(string, railCount) {
const fence = buildFence(railCount);
const filledFence = fillEncodeFence({
railCount,
fence,
currentRail: 0,
direction: DIRECTIONS.DOWN,
string: string.split(''),
});
return filledFence.flat().join('');
}

View File

@ -0,0 +1,50 @@
/**
* @constant DIRECTIONS
* @type {object}
* @property {number} UP
* @property {number} DOWN
*/
export const DIRECTIONS = { UP: -1, DOWN: 1 };
/**
* @param {number} rows
*
* @returns {Array}
*/
export const buildFence = (rows) => Array(rows)
.fill()
.map(() => []);
/**
* @param {object} params
* @param {number} params.railCount
* @param {number} params.currentRail
* @param {number} params.direction
*
* @returns {number}
*/
export const getNextDirection = ({ railCount, currentRail, direction }) => {
switch (currentRail) {
case 0: return DIRECTIONS.DOWN;
case railCount - 1: return DIRECTIONS.UP;
default: return direction;
}
};
/**
* Given a rail, adds a char to it
* if it matches a targetIndex.
* @callback charAdder
* @param {number} rail
* @param {currentRail} number
*/
/**
* @param {number} targetIndex
* @param {string} letter
*
* @returns {charAdder}
*/
export const addChar = (targetIndex, letter) => (rail, currentRail) => {
return (currentRail === targetIndex ? [...rail, letter] : rail);
};