Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions bit_manipulation/next_higher_same_ones.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""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: input_value must be a non-negative integer
"""

from __future__ import annotations


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 input_value: Non-negative integer
:return: Next higher integer with same popcount or -1 if none
:raises ValueError: if input_value < 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 = 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 ^ input_value) >> 2) // c
return r | ones


if __name__ == "__main__": # pragma: no cover
import doctest

doctest.testmod()
127 changes: 127 additions & 0 deletions ciphers/columnar_transposition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""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 indexed_pair: (indexed_pair[1], indexed_pair[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)
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] = []
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()
103 changes: 103 additions & 0 deletions ciphers/skytale_cipher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,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()