-
Notifications
You must be signed in to change notification settings - Fork 0
/
ocal.py
261 lines (213 loc) · 8.18 KB
/
ocal.py
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
#! /usr/binn/env python3
import jdcal
GREGORIAN = 1
JULIAN = 2
dows = ('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat')
class ocal(object):
"""
ocal -- Orthodox calendar management class
The main purpose of this class is to provide conversion between various date
formats. The impetus is the algorithm for computing Pascha (which this class
does not do). So the goal is whatever methods are needed to "give me the sunday
after the Nth day after March 21st of this year".
To that end, it includes methods for initializing dates (using both Gregorian
and Julian calendar dates), adding and subtracting days, getting the next or
previous <day of week>. Finally, it can return a date as a Gregorian or Julian
year,month,day tuple or as a formatted string (either the Gregorian, Julian,
or a combination of the two).
Care must be taken to avoid confusing the Julian calendar with Julian dates.
The Julian calendar is a calendar system where every 4 years is a leap year.
Julian dates are a way of counting days since an epoch (<julian> January 1
4713 BC or <gregorian> Nov 17, 1858).
This class will use the jdcal package of functions to provide some of the heavy
lifting. All days stored will be in MJD (possibly negative, if before 1858).
jdcal functions often return two values: jdcal.MJD_0 (2400000.5) and the
modified julian date. I'll just always pass that + jdcal.MJD_0.
"""
@classmethod
def gregorian(cls, year, month, day):
"""Given year,month,day, create an ocal instance according to the
Gregorian calendar."""
return cls(year=year, month=month, day=day, calendar=GREGORIAN)
@classmethod
def julian(cls, year, month, day):
"""Given year,month,day, create an ocal instance
according to the Julian calendar."""
return cls(year=year, month=month, day=day, calendar=JULIAN)
@classmethod
def mj_date(cls, date):
"Create an ocal instance with date as a modified julian date."
return cls(date=date)
@classmethod
def today(cls, midnight=24):
"""Return today. If midnight is set to something other than 24 (eg 18),
hours after that are considered the next day"""
import time
t = time.time()
lt = time.localtime(t)
if lt.tm_hour >= midnight:
t += 86400
lt = time.localtime(t)
return cls(year=lt.tm_year, month=lt.tm_mon, day=lt.tm_mday)
def __init__(self, **kw):
"""Initialize.
Ideally called with constructor methods. Optional parameters include:
* year, month, day: year, month, day according to the calendar system.
* date: modified julian date
* gregorian: if true (the default), use the Gregorian calendar to
* convert year,month,day to a julian date.julian: if true, use the
* Julian calendar to convert year,month,day to a julian date.
"""
if 'calendar' in kw:
cal = kw['calendar']
else:
# default calendar: Gregorian
cal = GREGORIAN
if 'date' in kw:
self.date = kw['date']
else:
y = kw['year']
m = kw['month']
d = kw['day']
if cal == GREGORIAN:
self.date = int(jdcal.gcal2jd(y, m, d)[1])
elif cal == JULIAN:
self.date = int(jdcal.jcal2jd(y, m, d)[1])
else:
raise ValueError("Unknown calendar:{}".format(cal))
self.calendar = cal
self.sync_ymd()
def sync_ymd(self):
def setymdat(p, ymd):
for ii, xx in enumerate(("year", "month", "day")):
setattr(self, p+xx, ymd[ii])
# setattr(self, p+'year', ymd[0])
# setattr(self, p+'month', ymd[1])
# setattr(self, p+'day', ymd[2])
setymdat('g', self.get_ymd_g())
setymdat('j', self.get_ymd_j())
if self.calendar == GREGORIAN:
ymd = self.get_ymd_g()
else:
ymd = self.get_ymd_j()
setymdat('', ymd)
setattr(self, 'dow', self.get_dow())
def get_date(self):
"Return the modified julian date"
return self.date
def get_ymd_g(self):
"""return the year, month, and day of the instance, according to the
Gregorian calendar"""
return jdcal.jd2gcal(jdcal.MJD_0, self.date)[:3]
def get_ymd_j(self):
"""return the year, month, and day of the instance, according to the
Julian calendar"""
return jdcal.jd2jcal(jdcal.MJD_0, self.date)[:3]
def __repr__(self):
# 8:-2: change "<class 'foo'>" to "foo"
if self.calendar == GREGORIAN:
return "{}.gregorian({}, {}, {})".format(
repr(self.__class__)[8:-2], *self.get_ymd_g())
else:
return "{}.julian({}, {}, {})".format(
repr(self.__class__)[8:-2], *self.get_ymd_j())
def get_dow(self):
"return the day of week. 0: Sunday, 6: Saturday"
# the MJD starts on a Wednesday. Offset it from Sunday so modulo works.
return (self.date+3) % 7
# The above are all the gazintas/gazattas
def add_days(self, ndays):
"add (or subtract if negative) ndays to the date"
self.date += ndays
self.sync_ymd()
def __cmp__(self, oocal):
try:
return self.date - oocal.date
except AttributeError:
return NotImplemented
def __lt__(self, oocal):
try:
return self.date < oocal.date
except AttributeError:
return NotImplemented
def __le__(self, oocal):
try:
return self.date <= oocal.date
except AttributeError:
return NotImplemented
def __eq__(self, oocal):
try:
return self.date == oocal.date
except AttributeError:
return NotImplemented
def __ne__(self, oocal):
try:
return self.date != oocal.date
except AttributeError:
return NotImplemented
def __ge__(self, oocal):
try:
return self.date >= oocal.date
except AttributeError:
return NotImplemented
def __gt__(self, oocal):
try:
return self.date > oocal.date
except AttributeError:
return NotImplemented
def __iadd__(self, n):
self.add_days(n)
return self
def __isub__(self, n):
self.add_days(-n)
return self
def __add__(self, ndays):
return ocal(date=self.date + ndays, calendar=self.calendar)
# o could be int (get date n days ago), or ocal (diff between 2 ocals)
def __sub__(self, o):
if hasattr(o, 'date'):
return int(self.date - o.date)
else:
return ocal(date=self.date - o, calendar=self.calendar)
def next_dow(self, nweeks, dow, offset=0):
"""advance the date to the next given day of week (dow. 0==Sunday)
nweeks is number of weeks to advance (-1 means previous day of week)
Offset is added to date before the calculation.
d=ocal.gregorian(year, 11, 1)
d.next_dow(1, 1, offset=-1) # so if Nov 1 is Monday, doesn't change
d.next_dow(1, 2)
"""
if nweeks > 0:
self.date += 1
self.date += offset
self.date = self.date + (dow - self.get_dow()) % 7
self.date += (nweeks-1)*7
elif nweeks < 0:
self.date -= 1
self.date += offset
self.date = self.date - (self.get_dow()-dow) % 7
self.date += (nweeks+1)*7
else:
raise ValueError("nweeks of 0 makes no sense")
self.sync_ymd()
def gregorian(*a, **k):
return ocal.gregorian(*a, **k)
def julian(*a, **k):
return ocal.julian(*a, **k)
def today(*a, **k):
return ocal.today(*a, **k)
# paschacache = {}
def pascha(year):
# if year in paschacache:
# return paschacache[year]
pdate = ocal.julian(year, 3, 21)
offset = ((year-1) % 19) + 1
offset = (offset * 19) + 15
offset = offset % 30
# print("Offset is {}".format(offset))
pdate.add_days(offset)
# print("pdate full moon on day:{}".format(pdate.get_dow()))
pdate.next_dow(1, 0)
# print("pdate:{}".format(pdate.get_ymd_g()))
# paschacache[year] = pdate
return pdate