Skip to content

Commit 4ca35cc

Browse files
author
Saiprashanth Pulisetti
committed
feat(ciphers): add scytale (skytale) transposition cipher with doctests
1 parent a71618f commit 4ca35cc

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

ciphers/skytale_cipher.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""Scytale (Skytale) transposition cipher.
2+
3+
A classical transposition cipher used in ancient Greece. The sender wraps a
4+
strip of parchment around a rod (scytale) and writes the message along the rod.
5+
The recipient with a rod of the same diameter can read the message.
6+
7+
Reference: https://en.wikipedia.org/wiki/Scytale
8+
9+
Functions here keep characters as-is (including spaces). The key is a positive
10+
integer representing the circumference count (number of rows).
11+
12+
>>> encrypt("WE ARE DISCOVERED FLEE AT ONCE", 3)
13+
'WA SVEFETNERDCEDL C EIOR EAOE'
14+
>>> decrypt('WA SVEFETNERDCEDL C EIOR EAOE', 3)
15+
'WE ARE DISCOVERED FLEE AT ONCE'
16+
17+
Edge cases:
18+
>>> encrypt("HELLO", 1)
19+
'HELLO'
20+
>>> decrypt("HELLO", 1)
21+
'HELLO'
22+
>>> encrypt("HELLO", 5) # key equals length
23+
'HELLO'
24+
>>> decrypt("HELLO", 5)
25+
'HELLO'
26+
>>> encrypt("HELLO", 0)
27+
Traceback (most recent call last):
28+
...
29+
ValueError: Key must be a positive integer
30+
>>> decrypt("HELLO", -2)
31+
Traceback (most recent call last):
32+
...
33+
ValueError: Key must be a positive integer
34+
"""
35+
from __future__ import annotations
36+
37+
from typing import List
38+
39+
40+
def encrypt(plaintext: str, key: int) -> str:
41+
"""Encrypt plaintext using Scytale transposition.
42+
43+
Write characters around a rod with `key` rows, then read off by rows.
44+
45+
:param plaintext: Input message to encrypt
46+
:param key: Positive integer number of rows
47+
:return: Ciphertext string
48+
:raises ValueError: if key <= 0
49+
"""
50+
if key <= 0:
51+
raise ValueError("Key must be a positive integer")
52+
if key == 1 or len(plaintext) <= key:
53+
return plaintext
54+
55+
# Read every key-th character starting from each row offset
56+
return "".join(plaintext[row::key] for row in range(key))
57+
58+
59+
def decrypt(ciphertext: str, key: int) -> str:
60+
"""Decrypt Scytale ciphertext.
61+
62+
Reconstruct rows by their lengths and interleave by columns.
63+
64+
:param ciphertext: Encrypted string
65+
:param key: Positive integer number of rows
66+
:return: Decrypted plaintext
67+
:raises ValueError: if key <= 0
68+
"""
69+
if key <= 0:
70+
raise ValueError("Key must be a positive integer")
71+
if key == 1 or len(ciphertext) <= key:
72+
return ciphertext
73+
74+
length = len(ciphertext)
75+
base = length // key
76+
extra = length % key
77+
78+
# Determine each row length
79+
row_lengths: List[int] = [base + (1 if r < extra else 0) for r in range(key)]
80+
81+
# Slice ciphertext into rows
82+
rows: List[str] = []
83+
idx = 0
84+
for r_len in row_lengths:
85+
rows.append(ciphertext[idx : idx + r_len])
86+
idx += r_len
87+
88+
# Pointers to current index in each row
89+
pointers = [0] * key
90+
91+
# Reconstruct by taking characters column-wise across rows
92+
result_chars: List[str] = []
93+
for i in range(length):
94+
r = i % key
95+
if pointers[r] < len(rows[r]):
96+
result_chars.append(rows[r][pointers[r]])
97+
pointers[r] += 1
98+
return "".join(result_chars)
99+
100+
101+
if __name__ == "__main__": # pragma: no cover
102+
import doctest
103+
104+
doctest.testmod()

0 commit comments

Comments
 (0)