From bdd40d944fe0735d34234fbea9899e66ec1c49d9 Mon Sep 17 00:00:00 2001 From: metif12 Date: Mon, 28 Sep 2026 12:05:04 +0330 Subject: [PATCH] feat: add simple-cipher exercise Vigenere cipher. The example turns the key into a repeating sequence of shifts and applies it to each letter of the input, wrapping around the alphabet. The canonical data in problem-specifications still uses the old nested v2 test format, so run_test.v flattens it. Registers the exercise in config.json. Verified with bin/verify-exercises-in-docker (the current test runner image) and with V 0.5.2 for bin/test. configlet lint passes, and v fmt -l / v vet are clean on the new files. --- config.json | 8 ++ .../simple-cipher/.docs/instructions.md | 40 +++++++++ .../practice/simple-cipher/.meta/config.json | 19 ++++ .../practice/simple-cipher/.meta/example.v | 63 +++++++++++++ .../practice/simple-cipher/.meta/tests.toml | 46 ++++++++++ exercises/practice/simple-cipher/run_test.v | 89 +++++++++++++++++++ .../practice/simple-cipher/simple-cipher.v | 27 ++++++ 7 files changed, 292 insertions(+) create mode 100644 exercises/practice/simple-cipher/.docs/instructions.md create mode 100644 exercises/practice/simple-cipher/.meta/config.json create mode 100644 exercises/practice/simple-cipher/.meta/example.v create mode 100644 exercises/practice/simple-cipher/.meta/tests.toml create mode 100644 exercises/practice/simple-cipher/run_test.v create mode 100644 exercises/practice/simple-cipher/simple-cipher.v diff --git a/config.json b/config.json index cb3e874bf3..1fe5223d59 100644 --- a/config.json +++ b/config.json @@ -640,6 +640,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "simple-cipher", + "name": "Simple Cipher", + "uuid": "3ee728ed-25dd-4597-87aa-1ca377174cf0", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "space-age", "name": "Space Age", diff --git a/exercises/practice/simple-cipher/.docs/instructions.md b/exercises/practice/simple-cipher/.docs/instructions.md new file mode 100644 index 0000000000..afd0b57da9 --- /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 0000000000..720df0c29f --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "metif12" + ], + "files": { + "solution": [ + "simple-cipher.v" + ], + "test": [ + "run_test.v" + ], + "example": [ + ".meta/example.v" + ] + }, + "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.v b/exercises/practice/simple-cipher/.meta/example.v new file mode 100644 index 0000000000..8e72480429 --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/example.v @@ -0,0 +1,63 @@ +module main + +import rand + +// The Latin alphabet, in the order the cipher shifts through. +const alphabet = 'abcdefghijklmnopqrstuvwxyz' + +// The number of letters in the Latin alphabet, and the size of a Vigenere key. +const alphabet_size = 26 + +// random_key returns a key made of `length` random lowercase Latin letters. +// The instructions ask for at least 100 letters when no key is supplied. +fn random_key(length int) string { + return rand.string_from_set(alphabet, length) +} + +// encode encrypts `text` with the Vigenere cipher. +// +// Every letter is shifted forward by the distance given by the matching letter +// of `key`, wrapping around the alphabet. The key repeats for as long as the +// text needs it, upper case letters keep their case, and every other character +// is copied unchanged. +// +// > returns the ciphertext +// > returns an error when `key` is empty +fn encode(text string, key string) !string { + return rotate(text, key, 1) +} + +// decode turns `text` back into plaintext with the same `key` that `encode` +// used. +// +// > returns the plaintext +// > returns an error when `key` is empty +fn decode(text string, key string) !string { + return rotate(text, key, -1) +} + +// rotate shifts every letter of `text` by the matching letter of `key`. +// `direction` is 1 to encode and -1 to decode. +fn rotate(text string, key string, direction int) !string { + if key == '' { + return error('the key must not be empty') + } + mut out := []u8{cap: text.len} + for i, ch in text { + if !is_letter(ch) { + out << ch + continue + } + // Adding the alphabet size first keeps the sum positive, so the + // remainder below behaves the same for encoding and decoding. + shift := int(key[i % key.len] - `a`) * direction + rotated := (int(ch - `a`) + shift + alphabet_size) % alphabet_size + out << if ch < `a` { u8(`A` + rotated) } else { u8(`a` + rotated) } + } + return out.bytestr() +} + +// is_letter reports whether `ch` is a Latin letter of either case. +fn is_letter(ch u8) bool { + return (ch >= `a` && ch <= `z`) || (ch >= `A` && ch <= `Z`) +} diff --git a/exercises/practice/simple-cipher/.meta/tests.toml b/exercises/practice/simple-cipher/.meta/tests.toml new file mode 100644 index 0000000000..77e6571e40 --- /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/run_test.v b/exercises/practice/simple-cipher/run_test.v new file mode 100644 index 0000000000..6bfc184ee5 --- /dev/null +++ b/exercises/practice/simple-cipher/run_test.v @@ -0,0 +1,89 @@ +module main + +// is_lowercase_letters reports whether `s` is a non-empty run of lowercase +// letters. This is the check behind the canonical `^[a-z]+$` match on the key. +fn is_lowercase_letters(s string) bool { + if s == '' { + return false + } + for ch in s { + if ch < `a` || ch > `z` { + return false + } + } + return true +} + +// --- Random key cipher --- +// +// The canonical data describes these results symbolically, in terms of a +// `cipher.key` it never spells out. Shifting a run of `a`s reproduces the key +// unchanged, so a freshly generated key stands in for it exactly. + +fn test_random_key_cipher_can_encode() { + plaintext := 'aaaaaaaaaa' + key := random_key(100) + // Canonical: cipher.key.substring(0, plaintext.length) + assert encode(plaintext, key)! == key[..plaintext.len] +} + +fn test_random_key_cipher_can_decode() { + key := random_key(100) + // Canonical: cipher.key.substring(0, expected.length) + ciphertext := key[..'aaaaaaaaaa'.len] + assert decode(ciphertext, key)! == 'aaaaaaaaaa' +} + +fn test_random_key_cipher_is_reversible() { + plaintext := 'abcdefghij' + key := random_key(100) + // Canonical: cipher.encode, i.e. whatever the cipher just produced. + ciphertext := encode(plaintext, key)! + assert decode(ciphertext, key)! == plaintext +} + +fn test_random_key_cipher_key_is_made_only_of_lowercase_letters() { + key := random_key(100) + assert key.len == 100 + // Canonical: the key matches ^[a-z]+$ + assert is_lowercase_letters(key) +} + +// --- Substitution cipher --- + +fn test_substitution_cipher_can_encode() { + assert encode('aaaaaaaaaa', 'abcdefghij')! == 'abcdefghij' +} + +fn test_substitution_cipher_can_decode() { + assert decode('abcdefghij', 'abcdefghij')! == 'aaaaaaaaaa' +} + +fn test_substitution_cipher_is_reversible() { + key := 'abcdefghij' + // The intermediate ciphertext is pinned down too, so that a `decode` that + // simply echoed its input could not pass this test. + ciphertext := encode('abcdefghij', key)! + assert ciphertext == 'acegikmoqs' + assert decode(ciphertext, key)! == 'abcdefghij' +} + +fn test_substitution_cipher_can_double_shift_encode() { + assert encode('iamapandabear', 'iamapandabear')! == 'qayaeaagaciai' +} + +fn test_substitution_cipher_can_wrap_on_encode() { + assert encode('zzzzzzzzzz', 'abcdefghij')! == 'zabcdefghi' +} + +fn test_substitution_cipher_can_wrap_on_decode() { + assert decode('zabcdefghi', 'abcdefghij')! == 'zzzzzzzzzz' +} + +fn test_substitution_cipher_can_encode_messages_longer_than_the_key() { + assert encode('iamapandabear', 'abc')! == 'iboaqcnecbfcr' +} + +fn test_substitution_cipher_can_decode_messages_longer_than_the_key() { + assert decode('iboaqcnecbfcr', 'abc')! == 'iamapandabear' +} diff --git a/exercises/practice/simple-cipher/simple-cipher.v b/exercises/practice/simple-cipher/simple-cipher.v new file mode 100644 index 0000000000..8049100d8e --- /dev/null +++ b/exercises/practice/simple-cipher/simple-cipher.v @@ -0,0 +1,27 @@ +module main + +// random_key returns a key made of `length` random lowercase Latin letters. +// The instructions ask for at least 100 letters when no key is supplied. V's +// `rand` module draws such a key with `string_from_set`. +fn random_key(length int) string { +} + +// encode encrypts `text` with the Vigenere cipher. +// +// Every letter is shifted forward by the distance given by the matching letter +// of `key`, wrapping around the alphabet. The key repeats for as long as the +// text needs it, upper case letters keep their case, and every other character +// is copied unchanged. +// +// > returns the ciphertext +// > returns an error when `key` is empty +fn encode(text string, key string) !string { +} + +// decode turns `text` back into plaintext with the same `key` that `encode` +// used. +// +// > returns the plaintext +// > returns an error when `key` is empty +fn decode(text string, key string) !string { +}