/
bcdate_util.py
226 lines (170 loc) · 6.44 KB
/
bcdate_util.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
import re
from datetime import datetime, timedelta
from PyQt4.QtCore import QDateTime
import PyQt4.QtCore as QtCore
from logging import warn
import conf
""" A module to support dates of BC/AD form"""
__author__ = "Karolina Alexiou"
__email__ = "karolina.alexiou@teralytics.ch"
class CustomDate(object):
pass
_DIGITS = conf.DEFAULT_DIGITS # default digits for archaeology mode
def getGlobalDigitSetting():
return _DIGITS
def setGlobalDigitSetting(digits):
global _DIGITS
_DIGITS = digits
class ZeroFormatException(Exception):
pass
def get_max_dt():
return BCDate((10 ** getGlobalDigitSetting()) - 1)
def get_min_dt():
return BCDate(-1 * ((10 ** getGlobalDigitSetting()) - 1))
class BCDate(CustomDate):
def __init__(self, y, m=1, d=1):
self.digits = getGlobalDigitSetting()
self.y = y
self.m = m
self.d = d
def setDigits(self, d):
self.d = d
def isBC(self):
return self.y < 0
def __cmp__(self, other):
if not isinstance(other, self.__class__):
return -1
if self.__lt__(other):
return -1
if self.__eq__(other):
return 0
return 1
def __lt__(self, other):
if not isinstance(other, self.__class__):
return True
if (self.y < other.y or (self.y == other.y and self.m < other.m) or (
self.y == other.y and self.d == other.d and self.d < other.d)):
return True
return False
def __str__(self):
year = self.y
if year >= 0:
return str(year).zfill(self.digits) + " AD"
else:
return str(-year).zfill(self.digits) + " BC"
def __repr__(self):
return self.__str__()
def as_datetime(self):
return datetime(self.y, self.m, self.d)
@classmethod
def from_str(cls, bc, strict_zeros=True):
try:
m = re.match("(\d*)\s(AD|BC)", bc)
year_str = m.group(1)
if strict_zeros and len(year_str) != getGlobalDigitSetting():
raise ZeroFormatException(
"{} is an invalid date. Need a date string with exactly {} digits, for example {}"
.format(bc, getGlobalDigitSetting(),
"22".zfill(getGlobalDigitSetting()) + " BC"))
y = int(year_str)
bc = m.group(2)
if bc == "BC":
y = -y
if bc not in ("BC", "AD") or y == 0:
raise Exception
return BCDate(y)
except ZeroFormatException, z:
raise z
except Exception, e:
raise Exception(
"{} is an invalid archaelogical date, should be 'number AD' or 'number BC'".format(
bc) +
"and year 0 (use {} AD or BC instead) doesn't exist.".format(
"1".zfill(getGlobalDigitSetting())))
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.y == other.y and self.m == other.m and self.d == other.d
else:
return False
def _get_years_from_timedelta(self, td):
try:
return td.years
except Exception, e:
# FIXME v.1.7 what about offset?
msg = "BC dates can only be used with year intervals, found {}".format(td)
warn(msg)
return 0
@classmethod
def dist(cls, bc1, bc2):
if (bc1.y * bc2.y > 0):
return bc1.y - bc2.y
else:
return bc1.y - bc2.y - 1
def __isub__(self, td):
return self._iadd__(td * -1)
def __sub__(self, td):
return self.__add__(td * -1)
def __iadd__(self, td):
if isinstance(td, BCDate):
to_add = td.y
else: # some sort of timedelta/relativedelta
to_add = self._get_years_from_timedelta(td)
self.y = self._get_new_year_value(self.y, to_add)
return self
def _get_new_year_value(self, old_y, to_add):
"""Adjust for absence of year zero"""
new_y = old_y + to_add
if (new_y == 0 or new_y * old_y < 0) and new_y < old_y:
# if we went from AD to BC substract one for non-existant year 0
new_y -= 1
elif (new_y == 0 or new_y * old_y < 0) and new_y > old_y:
# if we went from BC to AD add one for non-existant year 0
new_y += 1
return new_y
def __add__(self, td):
if isinstance(td, BCDate):
to_add = td.y
else: # some sort of timedelta/relativedelta
to_add = self._get_years_from_timedelta(td)
return BCDate(self._get_new_year_value(self.y, to_add))
def __hash__(self):
return (self.y << 10) + (self.m << 4) + self.d
BC_FORMAT = "Y with BC/AD"
SECONDS_IN_YEAR = 60 * 60 * 24 * 365
def _year(fdate):
return int(fdate.y)
def timeval_to_epoch(val):
if isinstance(val, str) or isinstance(val, unicode):
return bcdate_to_epoch(str_to_bcdate(val))
if isinstance(val, BCDate):
return bcdate_to_epoch(val)
def timeval_to_bcdate(val):
epoch = timeval_to_epoch(val)
return epoch_to_bcdate(epoch)
def epoch_to_bcdate(seconds_from_epoch):
"""Convert seconds since 1970-1-1 (UNIX epoch) to a FlexiDate object"""
if seconds_from_epoch >= YEAR_ONE_EPOCH:
dt = datetime(1970, 1, 1) + timedelta(seconds=seconds_from_epoch)
return BCDate(dt.year, dt.month, dt.day)
else:
seconds_bc = abs(seconds_from_epoch - YEAR_ONE_EPOCH)
years_bc = seconds_bc / SECONDS_IN_YEAR
years_bc += 1 # for year 0
return BCDate.from_str(str(years_bc).zfill(getGlobalDigitSetting()) + " BC")
def bcdate_to_epoch(fd):
""" convert a FlexiDate to seconds after (or possibly before) 1970-1-1 """
if _year(fd) > 0:
res = (fd.as_datetime() - datetime(1970, 1, 1)).total_seconds()
else:
part1 = (datetime(1, 1, 1) - datetime(1970, 1, 1)).total_seconds()
# coarsely get the epoch time for time BC, people didn't have a normal
# calendar back then anyway
part2 = - (abs(_year(fd)) - 1) * SECONDS_IN_YEAR # -1 because year 0 doesn't exist
res = part1 + part2
return int(res)
YEAR_ONE_EPOCH = bcdate_to_epoch(BCDate(1))
def str_to_bcdate(datetimeString):
"""convert a date/time string into a FlexiDate object"""
return BCDate.from_str(datetimeString)
def epoch_to_str(seconds_from_epoch):
return datetime_to_str(epoch_to_datetime(seconds_from_epoch))