|
class RingString(str): |
|
# String class that wraps around |
|
|
|
def __new__(cls, *args, **kw): |
|
return str.__new__(cls, *args, **kw) |
|
|
|
def __getitem__(self, key): |
|
# Implements wrap-around indices |
|
# Doesn't work with negative indices |
|
|
|
# Slices: |
|
if isinstance(key, slice): |
|
|
|
# If start and stop are both past the end of the string |
|
# reduce both by 1 string length and try again |
|
if key.start and key.start >= len(self): |
|
return self.__getitem__( |
|
slice(key.start - len(self), key.stop - len(self))) |
|
|
|
# If only stop is past the end of the string, reduce stop |
|
# by one string length (wrap-around). |
|
# Take from start to the end of the string and concatenate |
|
# with the beginning of the string up to the wrapped around |
|
# stop |
|
# Or at least try again. Will recurse if stop is still past |
|
# the end of the string |
|
if key.stop and key.stop > len(self): |
|
return self[key.start:] + self[:key.stop - len(self)] |
|
|
|
# Otherwise, just return the slice |
|
return super(RingString, self).__getitem__(key) |
|
|
|
# Simple index: |
|
# Use modulo to implement wrap-around |
|
return super(RingString, self).__getitem__(key % len(self)) |
|
|
|
|
|
def criteria(c): |
|
# Check some basic criteria |
|
|
|
# Convert to a string representing c as a binary number which will |
|
# represent our candidate Metonic cycle |
|
# 0 = Ordinary year |
|
# 1 = Intercalary year |
|
# |
|
# Double it so we can check across the boundary where it repeats |
|
cycled = f"{c:019b}" * 2 |
|
|
|
# We cant have more than 2 ordinary years in a row, so reject any |
|
# strings that have 3 0's in a row |
|
if "000" in cycled: |
|
return False |
|
|
|
# We can't have two intercalary years in a row, so reject any |
|
# strings that have 2 1's in a row |
|
if "11" in cycled: |
|
return False |
|
|
|
# We only want 7 intercalary years in each cycle. Since we doubled |
|
# the string we want 14, so reject any that don't have exactly 14 1's |
|
if cycled.count("1") != 7 * 2: |
|
return False |
|
|
|
return True |
|
|
|
|
|
def is_rep(a, b): |
|
# Check whether b is equivalent to a if a is a repeating sequence |
|
a_str = RingString(f"{a:019b}") |
|
b_str = f"{b:019b}" |
|
|
|
# We know both strings are 19 characters long, so we just need to |
|
# "turn the dial" 19 times to see if b matches any |
|
# 19-character-long sequence in repeating sequence of a |
|
for i in range(0, 19): |
|
if b_str == a_str[i:i+19]: |
|
return i |
|
|
|
return False |
|
|
|
|
|
def find_unique(u, c): |
|
# Take the first item of the list c and find all other items that |
|
# are not equivalent repetitions, just starting from a different |
|
# position. |
|
# |
|
# That is, if the first is treated as a ring, ignore all others |
|
# that are the same ring but only "turned" one or more positions. |
|
# |
|
# Then recurse, and return the list of all items that had no equivalents |
|
|
|
if not c: |
|
# We're done |
|
return u |
|
|
|
# Take the first item |
|
c1 = c[0] |
|
|
|
# Find all other items that are not equivalent |
|
c2 = [d for d in c if is_rep(c1, d) is False] |
|
|
|
# Add c1 to the list of uniques and recurse |
|
# On the next run, c1 will be the first item in c2 that wasn't equivalent |
|
# to this c1 |
|
return find_unique(u + [c1], c2) |
|
|
|
|
|
def unique_cycles(): |
|
return find_unique([], [i for i in range(0, 524_287 + 1) if criteria(i)]) |
|
|
|
|
|
def fmt_cycle(c): |
|
return f"{c:019b}".replace("0", "O").replace("1", "I") |
|
|
|
|
|
if __name__ == "__main__": |
|
# iterate from 0 to 524,287 (0b1111111111111111111 in decimal). |
|
# |
|
# From this we generate every bit pattern from |
|
# 000000000000000000 to 1111111111111111111 |
|
# |
|
# This is every possible Metonic cycle (0 = ordinary, 1 = intercalary) |
|
# including impossible ones (like 19 ordinary or 19 intercalary years) |
|
|
|
# There will be 57 cycles that meet the basic criteria which |
|
# represent repetitions of 3 unique cycles |
|
|
|
cycles = unique_cycles() |
|
|
|
for c in cycles: |
|
print(f"{c}: {fmt_cycle(c)}") |