Skip to content

Commit 2cb932c

Browse files
authored
Implement the Simple Cipher Exercise (#149)
1 parent f683516 commit 2cb932c

File tree

7 files changed

+365
-4
lines changed

7 files changed

+365
-4
lines changed

config.json

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@
4646
"prerequisites": [],
4747
"difficulty": 1
4848
},
49+
{
50+
"slug": "space-age",
51+
"name": "Space Age",
52+
"uuid": "031f2fe0-e8b1-48c2-8ce3-f9ab8b76423c",
53+
"practices": [],
54+
"prerequisites": [],
55+
"difficulty": 1
56+
},
4957
{
5058
"slug": "two-fer",
5159
"name": "Two Fer",
@@ -319,12 +327,12 @@
319327
"difficulty": 2
320328
},
321329
{
322-
"slug": "space-age",
323-
"name": "Space Age",
324-
"uuid": "031f2fe0-e8b1-48c2-8ce3-f9ab8b76423c",
330+
"slug": "simple-cipher",
331+
"name": "Simple Cipher",
332+
"uuid": "d8ab5e48-f9b3-4016-a576-c14f361d2db1",
325333
"practices": [],
326334
"prerequisites": [],
327-
"difficulty": 1
335+
"difficulty": 2
328336
},
329337
{
330338
"slug": "triangle",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Instructions
2+
3+
Create an implementation of the [Vigenère cipher][wiki].
4+
The Vigenère cipher is a simple substitution cipher.
5+
6+
## Cipher terminology
7+
8+
A cipher is an algorithm used to encrypt, or encode, a string.
9+
The unencrypted string is called the _plaintext_ and the encrypted string is called the _ciphertext_.
10+
Converting plaintext to ciphertext is called _encoding_ while the reverse is called _decoding_.
11+
12+
In a _substitution cipher_, each plaintext letter is replaced with a ciphertext letter which is computed with the help of a _key_.
13+
(Note, it is possible for replacement letter to be the same as the original letter.)
14+
15+
## Encoding details
16+
17+
In this cipher, the key is a series of lowercase letters, such as `"abcd"`.
18+
Each letter of the plaintext is _shifted_ or _rotated_ by a distance based on a corresponding letter in the key.
19+
An `"a"` in the key means a shift of 0 (that is, no shift).
20+
A `"b"` in the key means a shift of 1.
21+
A `"c"` in the key means a shift of 2, and so on.
22+
23+
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.
24+
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.
25+
26+
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).
27+
For example, the plaintext `"iamapandabear"` would become `"ldpdsdqgdehdu"`.
28+
29+
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.
30+
31+
Usually the key is more complicated than that, though!
32+
If the key is `"abcd"` then letters of the plaintext would be shifted by a distance of 0, 1, 2, and 3.
33+
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.
34+
Applying those shifts to the letters of `"hello"` we get `"hfnoo"`.
35+
36+
## Random keys
37+
38+
If no key is provided, generate a key which consists of at least 100 random lowercase letters from the Latin alphabet.
39+
40+
[wiki]: https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"authors": [
3+
"rmonnet"
4+
],
5+
"files": {
6+
"solution": [
7+
"simple_cipher.odin"
8+
],
9+
"test": [
10+
"simple_cipher_test.odin"
11+
],
12+
"example": [
13+
".meta/example.odin"
14+
]
15+
},
16+
"blurb": "Implement the Vigenère cipher, a simple substitution cipher.",
17+
"source": "Substitution Cipher at Wikipedia",
18+
"source_url": "https://en.wikipedia.org/wiki/Substitution_cipher"
19+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package simple_cipher
2+
3+
import "core:math/rand"
4+
5+
decode :: proc(ciphertext, key: string) -> string {
6+
7+
plaintext := make([]byte, len(ciphertext))
8+
for i := 0; i < len(ciphertext); i += 1 {
9+
shift := key[i % len(key)] - 'a'
10+
plaintext[i] = ciphertext[i] - shift
11+
// If we go before 'a', start again at 'z'.
12+
if plaintext[i] < 'a' {
13+
plaintext[i] += 26
14+
}
15+
}
16+
return string(plaintext)
17+
}
18+
19+
encode :: proc(plaintext, key: string) -> string {
20+
21+
ciphertext := make([]byte, len(plaintext))
22+
for i := 0; i < len(plaintext); i += 1 {
23+
shift := key[i % len(key)] - 'a'
24+
ciphertext[i] = plaintext[i] + shift
25+
// If we go past 'z', start again at 'a'.
26+
if ciphertext[i] > 'z' {
27+
ciphertext[i] -= 26
28+
}
29+
}
30+
return string(ciphertext)
31+
}
32+
33+
key :: proc() -> string {
34+
35+
key_bytes := make([]byte, 100)
36+
for i := 0; i < len(key_bytes); i += 1 {
37+
key_bytes[i] = u8(rand.int_max(26)) + 'a'
38+
}
39+
return string(key_bytes)
40+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[b8bdfbe1-bea3-41bb-a999-b41403f2b15d]
13+
description = "Random key cipher -> Can encode"
14+
15+
[3dff7f36-75db-46b4-ab70-644b3f38b81c]
16+
description = "Random key cipher -> Can decode"
17+
18+
[8143c684-6df6-46ba-bd1f-dea8fcb5d265]
19+
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"
20+
21+
[defc0050-e87d-4840-85e4-51a1ab9dd6aa]
22+
description = "Random key cipher -> Key is made only of lowercase letters"
23+
24+
[565e5158-5b3b-41dd-b99d-33b9f413c39f]
25+
description = "Substitution cipher -> Can encode"
26+
27+
[d44e4f6a-b8af-4e90-9d08-fd407e31e67b]
28+
description = "Substitution cipher -> Can decode"
29+
30+
[70a16473-7339-43df-902d-93408c69e9d1]
31+
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"
32+
33+
[69a1458b-92a6-433a-a02d-7beac3ea91f9]
34+
description = "Substitution cipher -> Can double shift encode"
35+
36+
[21d207c1-98de-40aa-994f-86197ae230fb]
37+
description = "Substitution cipher -> Can wrap on encode"
38+
39+
[a3d7a4d7-24a9-4de6-bdc4-a6614ced0cb3]
40+
description = "Substitution cipher -> Can wrap on decode"
41+
42+
[e31c9b8c-8eb6-45c9-a4b5-8344a36b9641]
43+
description = "Substitution cipher -> Can encode messages longer than the key"
44+
45+
[93cfaae0-17da-4627-9a04-d6d1e1be52e3]
46+
description = "Substitution cipher -> Can decode messages longer than the key"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package simple_cipher
2+
3+
import "core:math/rand"
4+
5+
decode :: proc(ciphertext, key: string) -> string {
6+
// Implement the procedure.
7+
return ""
8+
}
9+
10+
encode :: proc(plaintext, key: string) -> string {
11+
// Implement the procedure.
12+
return ""
13+
}
14+
15+
key :: proc() -> string {
16+
// Implement the procedure.
17+
return ""
18+
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package simple_cipher
2+
3+
import "core:strings"
4+
import "core:testing"
5+
6+
@(test)
7+
/// description = Random key cipher -> Can encode
8+
test_random_key_cipher___can_encode :: proc(t: ^testing.T) {
9+
10+
plaintext := "aaaaaaaaaa"
11+
random_key := key()
12+
result := encode(plaintext, random_key)
13+
expected, _ := strings.substring(random_key, 0, len(plaintext))
14+
defer {
15+
delete(random_key)
16+
delete(result)
17+
}
18+
19+
testing.expectf(
20+
t,
21+
len(random_key) >= 100,
22+
"key() returns key of less than 100 characters (%d)",
23+
len(random_key),
24+
)
25+
testing.expect_value(t, result, expected)
26+
}
27+
28+
@(test)
29+
/// description = Random key cipher -> Can decode
30+
test_random_key_cipher___can_decode :: proc(t: ^testing.T) {
31+
32+
random_key := key()
33+
expected := "aaaaaaaaaa"
34+
ciphertext, _ := strings.substring(random_key, 0, len(expected))
35+
result := decode(ciphertext, random_key)
36+
defer {
37+
delete(random_key)
38+
delete(result)
39+
}
40+
41+
testing.expectf(
42+
t,
43+
len(random_key) >= 100,
44+
"key() returns key of less than 100 characters (%d)",
45+
len(random_key),
46+
)
47+
testing.expect_value(t, result, expected)
48+
}
49+
50+
@(test)
51+
/// 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
52+
test_random_key_cipher___is_reversible_ie_if_you_apply_decode_in_a_encoded_result_you_must_see_the_same_plaintext_encode_parameter_as_a_result_of_the_decode_method :: proc(
53+
t: ^testing.T,
54+
) {
55+
56+
random_key := key()
57+
plaintext := "abcdefghij"
58+
ciphertext := encode(plaintext, random_key)
59+
result := decode(ciphertext, random_key)
60+
defer {
61+
delete(random_key)
62+
delete(ciphertext)
63+
delete(result)
64+
}
65+
66+
testing.expect_value(t, result, plaintext)
67+
}
68+
69+
@(test)
70+
/// description = Random key cipher -> Key is made only of lowercase letters
71+
test_random_key_cipher___key_is_made_only_of_lowercase_letters :: proc(t: ^testing.T) {
72+
73+
random_key := key()
74+
expected := strings.to_lower(random_key)
75+
defer {
76+
delete(random_key)
77+
delete(expected)
78+
}
79+
80+
testing.expect_value(t, random_key, expected)
81+
}
82+
83+
@(test)
84+
/// description = Substitution cipher -> Can encode
85+
test_substitution_cipher___can_encode :: proc(t: ^testing.T) {
86+
87+
key := "abcdefghij"
88+
plaintext := "aaaaaaaaaa"
89+
result := encode(plaintext, key)
90+
expected := "abcdefghij"
91+
defer delete(result)
92+
93+
testing.expect_value(t, result, expected)
94+
}
95+
96+
@(test)
97+
/// description = Substitution cipher -> Can decode
98+
test_substitution_cipher___can_decode :: proc(t: ^testing.T) {
99+
100+
key := "abcdefghij"
101+
ciphertext := "abcdefghij"
102+
result := decode(ciphertext, key)
103+
expected := "aaaaaaaaaa"
104+
defer delete(result)
105+
106+
testing.expect_value(t, result, expected)
107+
}
108+
109+
@(test)
110+
/// 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
111+
test_substitution_cipher___is_reversible_ie_if_you_apply_decode_in_a_encoded_result_you_must_see_the_same_plaintext_encode_parameter_as_a_result_of_the_decode_method :: proc(
112+
t: ^testing.T,
113+
) {
114+
115+
key := "abcdefghij"
116+
plaintext := "abcdefghij"
117+
ciphertext := encode(plaintext, key)
118+
result := decode(ciphertext, key)
119+
defer {
120+
delete(ciphertext)
121+
delete(result)
122+
}
123+
124+
testing.expect_value(t, result, plaintext)
125+
}
126+
127+
@(test)
128+
/// description = Substitution cipher -> Can double shift encode
129+
test_substitution_cipher___can_double_shift_encode :: proc(t: ^testing.T) {
130+
131+
key := "iamapandabear"
132+
plaintext := "iamapandabear"
133+
result := encode(plaintext, key)
134+
expected := "qayaeaagaciai"
135+
defer delete(result)
136+
137+
testing.expect_value(t, result, expected)
138+
}
139+
140+
@(test)
141+
/// description = Substitution cipher -> Can wrap on encode
142+
test_substitution_cipher___can_wrap_on_encode :: proc(t: ^testing.T) {
143+
144+
key := "abcdefghij"
145+
plaintext := "zzzzzzzzzz"
146+
result := encode(plaintext, key)
147+
expected := "zabcdefghi"
148+
defer delete(result)
149+
150+
testing.expect_value(t, result, expected)
151+
}
152+
153+
@(test)
154+
/// description = Substitution cipher -> Can wrap on decode
155+
test_substitution_cipher___can_wrap_on_decode :: proc(t: ^testing.T) {
156+
157+
key := "abcdefghij"
158+
ciphertext := "zabcdefghi"
159+
result := decode(ciphertext, key)
160+
expected := "zzzzzzzzzz"
161+
defer delete(result)
162+
163+
testing.expect_value(t, result, expected)
164+
}
165+
166+
@(test)
167+
/// description = Substitution cipher -> Can encode messages longer than the key
168+
test_substitution_cipher___can_encode_messages_longer_than_the_key :: proc(t: ^testing.T) {
169+
170+
key := "abc"
171+
plaintext := "iamapandabear"
172+
result := encode(plaintext, key)
173+
expected := "iboaqcnecbfcr"
174+
defer delete(result)
175+
176+
testing.expect_value(t, result, expected)
177+
}
178+
179+
@(test)
180+
/// description = Substitution cipher -> Can decode messages longer than the key
181+
test_substitution_cipher___can_decode_messages_longer_than_the_key :: proc(t: ^testing.T) {
182+
183+
key := "abc"
184+
ciphertext := "iboaqcnecbfcr"
185+
result := decode(ciphertext, key)
186+
expected := "iamapandabear"
187+
defer delete(result)
188+
189+
testing.expect_value(t, result, expected)
190+
}

0 commit comments

Comments
 (0)