diff --git a/config.json b/config.json index c7665a1..7680322 100644 --- a/config.json +++ b/config.json @@ -746,6 +746,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "simple-cipher", + "name": "Simple Cipher", + "uuid": "f55d8d06-21cf-4ac4-a6cc-61ed90ff3588", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "simple-linked-list", "name": "Simple Linked List", diff --git a/exercises/practice/simple-cipher/.busted b/exercises/practice/simple-cipher/.busted new file mode 100644 index 0000000..86b84e7 --- /dev/null +++ b/exercises/practice/simple-cipher/.busted @@ -0,0 +1,5 @@ +return { + default = { + ROOT = { '.' } + } +} diff --git a/exercises/practice/simple-cipher/.docs/instructions.md b/exercises/practice/simple-cipher/.docs/instructions.md new file mode 100644 index 0000000..afd0b57 --- /dev/null +++ b/exercises/practice/simple-cipher/.docs/instructions.md @@ -0,0 +1,40 @@ +# Instructions + +Create an implementation of the [Vigenère cipher][wiki]. +The Vigenère cipher is a simple substitution cipher. + +## Cipher terminology + +A cipher is an algorithm used to encrypt, or encode, a string. +The unencrypted string is called the _plaintext_ and the encrypted string is called the _ciphertext_. +Converting plaintext to ciphertext is called _encoding_ while the reverse is called _decoding_. + +In a _substitution cipher_, each plaintext letter is replaced with a ciphertext letter which is computed with the help of a _key_. +(Note, it is possible for replacement letter to be the same as the original letter.) + +## Encoding details + +In this cipher, the key is a series of lowercase letters, such as `"abcd"`. +Each letter of the plaintext is _shifted_ or _rotated_ by a distance based on a corresponding letter in the key. +An `"a"` in the key means a shift of 0 (that is, no shift). +A `"b"` in the key means a shift of 1. +A `"c"` in the key means a shift of 2, and so on. + +The first letter of the plaintext uses the first letter of the key, the second letter of the plaintext uses the second letter of the key and so on. +If you run out of letters in the key before you run out of letters in the plaintext, start over from the start of the key again. + +If the key only contains one letter, such as `"dddddd"`, then all letters of the plaintext are shifted by the same amount (three in this example), which would make this the same as a rotational cipher or shift cipher (sometimes called a Caesar cipher). +For example, the plaintext `"iamapandabear"` would become `"ldpdsdqgdehdu"`. + +If the key only contains the letter `"a"` (one or more times), the shift distance is zero and the ciphertext is the same as the plaintext. + +Usually the key is more complicated than that, though! +If the key is `"abcd"` then letters of the plaintext would be shifted by a distance of 0, 1, 2, and 3. +If the plaintext is `"hello"`, we need 5 shifts so the key would wrap around, giving shift distances of 0, 1, 2, 3, and 0. +Applying those shifts to the letters of `"hello"` we get `"hfnoo"`. + +## Random keys + +If no key is provided, generate a key which consists of at least 100 random lowercase letters from the Latin alphabet. + +[wiki]: https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher diff --git a/exercises/practice/simple-cipher/.meta/config.json b/exercises/practice/simple-cipher/.meta/config.json new file mode 100644 index 0000000..2f1ca1c --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "simple_cipher.moon" + ], + "test": [ + "simple_cipher_spec.moon" + ], + "example": [ + ".meta/example.moon" + ] + }, + "blurb": "Implement the Vigenère cipher, a simple substitution cipher.", + "source": "Substitution Cipher at Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Substitution_cipher" +} diff --git a/exercises/practice/simple-cipher/.meta/example.moon b/exercises/practice/simple-cipher/.meta/example.moon new file mode 100644 index 0000000..70bc159 --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/example.moon @@ -0,0 +1,27 @@ +KEY_CHARS = [c for c in 'abcdefghijklmnopqrstuvwxyz'\gmatch '.'] +A = string.byte 'a' +INDEX = {c, string.byte(c) - A for c in *KEY_CHARS} + +randomKey = -> + table.concat [KEY_CHARS[math.random #KEY_CHARS] for _ = 1, 100] + +class SimpleCipher + new: (key) => + @Key = key or randomKey! + + key: => @Key + + encipher: (text, direction) => + while #@Key < #text + @Key ..= @Key + + idx = 0 + text\gsub '.', (char) -> + idx += 1 + t = INDEX[char] + k = INDEX[@Key\sub idx, idx] + i = (t + direction * k) % #KEY_CHARS + 1 + KEY_CHARS[i] + + encode: (text) => @encipher text, 1 + decode: (text) => @encipher text, -1 diff --git a/exercises/practice/simple-cipher/.meta/spec_generator.moon b/exercises/practice/simple-cipher/.meta/spec_generator.moon new file mode 100644 index 0000000..5bbc7f5 --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/spec_generator.moon @@ -0,0 +1,59 @@ +{ + module_name: 'SimpleCipher', + + generate_test: (case, level) -> + local lines + switch case.uuid + when "b8bdfbe1-bea3-41bb-a999-b41403f2b15d" + lines = { + "cipher = SimpleCipher!" + "plaintext = #{quote case.input.plaintext}" + "encoded = cipher\\encode plaintext" + "expected = cipher\\key!\\sub(1, #plaintext)" + "assert.are.equal expected, encoded" + } + when "3dff7f36-75db-46b4-ab70-644b3f38b81c" + lines = { + "cipher = SimpleCipher!" + "expected = #{quote case.expected}" + "ciphertext = cipher\\key!\\sub(1, #expected)" + "decoded = cipher\\decode ciphertext" + "assert.are.equal expected, decoded" + } + when "8143c684-6df6-46ba-bd1f-dea8fcb5d265" + lines = { + "cipher = SimpleCipher!" + "plaintext = #{quote case.input.plaintext}" + "ciphertext = cipher\\encode plaintext" + "decoded = cipher\\decode ciphertext" + "assert.are.equal plaintext, decoded" + } + when "defc0050-e87d-4840-85e4-51a1ab9dd6aa" + lines = { + "cipher = SimpleCipher!" + "key = cipher\\key!" + "assert.is.truthy key\\match('^[a-z]+$')" + } + when "70a16473-7339-43df-902d-93408c69e9d1" + lines = { + "cipher = SimpleCipher #{quote case.input.key}" + "ciphertext = cipher\\encode #{quote case.input.plaintext}" + "result = cipher\\decode ciphertext" + "assert.are.equal #{quote case.expected}, result" + } + else + switch case.property + when "encode" + lines = { + "cipher = SimpleCipher #{quote case.input.key}" + "result = cipher\\encode #{quote case.input.plaintext}" + "assert.are.equal #{quote case.expected}, result" + } + when "decode" + lines = { + "cipher = SimpleCipher #{quote case.input.key}" + "result = cipher\\decode #{quote case.input.ciphertext}" + "assert.are.equal #{quote case.expected}, result" + } + table.concat [indent line, level for line in *lines], '\n' +} diff --git a/exercises/practice/simple-cipher/.meta/tests.toml b/exercises/practice/simple-cipher/.meta/tests.toml new file mode 100644 index 0000000..77e6571 --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/tests.toml @@ -0,0 +1,46 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[b8bdfbe1-bea3-41bb-a999-b41403f2b15d] +description = "Random key cipher -> Can encode" + +[3dff7f36-75db-46b4-ab70-644b3f38b81c] +description = "Random key cipher -> Can decode" + +[8143c684-6df6-46ba-bd1f-dea8fcb5d265] +description = "Random key cipher -> Is reversible. I.e., if you apply decode in a encoded result, you must see the same plaintext encode parameter as a result of the decode method" + +[defc0050-e87d-4840-85e4-51a1ab9dd6aa] +description = "Random key cipher -> Key is made only of lowercase letters" + +[565e5158-5b3b-41dd-b99d-33b9f413c39f] +description = "Substitution cipher -> Can encode" + +[d44e4f6a-b8af-4e90-9d08-fd407e31e67b] +description = "Substitution cipher -> Can decode" + +[70a16473-7339-43df-902d-93408c69e9d1] +description = "Substitution cipher -> Is reversible. I.e., if you apply decode in a encoded result, you must see the same plaintext encode parameter as a result of the decode method" + +[69a1458b-92a6-433a-a02d-7beac3ea91f9] +description = "Substitution cipher -> Can double shift encode" + +[21d207c1-98de-40aa-994f-86197ae230fb] +description = "Substitution cipher -> Can wrap on encode" + +[a3d7a4d7-24a9-4de6-bdc4-a6614ced0cb3] +description = "Substitution cipher -> Can wrap on decode" + +[e31c9b8c-8eb6-45c9-a4b5-8344a36b9641] +description = "Substitution cipher -> Can encode messages longer than the key" + +[93cfaae0-17da-4627-9a04-d6d1e1be52e3] +description = "Substitution cipher -> Can decode messages longer than the key" diff --git a/exercises/practice/simple-cipher/simple_cipher.moon b/exercises/practice/simple-cipher/simple_cipher.moon new file mode 100644 index 0000000..0899a61 --- /dev/null +++ b/exercises/practice/simple-cipher/simple_cipher.moon @@ -0,0 +1,12 @@ +class SimpleCipher + new: () => + error 'Implement the constructor' + + key: => + error 'Implement the "key" method' + + encode: (plaintext) => + error 'Implement the "encode" method' + + decode: (ciphertext) => + error 'Implement the "decode" method' diff --git a/exercises/practice/simple-cipher/simple_cipher_spec.moon b/exercises/practice/simple-cipher/simple_cipher_spec.moon new file mode 100644 index 0000000..537c06b --- /dev/null +++ b/exercises/practice/simple-cipher/simple_cipher_spec.moon @@ -0,0 +1,71 @@ +SimpleCipher = require 'simple_cipher' + +describe 'simple-cipher', -> + describe 'Random key cipher', -> + it 'Can encode', -> + cipher = SimpleCipher! + plaintext = 'aaaaaaaaaa' + encoded = cipher\encode plaintext + expected = cipher\key!\sub(1, #plaintext) + assert.are.equal expected, encoded + + pending 'Can decode', -> + cipher = SimpleCipher! + expected = 'aaaaaaaaaa' + ciphertext = cipher\key!\sub(1, #expected) + decoded = cipher\decode ciphertext + assert.are.equal expected, decoded + + pending 'Is reversible. I.e., if you apply decode in a encoded result, you must see the same plaintext encode parameter as a result of the decode method', -> + cipher = SimpleCipher! + plaintext = 'abcdefghij' + ciphertext = cipher\encode plaintext + decoded = cipher\decode ciphertext + assert.are.equal plaintext, decoded + + pending 'Key is made only of lowercase letters', -> + cipher = SimpleCipher! + key = cipher\key! + assert.is.truthy key\match('^[a-z]+$') + + describe 'Substitution cipher', -> + pending 'Can encode', -> + cipher = SimpleCipher 'abcdefghij' + result = cipher\encode 'aaaaaaaaaa' + assert.are.equal 'abcdefghij', result + + pending 'Can decode', -> + cipher = SimpleCipher 'abcdefghij' + result = cipher\decode 'abcdefghij' + assert.are.equal 'aaaaaaaaaa', result + + pending 'Is reversible. I.e., if you apply decode in a encoded result, you must see the same plaintext encode parameter as a result of the decode method', -> + cipher = SimpleCipher 'abcdefghij' + ciphertext = cipher\encode 'abcdefghij' + result = cipher\decode ciphertext + assert.are.equal 'abcdefghij', result + + pending 'Can double shift encode', -> + cipher = SimpleCipher 'iamapandabear' + result = cipher\encode 'iamapandabear' + assert.are.equal 'qayaeaagaciai', result + + pending 'Can wrap on encode', -> + cipher = SimpleCipher 'abcdefghij' + result = cipher\encode 'zzzzzzzzzz' + assert.are.equal 'zabcdefghi', result + + pending 'Can wrap on decode', -> + cipher = SimpleCipher 'abcdefghij' + result = cipher\decode 'zabcdefghi' + assert.are.equal 'zzzzzzzzzz', result + + pending 'Can encode messages longer than the key', -> + cipher = SimpleCipher 'abc' + result = cipher\encode 'iamapandabear' + assert.are.equal 'iboaqcnecbfcr', result + + pending 'Can decode messages longer than the key', -> + cipher = SimpleCipher 'abc' + result = cipher\decode 'iboaqcnecbfcr' + assert.are.equal 'iamapandabear', result