-
Notifications
You must be signed in to change notification settings - Fork 19
/
convert.py
156 lines (122 loc) · 5.58 KB
/
convert.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
from datetime import date, datetime
from time import struct_time
from edtf import jdutil
TIME_EMPTY_TIME = [0, 0, 0] # tm_hour, tm_min, tm_sec
TIME_EMPTY_EXTRAS = [0, 0, -1] # tm_wday, tm_yday, tm_isdst
def old_specs_to_new_specs_expression(expression):
expression = expression.replace("unknown", "")
expression = expression.replace("open", "..")
expression = expression.replace("u", "X")
expression = expression.replace("x", "X")
expression = expression.replace("?~", "%")
expression = expression.replace("~?", "%")
expression = expression.replace("e", "E")
expression = expression.replace("y", "Y")
expression = expression.replace("p", "S")
return expression
def dt_to_struct_time(dt):
"""
Convert a `datetime.date` or `datetime.datetime` to a `struct_time`
representation *with zero values* for data fields that we cannot always
rely on for ancient or far-future dates: tm_wday, tm_yday, tm_isdst
NOTE: If it wasn't for the requirement that the extra fields are unset
we could use the `timetuple()` method instead of this function.
"""
if isinstance(dt, datetime):
return struct_time(
[dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second]
+ TIME_EMPTY_EXTRAS
)
elif isinstance(dt, date):
return struct_time(
[dt.year, dt.month, dt.day] + TIME_EMPTY_TIME + TIME_EMPTY_EXTRAS
)
else:
raise NotImplementedError(f"Cannot convert {type(dt)} to `struct_time`")
def struct_time_to_date(st):
"""
Return a `datetime.date` representing the provided `struct_time.
WARNING: This will fail for dates with years before 1 AD or after 9999 AD.
"""
return date(*st[:3])
def struct_time_to_datetime(st):
"""
Return a `datetime.datetime` representing the provided `struct_time.
WARNING: This will fail for dates with years before 1 AD or after 9999 AD.
"""
return datetime(*st[:6])
def trim_struct_time(st, strip_time=False):
"""
Return a `struct_time` based on the one provided but with the extra fields
`tm_wday`, `tm_yday`, and `tm_isdst` reset to default values.
If `strip_time` is set to true the time value are also set to zero:
`tm_hour`, `tm_min`, and `tm_sec`.
"""
if strip_time:
return struct_time(list(st[:3]) + TIME_EMPTY_TIME + TIME_EMPTY_EXTRAS)
else:
return struct_time(list(st[:6]) + TIME_EMPTY_EXTRAS)
def struct_time_to_jd(st):
"""
Return a float number representing the Julian Date for the given
`struct_time`.
NOTE: extra fields `tm_wday`, `tm_yday`, and `tm_isdst` are ignored.
"""
year, month, day = st[:3]
hours, minutes, seconds = st[3:6]
# Convert time of day to fraction of day
day += jdutil.hmsm_to_days(hours, minutes, seconds)
return jdutil.date_to_jd(year, month, day)
def jd_to_struct_time(jd):
"""
Return a `struct_time` converted from a Julian Date float number.
WARNING: Conversion to then from Julian Date value to `struct_time` can be
inaccurate and lose or gain time, especially for BC (negative) years.
NOTE: extra fields `tm_wday`, `tm_yday`, and `tm_isdst` are set to default
values, not real ones.
"""
year, month, day = jdutil.jd_to_date(jd)
# Convert time of day from fraction of day
day_fraction = day - int(day)
hour, minute, second, ms = jdutil.days_to_hmsm(day_fraction)
day = int(day)
# This conversion can return negative values for items we do not want to be
# negative: month, day, hour, minute, second.
year, month, day, hour, minute, second = _roll_negative_time_fields(
year, month, day, hour, minute, second
)
return struct_time([year, month, day, hour, minute, second] + TIME_EMPTY_EXTRAS)
def _roll_negative_time_fields(year, month, day, hour, minute, second):
"""
Fix date/time fields which have nonsense negative values for any field
except for year by rolling the overall date/time value backwards, treating
negative values as relative offsets of the next higher unit.
For example minute=5, second=-63 becomes minute=3, second=57 (5 minutes
less 63 seconds)
This is very unsophisticated handling of negative values which we would
ideally do with `dateutil.relativedelta` but cannot because that class does
not support arbitrary dates, especially not negative years which is the
only case where these nonsense values are likely to occur anyway.
NOTE: To greatly simplify the logic we assume all months are 30 days long.
"""
if second < 0:
minute += int(second / 60.0) # Adjust by whole minute in secs
minute -= 1 # Subtract 1 for negative second
second %= 60 # Convert negative second to positive remainder
if minute < 0:
hour += int(minute / 60.0) # Adjust by whole hour in minutes
hour -= 1 # Subtract 1 for negative minutes
minute %= 60 # Convert negative minute to positive remainder
if hour < 0:
day += int(hour / 24.0) # Adjust by whole day in hours
day -= 1 # Subtract 1 for negative minutes
hour %= 24 # Convert negative hour to positive remainder
if day < 0:
month += int(day / 30.0) # Adjust by whole month in days (assume 30)
month -= 1 # Subtract 1 for negative minutes
day %= 30 # Convert negative day to positive remainder
if month < 0:
year += int(month / 12.0) # Adjust by whole year in months
year -= 1 # Subtract 1 for negative minutes
month %= 12 # Convert negative month to positive remainder
return (year, month, day, hour, minute, second)