Skip to content

Commit 78c53a8

Browse files
author
Saiprashanth Pulisetti
committed
feat(ciphers): add Four-Square cipher with CLI
1 parent 8d0d777 commit 78c53a8

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

ciphers/four_square_cipher.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
#!/usr/bin/env python3
2+
"""Four-Square cipher implementation with CLI encode/decode.
3+
4+
Usage examples:
5+
python -m ciphers.four_square_cipher encode --key1 EXAMPLE --key2 KEYWORD --text "attack at once"
6+
python -m ciphers.four_square_cipher decode --key1 EXAMPLE --key2 KEYWORD --text <ciphertext>
7+
8+
This is a digraph substitution cipher using four 5x5 squares with I/J merged.
9+
"""
10+
from __future__ import annotations
11+
12+
import argparse
13+
import sys
14+
from typing import List, Tuple
15+
16+
ALPHABET = "ABCDEFGHIKLMNOPQRSTUVWXYZ"
17+
18+
19+
def build_square(keyword: str) -> str:
20+
key = "".join(ch for ch in keyword.upper() if ch.isalpha())
21+
key = key.replace("J", "I")
22+
seen = set()
23+
sq = []
24+
for ch in key + ALPHABET:
25+
if ch not in seen:
26+
seen.add(ch)
27+
sq.append(ch)
28+
return "".join(sq[:25])
29+
30+
31+
def clean_text(text: str) -> str:
32+
s = []
33+
for ch in text.upper():
34+
if ch.isalpha():
35+
s.append("I" if ch == "J" else ch)
36+
return "".join(s)
37+
38+
39+
def to_digraphs(text: str) -> List[Tuple[str, str]]:
40+
t = clean_text(text)
41+
if len(t) % 2 == 1:
42+
t += "X"
43+
return [(t[i], t[i + 1]) for i in range(0, len(t), 2)]
44+
45+
46+
def pos(square: str, ch: str) -> Tuple[int, int]:
47+
i = square.index(ch)
48+
return (i // 5, i % 5)
49+
50+
51+
def encode_pair(a: str, b: str, sq1: str, sq2: str) -> Tuple[str, str]:
52+
# top-left: standard alphabet, top-right: sq1, bottom-left: sq2, bottom-right: standard
53+
r1, c1 = pos(ALPHABET, a)
54+
r2, c2 = pos(ALPHABET, b)
55+
return (sq1[r1 * 5 + c2], sq2[r2 * 5 + c1])
56+
57+
58+
def decode_pair(a: str, b: str, sq1: str, sq2: str) -> Tuple[str, str]:
59+
# reverse mapping
60+
r1, c2 = pos(sq1, a)
61+
r2, c1 = pos(sq2, b)
62+
return (ALPHABET[r1 * 5 + c1], ALPHABET[r2 * 5 + c2])
63+
64+
65+
def encode(key1: str, key2: str, text: str) -> str:
66+
sq1 = build_square(key1)
67+
sq2 = build_square(key2)
68+
out: List[str] = []
69+
for a, b in to_digraphs(text):
70+
x, y = encode_pair(a, b, sq1, sq2)
71+
out.extend([x, y])
72+
return "".join(out)
73+
74+
75+
def decode(key1: str, key2: str, text: str) -> str:
76+
sq1 = build_square(key1)
77+
sq2 = build_square(key2)
78+
t = clean_text(text)
79+
if len(t) % 2 == 1:
80+
t += "X"
81+
out: List[str] = []
82+
for i in range(0, len(t), 2):
83+
a, b = t[i], t[i + 1]
84+
x, y = decode_pair(a, b, sq1, sq2)
85+
out.extend([x, y])
86+
return "".join(out)
87+
88+
89+
def parse_args(argv: List[str]) -> argparse.Namespace:
90+
parser = argparse.ArgumentParser(description="Four-Square cipher encode/decode")
91+
sub = parser.add_subparsers(dest="cmd", required=True)
92+
93+
p_enc = sub.add_parser("encode")
94+
p_enc.add_argument("--key1", required=True)
95+
p_enc.add_argument("--key2", required=True)
96+
p_enc.add_argument("--text", required=True)
97+
98+
p_dec = sub.add_parser("decode")
99+
p_dec.add_argument("--key1", required=True)
100+
p_dec.add_argument("--key2", required=True)
101+
p_dec.add_argument("--text", required=True)
102+
103+
return parser.parse_args(argv)
104+
105+
106+
def main(argv: List[str] | None = None) -> int:
107+
args = parse_args(sys.argv[1:] if argv is None else argv)
108+
if args.cmd == "encode":
109+
print(encode(args.key1, args.key2, args.text))
110+
return 0
111+
if args.cmd == "decode":
112+
print(decode(args.key1, args.key2, args.text))
113+
return 0
114+
return 2
115+
116+
117+
if __name__ == "__main__": # pragma: no cover
118+
raise SystemExit(main())

0 commit comments

Comments
 (0)