Skip to content

Commit

Permalink
feat: Add Multi-Tap cipher
Browse files Browse the repository at this point in the history
fixes #4
  • Loading branch information
manniL committed Mar 31, 2018
1 parent 19d11a0 commit 4114b88
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ console.log(rot('Hello world!'))
- Morse (custom delimiter, custom handling of unknown characters)
- Fractionated Morse
- Pollux
- Multi-Tap


## Contributing
Expand Down
135 changes: 135 additions & 0 deletions docs/ciphers/multi-tap.md
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!
```
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
* [Morse](./ciphers/morse.md)
* [Fractionated Morse](./ciphers/fractionated-morse.md)
* [Pollux](./ciphers/pollux.md)
* [Multi-Tap](./ciphers/multi-tap.md)

4 changes: 3 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import rot from './rot'
import morse from './morse'
import fractionatedMorse from './fractionatedMorse'
import pollux from './pollux'
import multiTap from './multiTap'

export default {
rot,
morse,
fractionatedMorse,
pollux
pollux,
multiTap
}
69 changes: 69 additions & 0 deletions src/multiTap.js
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
}
90 changes: 90 additions & 0 deletions test/multiTap.test.js
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')
})
})

0 comments on commit 4114b88

Please sign in to comment.