-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fixes #4
- Loading branch information
Showing
6 changed files
with
299 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
# Multi-Tap | ||
|
||
> This cipher is named by the *Multi-Tap* text entry system used in older mobile phone | ||
keypads. Characters will be converted to the string of keystrokes needed to write the | ||
particular character on such a phone. | ||
|
||
## Cipher behavior information | ||
|
||
* Case sensitive? ❌ | ||
* Deterministic? ✓❌ (Only in exponent form **or** with spacing) | ||
* Alphabet: `ABCDEFGHIJKLMNOPQRSTUVWXYZ ` (can be extended with special chars if needed) | ||
* Characters not in alphabet will be: **omitted** or **throwing an error (default)** | ||
|
||
## Default options object | ||
|
||
The options are the same for both methods, `encode` and `decode` | ||
|
||
``` | ||
const options = { | ||
customMapping: { // Setup additional character mappings if needed (for example special chars) | ||
0: ' ' // The letter position dertermines the number of strokes needed. | ||
}, // {0: ' !$'} would lead to: Space = 0^1, exclamation mark = 0^2, dollar sign = 0^3 | ||
exponentForm: false, // Output results in exponent form or decode exponent form input | ||
withSpacing: true, // Add spacing between each character number string. Also set to true if ciphertext has spaces between strings | ||
failOnUnknownCharacter: true, // Should an error be thrown when a character is not included in the alphabet | ||
} | ||
``` | ||
|
||
|
||
## Usage | ||
|
||
### Encoding | ||
|
||
#### Default | ||
|
||
``` | ||
import { multiTap } from 'cipher-collection' | ||
console.log(multiTap.encode('Hello World')) // 44 33 555 555 666 0 9 666 777 555 3 | ||
``` | ||
|
||
#### Without spacing | ||
|
||
**ATTENTION:** Without spacing, the created ciphertext can only be decoded ambiguously. | ||
We suggest to use either the exponent form or spacing. | ||
|
||
``` | ||
import { multiTap } from 'cipher-collection' | ||
console.log(multiTap.encode('Hello World', { withSpacing: false })) // 4433555555666096667775553 | ||
``` | ||
|
||
|
||
#### In exponent form | ||
|
||
``` | ||
import { multiTap } from 'cipher-collection' | ||
console.log(multiTap.encode('Hello World'), { exponentForm: true }) // 4^2 3^2 5^3 5^3 6^3 0^1 9^1 6^3 7^3 5^3 3^1 | ||
// Without spacing (can also be decoded without problems) | ||
console.log(multiTap.encode('Hello World'), { exponentForm: true, withSpacing: false }) // 4^23^25^35^36^30^19^16^37^35^33^1 | ||
``` | ||
|
||
|
||
#### With custom mapping | ||
|
||
|
||
**ATTENTION:** the mapping per character can only include a maximum | ||
of 4 (normal mode with spacing) or 9 charaters. For example: `0: ' !$/'` or `0: ' !$/()=?_'` | ||
|
||
|
||
``` | ||
import { multiTap } from 'cipher-collection' | ||
const customMappingOptions = { customMapping: { 0: ' !$' } } | ||
console.log(multiTap.encode('Give $$ to me!', customMappingOptions)) // 4 444 888 33 0 000 000 0 8 666 0 6 33 00 | ||
``` | ||
|
||
### Decoding | ||
|
||
#### Default | ||
|
||
``` | ||
import { multiTap } from 'cipher-collection' | ||
console.log(multiTap.decode('44 33 555 555 666 0 9 666 777 555 3')) // HELLO WORLD | ||
``` | ||
|
||
#### Without spacing | ||
|
||
**ATTENTION:** Without spacing, the created ciphertext can only be decoded ambiguously. | ||
We suggest to use either the exponent form or spacing. | ||
|
||
``` | ||
import { multiTap } from 'cipher-collection' | ||
const noSpacingOptions = { withSpacing: false } | ||
console.log(multiTap.decode('68855584440827', noSpacingOptions)) //MULTI TAP (this works well) | ||
console.log(multiTap.decode('44444', noSpacingOptions)) //IH (Wrong decoding, was "Hi" before) | ||
``` | ||
|
||
|
||
#### In exponent form | ||
|
||
``` | ||
import { multiTap } from 'cipher-collection' | ||
console.log(multiTap.deocde('4^2 3^2 5^3 5^3 6^3 0^1 9^1 6^3 7^3 5^3 3^1'), { exponentForm: true }) // HELLO WORLD | ||
// Without spacing | ||
console.log(multiTap.deocde('4^23^25^35^36^30^19^16^37^35^33^1'), { exponentForm: true, withSpacing: false }) // HELLO WORLD | ||
``` | ||
|
||
|
||
#### With custom mapping | ||
|
||
|
||
**ATTENTION:** the mapping per character can only include a maximum | ||
of 4 (normal mode with spacing) or 9 charaters. For example: `0: ' !$/'` or `0: ' !$/()=?_'` | ||
|
||
``` | ||
import { multiTap } from 'cipher-collection' | ||
const customMappingOptions = { customMapping: { 0: ' !$' } } | ||
console.log(multiTap.deocde('4 444 888 33 0 000 000 0 8 666 0 6 33 00', customMappingOptions)) // Give $$ to me! | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
export const encode = (input, options = {}) => { | ||
options = { ...DEFAULT_OPTIONS, ...options } | ||
return [...input.toUpperCase()] | ||
.map(c => { | ||
const decodedCharacter = Object.entries(alphabetWithSpaceKey(options.customMapping)).find(([k, v]) => v.includes(c)) | ||
if (decodedCharacter) { | ||
const amount = decodedCharacter[1].indexOf(c) + 1 | ||
return (options.exponentForm ? `${decodedCharacter[0]}^${amount}` : `${decodedCharacter[0]}`.repeat(amount)) | ||
} | ||
if (options.failOnUnknownCharacter) { | ||
throw Error(`Unencodable character ${c}`) | ||
} | ||
return '' | ||
}) | ||
.filter(c => c.length) | ||
.join(options.withSpacing ? ' ' : '') | ||
} | ||
|
||
export const decode = (input, options = {}) => { | ||
options = { ...DEFAULT_OPTIONS, ...options } | ||
const alphabet = alphabetWithSpaceKey(options.customMapping) | ||
|
||
const invalidInputRegex = /[^\d^*# ]/g | ||
|
||
// Validate input | ||
if (input.match(invalidInputRegex)) { | ||
if (options.failOnUnknownCharacter) { | ||
throw Error(`Undecodable characters`) | ||
} else { | ||
input.replace(invalidInputRegex) | ||
} | ||
} | ||
|
||
if (!input.length) { | ||
return '' | ||
} | ||
|
||
const capturedInput = options.exponentForm ? input.match(/\d\^\d ?/g) : input.match(/(([79])\2{0,4}|([234568])\3{0,2}|([01*#])\4{0,2}) ?/g) | ||
return capturedInput.map(expr => { | ||
expr = expr.replace(/ /g, '') | ||
return options.exponentForm ? alphabet[expr[0]][expr[2] - 1] : alphabet[expr[0]][expr.length - 1] | ||
}).join('') | ||
} | ||
|
||
const alphabetWithSpaceKey = customMapping => typeof customMapping === 'object' ? { ...ALPHABET, ...customMapping } : ALPHABET | ||
|
||
const DEFAULT_OPTIONS = { | ||
customMapping: { | ||
0: ' ' | ||
}, | ||
exponentForm: false, | ||
withSpacing: true, | ||
failOnUnknownCharacter: true | ||
} | ||
|
||
const ALPHABET = { | ||
2: 'ABC', | ||
3: 'DEF', | ||
4: 'GHI', | ||
5: 'JKL', | ||
6: 'MNO', | ||
7: 'PQRS', | ||
8: 'TUV', | ||
9: 'WXYZ' | ||
} | ||
export default { | ||
decode, | ||
encode | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import multiTap from 'multiTap' | ||
|
||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ' | ||
const encodedAlphabet = '2 22 222 3 33 333 4 44 444 5 55 555 6 66 666 7 77 777 7777 8 88 888 9 99 999 9999 0' | ||
const encodedAlphabetAsExponents = '2^1 2^2 2^3 3^1 3^2 3^3 4^1 4^2 4^3 5^1 5^2 5^3 6^1 6^2 6^3 7^1 7^2 7^3 7^4 8^1 8^2 8^3 9^1 9^2 9^3 9^4 0^1' | ||
|
||
const withoutSpacingOptions = { withSpacing: false } | ||
const customMappingOptions = { customMapping: { 0: ' !$' } } | ||
const exponentOptions = { exponentForm: true } | ||
const exponentWithoutSpacingOptions = { exponentForm: true, withSpacing: false } | ||
const silentFailOptions = { failOnUnknownCharacter: false } | ||
|
||
describe('encoding', () => { | ||
test('default', () => { | ||
expect(multiTap.encode(alphabet)).toBe(encodedAlphabet) | ||
expect(multiTap.encode(alphabet.toLowerCase())).toBe(encodedAlphabet) | ||
}) | ||
|
||
test('empty', () => { | ||
expect(multiTap.encode('')).toBe('') | ||
}) | ||
|
||
test('without spacing', () => { | ||
expect(multiTap.encode(alphabet, withoutSpacingOptions)).toBe(encodedAlphabet.replace(/ /g, '')) | ||
expect(multiTap.encode(alphabet.toLowerCase(), withoutSpacingOptions)).toBe(encodedAlphabet.replace(/ /g, '')) | ||
}) | ||
|
||
test('with custom mapping', () => { | ||
expect(multiTap.encode(alphabet, customMappingOptions)).toBe(encodedAlphabet) | ||
expect(multiTap.encode(alphabet.toLowerCase(), customMappingOptions)).toBe(encodedAlphabet) | ||
expect(multiTap.encode('Give $$$!', customMappingOptions)).toBe('4 444 888 33 0 000 000 000 00') | ||
}) | ||
|
||
test('with empty custom mapping', () => { | ||
expect(multiTap.encode(alphabet.slice(0, -1), { customMapping: false })).toBe(encodedAlphabet.slice(0, -2)) | ||
}) | ||
|
||
test('in exponent form', () => { | ||
expect(multiTap.encode(alphabet, exponentOptions)).toBe(encodedAlphabetAsExponents) | ||
expect(multiTap.encode(alphabet.toLowerCase(), exponentOptions)).toBe(encodedAlphabetAsExponents) | ||
}) | ||
|
||
test('in exponent form and without spacing', () => { | ||
expect(multiTap.encode(alphabet, exponentWithoutSpacingOptions)).toBe(encodedAlphabetAsExponents.replace(/ /g, '')) | ||
expect(multiTap.encode(alphabet.toLowerCase(), exponentWithoutSpacingOptions)).toBe(encodedAlphabetAsExponents.replace(/ /g, '')) | ||
}) | ||
|
||
test('with invalid characters', () => { | ||
expect(() => { multiTap.encode('$') }).toThrowError('Unencodable character $') | ||
}) | ||
|
||
test('with invalid characters and silent fail', () => { | ||
expect(multiTap.encode('$A', silentFailOptions)).toBe('2') | ||
}) | ||
}) | ||
|
||
describe('decoding', () => { | ||
test('default', () => { | ||
expect(multiTap.decode(encodedAlphabet)).toBe(alphabet) | ||
}) | ||
|
||
test('empty', () => { | ||
expect(multiTap.decode('')).toBe('') | ||
}) | ||
|
||
test('without spacing', () => { | ||
// No correct decoding without spacing or exponent form possible. :( | ||
expect(multiTap.decode(encodedAlphabet.replace(/ /g, ''), withoutSpacingOptions)).toBe('CCFFIILLOOVV ') | ||
}) | ||
|
||
test('in exponent form', () => { | ||
expect(multiTap.decode(encodedAlphabetAsExponents, exponentOptions)).toBe(alphabet) | ||
}) | ||
|
||
test('in exponent form without spacing', () => { | ||
expect(multiTap.decode(encodedAlphabetAsExponents.replace(/ /g, ''), exponentWithoutSpacingOptions)).toBe(alphabet) | ||
}) | ||
|
||
test('with custom mapping', () => { | ||
expect(multiTap.decode('4 444 888 33 0 000 000 000 00', customMappingOptions)).toBe('GIVE $$$!') | ||
}) | ||
|
||
test('with invalid characters', () => { | ||
expect(() => { multiTap.decode('$2') }).toThrowError('Undecodable character') | ||
}) | ||
|
||
test('with invalid characters and silent fail', () => { | ||
expect(multiTap.decode('$2', silentFailOptions)).toBe('A') | ||
}) | ||
}) |