Skip to content

Instantly share code, notes, and snippets.

@seanredmond
Last active January 9, 2022 18:28
Show Gist options
  • Save seanredmond/5f79d57de8b1660f1f1de0a53a00511c to your computer and use it in GitHub Desktop.
Save seanredmond/5f79d57de8b1660f1f1de0a53a00511c to your computer and use it in GitHub Desktop.
Brute force generation of all possible metonic cycles. Check historical cycles against these

Output of all_possible_metonic_cycles.py

74901: OOIOOIOOIOOIOOIOIOI
74917: OOIOOIOOIOOIOIOOIOI
75045: OOIOOIOOIOIOOIOOIOI
  • 'O' = An ordinary (12-month) year
  • 'I' = An intercalary (13-month) year

The integers are decimal representation of the cycle if it were a binary number where O = 0 and I = 1

Output of match_historical_cycles.py

This checks historical cycles against the possible cycles. Babylonians intercalated years 3, 6, 8, 11, 14, 17, and 19; Greeks (or at least possibly Greek astronomers) years 2, 5, 8, 10, 13, 16, 18.

Babylonian cycle: OOIOOIOIOOIOOIOOIOI
Babylonian cycle matches: 75045 starting at year 12
OOIOOIOOIOIOOIOOIOIOOIOOIOOIOIOOIOOIOI
           OOIOOIOIOOIOOIOOIOI

Greek cycle: OIOOIOOIOIOOIOOIOIO
Greek cycle matches: 75045 starting at year 2
OOIOOIOOIOIOOIOOIOIOOIOOIOOIOIOOIOOIOI
 OIOOIOOIOIOOIOOIOIO
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)}")
from all_possible_metonic_cycles import *
if __name__ == "__main__":
cycles = unique_cycles()
# The traditional Babyonian cycle of intercalation in year
# 3, 6, 8, 11, 14, 17, and 19 is 75045, offset by 11 years
babylon = 76_069 # 0010010100100100101
match = [d for d in [(c, is_rep(c, babylon)) for c in cycles] if d[1]][0]
print(f"Babylonian cycle: {fmt_cycle(babylon)}")
print(f"Babylonian cycle matches: {match[0]} "
f"starting at year {match[1] + 1}")
print(fmt_cycle(match[0]) * 2)
print(" " * match[1] + fmt_cycle(babylon))
# The possible Greek Metonic Cycle with intercalations in years
# 2, 5, 8, 10, 13, 16, 18 is also 75045, offset by 1 year
greece = 150_090 # 0100100101001001010
match = [d for d in [(c, is_rep(c, greece)) for c in cycles] if d[1]][0]
print()
print(f"Greek cycle: {fmt_cycle(greece)}")
print(f"Greek cycle matches: {match[0]} "
f"starting at year {match[1] + 1}")
print(fmt_cycle(match[0]) * 2)
print(" " * match[1] + fmt_cycle(greece))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment