Skip to content

Instantly share code, notes, and snippets.

@huntfx
Last active January 11, 2021 00:26
Show Gist options
  • Save huntfx/42232c3ac5ef3b3257861864de2d68db to your computer and use it in GitHub Desktop.
Save huntfx/42232c3ac5ef3b3257861864de2d68db to your computer and use it in GitHub Desktop.
Convert numbers to and from the Cistercian numeral system
"""Convert numbers to the Cistercian monk numeral system.
https://en.wikipedia.org/wiki/The_Ciphers_of_the_Monks
"""
import numpy as np
def horizontal_reverse(n):
"""Horizontally reverse a number index."""
row = n // 5 + 1
row_max = row * 5
return row_max - n % 5 - 1
def vertical_reverse(n):
"""Vertically reverse a number index."""
return 24 - n
def build_mapping():
"""Create an index mapping for the numbers."""
base = {0: [2, 7, 12, 17, 22], 1: [3, 4], 2: [8, 9], 3: [3, 9], 4: [4, 8], 6: [4, 9]}
result = base.copy()
# Mirror symbols
for k, v in base.items():
result[k * 10] = [horizontal_reverse(i) for i in v]
result[k * 100] = [horizontal_reverse(vertical_reverse(i)) for i in v]
result[k * 1000] = [vertical_reverse(i) for i in v]
# Build remaining numbers
for i in (10 ** i for i in range(4)):
result.update({
5*i: result[1*i] + result[4*i],
7*i: result[1*i] + result[6*i],
8*i: result[2*i] + result[6*i],
9*i: result[1*i] + result[2*i] + result[6*i],
})
return result
def int_to_cistercian(num):
"""Convert an interger to a cistercian number.
>>> int_to_cistercian(4567)
x xxx
x x x
x
xxx
x xxx
"""
# Validation
if not 0 <= num <= 9999:
raise ValueError('number out of range')
# Calculation
array = np.array([False] * 25)
while True:
for amount, indexes in sorted(MAPPING.items(), reverse=True):
if amount <= num:
np.put(array, indexes, True)
# Convert to ascii
if not amount:
return '\n'.join(
''.join('x' if cell else ' ' for cell in row)
for row in array.reshape(5, 5)
)
num -= amount
break
def cistercian_to_int(ascii):
"""Convert the ascii cistercian number back to an integer.
>>> ascii = [
... 'x xxx',
... 'x x x',
... ' x ',
... ' xxx ',
... 'x xxx',
... ]
>>> cistercian_to_int('\n'.join(ascii))
4567
"""
ascii = ascii.strip('\n')
# Validation
lines = ascii.count('\n')
size = len(ascii) - lines
if ascii.count('\n') != 4:
raise ValueError(f'invalid row count (need 4, got {lines}')
if size != 25:
# Attempt to fill in deleted whitespace
if size < 25:
ascii = '\n'.join(line + ' ' * (5 - len(line)) for line in ascii.split('\n'))
size = len(ascii) - lines
if size != 25:
raise ValueError(f'invalid data length (need 25, got {size}')
# Calculation
array = np.array([c == 'x' for c in ascii.replace('\n', '')])
max_val = 9999
numbers = []
for num, indexes in sorted(MAPPING.items(), reverse=True):
if max_val > num and all(array[i] for i in indexes):
numbers.append(num)
max_val = int('1' + str(num)[1:])
result = sum(numbers)
# It's possible for invalid results to return numbers, so do a check
if int_to_cistercian(result) != ascii:
raise ValueError('invalid cistercian number')
return result
MAPPING = build_mapping()
# Check it works
if __name__ == '__main__':
for i in range(10000):
c = int_to_cistercian(i)
assert i == cistercian_to_int(c), i
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment