1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
|
"""
Provides date conversion functions, HistDate, and date scales.
"""
# ========== For conversion between calendars and Julian day numbers. ==========
# Algorithms were obtained from:
# https://en.wikipedia.org/wiki/Julian_day#Converting_Gregorian_calendar_date_to_Julian_Day_Number.
def gregorianToJdn(year: int, month: int, day: int) -> int:
"""
Converts a Gregorian calendar date to a Julian day number,
denoting the noon-to-noon 'Julian day' that starts within the input day.
A year of 1 means 1 AD, and -1 means 1 BC (0 is treated like -1).
A month of 1 means January. Can use a month of 13 and a day of 0.
Valid for dates from 24th Nov 4714 BC onwards.
"""
if year < 0:
year += 1
x = int((month - 14) / 12)
jdn = int(1461 * (year + 4800 + x) / 4)
jdn += int((367 * (month - 2 - 12 * x)) / 12)
jdn -= int((3 * int((year + 4900 + x) / 100)) / 4)
jdn += day - 32075
return jdn
def julianToJdn(year: int, month: int, day: int) -> int:
"""
Like gregorianToJdn(), but converts a Julian calendar date.
Valid for dates from 1st Jan 4713 BC onwards.
"""
if year < 0:
year += 1
jdn = 367 * year
jdn -= int(7 * (year + 5001 + int((month - 9) / 7)) / 4)
jdn += int(275 * month / 9)
jdn += day + 1729777
return jdn
def jdnToGregorian(jdn: int) -> tuple[int, int, int]:
"""
Converts a Julian day number to a Gregorian calendar date, denoting the
day in which the given noon-to-noon 'Julian day' begins.
Valid for non-negative input.
"""
f = jdn + 1401 + (((4 * jdn + 274277) // 146097) * 3) // 4 - 38
e = 4 * f + 3
g = (e % 1461) // 4
h = 5 * g + 2
D = (h % 153) // 5 + 1
M = (h // 153 + 2) % 12 + 1
Y = (e // 1461) - 4716 + (12 + 2 - M) // 12
if Y <= 0:
Y -= 1
return Y, M, D
def jdnToJulian(jdn: int) -> tuple[int, int, int]:
""" Like jdnToGregorian(), but converts to a Julian calendar date """
f = jdn + 1401
e = 4 * f + 3
g = (e % 1461) // 4
h = 5 * g + 2
D = (h % 153) // 5 + 1
M = (h // 153 + 2) % 12 + 1
Y = (e // 1461) - 4716 + (12 + 2 - M) // 12
if Y <= 0:
Y -= 1
return Y, M, D
def julianToGregorian(year: int, month: int, day: int) -> tuple[int, int, int]:
return jdnToGregorian(julianToJdn(year, month, day))
def gregorianToJulian(year: int, month: int, day: int) -> tuple[int, int, int]:
return jdnToJulian(gregorianToJdn(year, month, day))
# ========== For date representation ==========
MIN_CAL_YEAR = -4713 # Year before which JDNs are not usable
MONTH_SCALE = -1;
DAY_SCALE = -2;
SCALES: list[int] = [int(s) for s in [1e9, 1e8, 1e7, 1e6, 1e5, 1e4, 1e3, 100, 10, 1, MONTH_SCALE, DAY_SCALE]];
# The timeline is divided into units of SCALES[0], then SCALES[1], etc
# Positive ints represent numbers of years, -1 represents 1 month, -2 represents 1 day
if __debug__: # Validate SCALES
if SCALES[-1] != DAY_SCALE or SCALES[-2] != MONTH_SCALE or SCALES[-3] != 1:
raise Exception('SCALES must end with [1, MONTH_SCALE, DAY_SCALE]');
for i in range(1, len(SCALES) - 2):
if SCALES[i] <= 0:
raise Exception('SCALES must only have positive ints before MONTH_SCALE')
if SCALES[i-1] <= SCALES[i]:
raise Exception('SCALES must hold decreasing values')
if SCALES[i-1] % SCALES[i] > 0:
raise Exception('Each positive int in SCALES must divide the previous int')
class HistDate:
"""
Represents a historical date
- 'year' may be negative (-1 means 1 BC)
- 'month' and 'day' are at least 1, if given
- 'gcal' may be:
- True: Indicates a Gregorian calendar date
- False: Indicates a Julian calendar date
- None: 'month' and 'day' are 1 (required for dates before MIN_CAL_YEAR)
"""
# Note: Intentionally not enforcing, for gcal=None, that year < MIN_CAL_YEAR
def __init__(self, gcal: bool | None, year: int, month=1, day=1):
self.gcal = gcal
self.year = year
self.month = month
self.day = day
def __eq__(self, other): # Used in unit testing
return isinstance(other, HistDate) and \
(self.gcal, self.year, self.month, self.day) == (other.gcal, other.year, other.month, other.day)
def __repr__(self): # Used in unit testing
return str(self.__dict__)
def dbDateToHistDate(n: int, fmt: int, end=False) -> HistDate:
""" Converts a start/start_upper/etc and fmt value in the 'events' db table, into a HistDate """
if fmt == 0: # year
return HistDate(None, n)
elif fmt == 1 or fmt == 3 and end: # jdn for gregorian calendar
return HistDate(True, *jdnToGregorian(n))
else: # fmt == 2 or fmt == 3 and not end
return HistDate(False, *jdnToJulian(n))
def dateToUnit(date: HistDate, scale: int) -> int:
""" Converts a date to an int representing a unit on a scale """
if scale >= 1:
return date.year // scale
elif scale == MONTH_SCALE:
if date.gcal == False:
return julianToJdn(date.year, date.month, 1)
else: # True or None
return gregorianToJdn(date.year, date.month, 1)
else: # scale == DAY_SCALE
if date.gcal == False:
return julianToJdn(date.year, date.month, date.day)
else:
return gregorianToJdn(date.year, date.month, date.day)
|