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