From 4ca35cc10f91fc08afc9698eb62da28b6ac50ff9 Mon Sep 17 00:00:00 2001 From: Saiprashanth Pulisetti Date: Thu, 2 Oct 2025 15:32:35 +0530 Subject: [PATCH 01/12] feat(ciphers): add scytale (skytale) transposition cipher with doctests --- ciphers/skytale_cipher.py | 104 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 ciphers/skytale_cipher.py diff --git a/ciphers/skytale_cipher.py b/ciphers/skytale_cipher.py new file mode 100644 index 000000000000..e8b3d5953e6c --- /dev/null +++ b/ciphers/skytale_cipher.py @@ -0,0 +1,104 @@ +"""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 + +from typing import List + + +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() From a27583a20abab2300ea334edac07bfb603028d9a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:05:54 +0000 Subject: [PATCH 02/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ciphers/skytale_cipher.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ciphers/skytale_cipher.py b/ciphers/skytale_cipher.py index e8b3d5953e6c..99e3750c3cdd 100644 --- a/ciphers/skytale_cipher.py +++ b/ciphers/skytale_cipher.py @@ -32,6 +32,7 @@ ... ValueError: Key must be a positive integer """ + from __future__ import annotations from typing import List From c6e3d9f93fb2e152746cda78c0fc8e5d9840355b Mon Sep 17 00:00:00 2001 From: Saiprashanth Pulisetti Date: Thu, 2 Oct 2025 15:36:24 +0530 Subject: [PATCH 03/12] chore(ciphers): satisfy ruff UP006/UP035 by using builtin generics --- ciphers/skytale_cipher.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ciphers/skytale_cipher.py b/ciphers/skytale_cipher.py index 99e3750c3cdd..9a77a16fac3f 100644 --- a/ciphers/skytale_cipher.py +++ b/ciphers/skytale_cipher.py @@ -35,7 +35,6 @@ from __future__ import annotations -from typing import List def encrypt(plaintext: str, key: int) -> str: @@ -77,10 +76,10 @@ def decrypt(ciphertext: str, key: int) -> str: extra = length % key # Determine each row length - row_lengths: List[int] = [base + (1 if r < extra else 0) for r in range(key)] + row_lengths: list[int] = [base + (1 if r < extra else 0) for r in range(key)] # Slice ciphertext into rows - rows: List[str] = [] + rows: list[str] = [] idx = 0 for r_len in row_lengths: rows.append(ciphertext[idx : idx + r_len]) @@ -90,7 +89,7 @@ def decrypt(ciphertext: str, key: int) -> str: pointers = [0] * key # Reconstruct by taking characters column-wise across rows - result_chars: List[str] = [] + result_chars: list[str] = [] for i in range(length): r = i % key if pointers[r] < len(rows[r]): From 2b2e584d392843b0a31f4ed7a2b05881ad8fcdd9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:09:46 +0000 Subject: [PATCH 04/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ciphers/skytale_cipher.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ciphers/skytale_cipher.py b/ciphers/skytale_cipher.py index 9a77a16fac3f..11fe83e5c45c 100644 --- a/ciphers/skytale_cipher.py +++ b/ciphers/skytale_cipher.py @@ -36,7 +36,6 @@ from __future__ import annotations - def encrypt(plaintext: str, key: int) -> str: """Encrypt plaintext using Scytale transposition. From e018e65d1a4aaedf31a44dba7f301e7b0cee3220 Mon Sep 17 00:00:00 2001 From: Saiprashanth Pulisetti Date: Thu, 2 Oct 2025 15:40:46 +0530 Subject: [PATCH 05/12] style(ciphers): fix import block formatting (isort I001) --- ciphers/skytale_cipher.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ciphers/skytale_cipher.py b/ciphers/skytale_cipher.py index 11fe83e5c45c..57c08c522407 100644 --- a/ciphers/skytale_cipher.py +++ b/ciphers/skytale_cipher.py @@ -32,7 +32,6 @@ ... ValueError: Key must be a positive integer """ - from __future__ import annotations From 16ebe367c00bf58ecb929cfe5c7165c44097d8c7 Mon Sep 17 00:00:00 2001 From: Saiprashanth Pulisetti Date: Thu, 2 Oct 2025 15:49:44 +0530 Subject: [PATCH 06/12] feat(ciphers): add columnar transposition cipher with doctests --- ciphers/columnar_transposition.py | 121 ++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 ciphers/columnar_transposition.py diff --git a/ciphers/columnar_transposition.py b/ciphers/columnar_transposition.py new file mode 100644 index 000000000000..bddf4e7aaede --- /dev/null +++ b/ciphers/columnar_transposition.py @@ -0,0 +1,121 @@ +"""Columnar Transposition cipher. + +This classical cipher writes the plaintext in rows under a keyword and reads +columns in the order of the alphabetical rank of the keyword letters. + +Reference: https://en.wikipedia.org/wiki/Transposition_cipher#Columnar_transposition + +We keep spaces and punctuation. Key must be alphabetic (case-insensitive). + +>>> pt = "WE ARE DISCOVERED. FLEE AT ONCE" +>>> ct = encrypt(pt, "ZEBRAS") +>>> decrypt(ct, "ZEBRAS") == pt +True + +Edge cases: +>>> encrypt("HELLO", "A") +'HELLO' +>>> decrypt("HELLO", "A") +'HELLO' +>>> encrypt("HELLO", "HELLO") +'EHLLO' +>>> decrypt("EHLLO", "HELLO") +'HELLO' +>>> encrypt("HELLO", "") +Traceback (most recent call last): + ... +ValueError: Key must be a non-empty alphabetic string +""" +from __future__ import annotations + + +def _normalize_key(key: str) -> str: + k = "".join(ch for ch in key.upper() if ch.isalpha()) + if not k: + raise ValueError("Key must be a non-empty alphabetic string") + return k + + +def _column_order(key: str) -> list[int]: + # Stable sort by character then original index to handle duplicates + indexed = list(enumerate(key)) + return [i for i, _ in sorted(indexed, key=lambda t: (t[1], t[0]))] + + +def encrypt(plaintext: str, key: str) -> str: + """Encrypt using columnar transposition. + + :param plaintext: Input text (any characters) + :param key: Alphabetic keyword + :return: Ciphertext + :raises ValueError: on invalid key + """ + k = _normalize_key(key) + cols = len(k) + if cols == 1: + return plaintext + + order = _column_order(k) + + # Build ragged rows without padding + rows = (len(plaintext) + cols - 1) // cols + grid: list[str] = [plaintext[i * cols : (i + 1) * cols] for i in range(rows)] + + # Read columns in sorted order, skipping missing cells + out: list[str] = [] + for col in order: + for r in range(rows): + if col < len(grid[r]): + out.append(grid[r][col]) + return "".join(out) + + +def decrypt(ciphertext: str, key: str) -> str: + """Decrypt columnar transposition ciphertext. + + :param ciphertext: Encrypted text + :param key: Alphabetic keyword + :return: Decrypted plaintext + :raises ValueError: on invalid key + """ + k = _normalize_key(key) + cols = len(k) + if cols == 1: + return ciphertext + + order = _column_order(k) + L = len(ciphertext) + rows = (L + cols - 1) // cols + r = L % cols + + # Column lengths based on ragged last row (no padding during encryption) + col_lengths: list[int] = [] + for c in range(cols): + if r == 0: + col_lengths.append(rows) + else: + col_lengths.append(rows if c < r else rows - 1) + + # Slice ciphertext into columns following the sorted order + columns: list[str] = [""] * cols + idx = 0 + for col in order: + ln = col_lengths[col] + columns[col] = ciphertext[idx : idx + ln] + idx += ln + + # Rebuild plaintext row-wise + out: list[str] = [] + pointers = [0] * cols + for _ in range(rows * cols): + c = len(out) % cols + if pointers[c] < len(columns[c]): + out.append(columns[c][pointers[c]]) + pointers[c] += 1 + return "".join(out) + + +if __name__ == "__main__": # pragma: no cover + import doctest + + doctest.testmod() From 5ea264ada183f62fe929b07998c078dae6f57a25 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:21:05 +0000 Subject: [PATCH 07/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ciphers/columnar_transposition.py | 1 + ciphers/skytale_cipher.py | 1 + 2 files changed, 2 insertions(+) diff --git a/ciphers/columnar_transposition.py b/ciphers/columnar_transposition.py index bddf4e7aaede..373210269cdf 100644 --- a/ciphers/columnar_transposition.py +++ b/ciphers/columnar_transposition.py @@ -26,6 +26,7 @@ ... ValueError: Key must be a non-empty alphabetic string """ + from __future__ import annotations diff --git a/ciphers/skytale_cipher.py b/ciphers/skytale_cipher.py index 57c08c522407..11fe83e5c45c 100644 --- a/ciphers/skytale_cipher.py +++ b/ciphers/skytale_cipher.py @@ -32,6 +32,7 @@ ... ValueError: Key must be a positive integer """ + from __future__ import annotations From 369788db471b4ae112d0588ecf6a08cc0e62daaf Mon Sep 17 00:00:00 2001 From: Saiprashanth Pulisetti Date: Thu, 2 Oct 2025 15:54:22 +0530 Subject: [PATCH 08/12] refactor(ciphers): improve variable naming for clarity in columnar transposition cipher --- ciphers/columnar_transposition.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ciphers/columnar_transposition.py b/ciphers/columnar_transposition.py index 373210269cdf..b016c82125e0 100644 --- a/ciphers/columnar_transposition.py +++ b/ciphers/columnar_transposition.py @@ -40,7 +40,7 @@ def _normalize_key(key: str) -> str: def _column_order(key: str) -> list[int]: # Stable sort by character then original index to handle duplicates indexed = list(enumerate(key)) - return [i for i, _ in sorted(indexed, key=lambda t: (t[1], t[0]))] + return [i for i, _ in sorted(indexed, key=lambda indexed_pair: (indexed_pair[1], indexed_pair[0]))] def encrypt(plaintext: str, key: str) -> str: @@ -85,9 +85,9 @@ def decrypt(ciphertext: str, key: str) -> str: return ciphertext order = _column_order(k) - L = len(ciphertext) - rows = (L + cols - 1) // cols - r = L % cols + text_len = len(ciphertext) + rows = (text_len + cols - 1) // cols + r = text_len % cols # Column lengths based on ragged last row (no padding during encryption) col_lengths: list[int] = [] From 3aac7a0dda2ddb159452d11050a111476dbf2ce4 Mon Sep 17 00:00:00 2001 From: Saiprashanth Pulisetti Date: Thu, 2 Oct 2025 16:09:35 +0530 Subject: [PATCH 09/12] feat(bit_manipulation): add next_higher_same_ones (SNOOB) with doctests --- bit_manipulation/next_higher_same_ones.py | 59 +++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 bit_manipulation/next_higher_same_ones.py diff --git a/bit_manipulation/next_higher_same_ones.py b/bit_manipulation/next_higher_same_ones.py new file mode 100644 index 000000000000..07d1c2966141 --- /dev/null +++ b/bit_manipulation/next_higher_same_ones.py @@ -0,0 +1,59 @@ +"""Next higher integer with the same number of set bits (SNOOB). +author: @0xPrashanthSec + +Given a non-negative integer n, return the next higher integer that has the same +number of 1 bits in its binary representation. If no such number exists within +Python's unbounded int range (practically always exists unless n is 0 or all +ones packed at the most significant end for fixed-width), this implementation +returns -1. + +This is the classic SNOOB algorithm from "Hacker's Delight". + +Reference: https://graphics.stanford.edu/~seander/bithacks.html#NextBitPermutation + +>>> next_higher_same_ones(0b0011) +5 +>>> bin(next_higher_same_ones(0b0011)) +'0b101' +>>> bin(next_higher_same_ones(0b01101)) # 13 -> 14 (0b01110) +'0b1110' +>>> next_higher_same_ones(1) +2 +>>> next_higher_same_ones(0) # no higher with same popcount +-1 +>>> next_higher_same_ones(-5) # negative not allowed +Traceback (most recent call last): + ... +ValueError: n must be a non-negative integer +""" +from __future__ import annotations + + +def next_higher_same_ones(n: int) -> int: + """Return the next higher integer with the same number of set bits as n. + + :param n: Non-negative integer + :return: Next higher integer with same popcount or -1 if none + :raises ValueError: if n < 0 + """ + if n < 0: + raise ValueError("n must be a non-negative integer") + if n == 0: + return -1 + + # snoob algorithm + # c = rightmost set bit + c = n & -n + # r = ripple carry: add c to n + r = n + c + if r == 0: + return -1 + # ones = pattern of ones that moved from lower part + ones = ((r ^ n) >> 2) // c + return r | ones + + +if __name__ == "__main__": # pragma: no cover + import doctest + + doctest.testmod() From 5b93e2bd874cfe80e96f2ac003cc8a5d913af84e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:42:00 +0000 Subject: [PATCH 10/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- bit_manipulation/next_higher_same_ones.py | 1 + ciphers/columnar_transposition.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/bit_manipulation/next_higher_same_ones.py b/bit_manipulation/next_higher_same_ones.py index 07d1c2966141..046c050f9f2d 100644 --- a/bit_manipulation/next_higher_same_ones.py +++ b/bit_manipulation/next_higher_same_ones.py @@ -26,6 +26,7 @@ ... ValueError: n must be a non-negative integer """ + from __future__ import annotations diff --git a/ciphers/columnar_transposition.py b/ciphers/columnar_transposition.py index b016c82125e0..d697db6e1176 100644 --- a/ciphers/columnar_transposition.py +++ b/ciphers/columnar_transposition.py @@ -40,7 +40,12 @@ def _normalize_key(key: str) -> str: def _column_order(key: str) -> list[int]: # Stable sort by character then original index to handle duplicates indexed = list(enumerate(key)) - return [i for i, _ in sorted(indexed, key=lambda indexed_pair: (indexed_pair[1], indexed_pair[0]))] + return [ + i + for i, _ in sorted( + indexed, key=lambda indexed_pair: (indexed_pair[1], indexed_pair[0]) + ) + ] def encrypt(plaintext: str, key: str) -> str: From 14dee0032f58a123065ba187c214eab0088e9db7 Mon Sep 17 00:00:00 2001 From: 0xPrashanthSec <40313110+0xPrashanthSec@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:14:44 +0530 Subject: [PATCH 11/12] Update next_higher_same_ones.py --- bit_manipulation/next_higher_same_ones.py | 25 +++++++++++------------ 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/bit_manipulation/next_higher_same_ones.py b/bit_manipulation/next_higher_same_ones.py index 046c050f9f2d..f4159e1412a5 100644 --- a/bit_manipulation/next_higher_same_ones.py +++ b/bit_manipulation/next_higher_same_ones.py @@ -24,33 +24,32 @@ >>> next_higher_same_ones(-5) # negative not allowed Traceback (most recent call last): ... -ValueError: n must be a non-negative integer +ValueError: input_value must be a non-negative integer """ - from __future__ import annotations -def next_higher_same_ones(n: int) -> int: - """Return the next higher integer with the same number of set bits as n. +def next_higher_same_ones(input_value: int) -> int: + """Return the next higher integer with the same number of set bits as the input. - :param n: Non-negative integer + :param input_value: Non-negative integer :return: Next higher integer with same popcount or -1 if none - :raises ValueError: if n < 0 + :raises ValueError: if input_value < 0 """ - if n < 0: - raise ValueError("n must be a non-negative integer") - if n == 0: + if input_value < 0: + raise ValueError("input_value must be a non-negative integer") + if input_value == 0: return -1 # snoob algorithm # c = rightmost set bit - c = n & -n - # r = ripple carry: add c to n - r = n + c + c = input_value & -input_value + # r = ripple carry: add c to input_value + r = input_value + c if r == 0: return -1 # ones = pattern of ones that moved from lower part - ones = ((r ^ n) >> 2) // c + ones = ((r ^ input_value) >> 2) // c return r | ones From dee6b7185ae55a72865207515c76bbb7c28fa33f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 2 Oct 2025 10:45:05 +0000 Subject: [PATCH 12/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- bit_manipulation/next_higher_same_ones.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bit_manipulation/next_higher_same_ones.py b/bit_manipulation/next_higher_same_ones.py index f4159e1412a5..b44c012ebbf9 100644 --- a/bit_manipulation/next_higher_same_ones.py +++ b/bit_manipulation/next_higher_same_ones.py @@ -26,6 +26,7 @@ ... ValueError: input_value must be a non-negative integer """ + from __future__ import annotations