Skip to content

Commit 6dacbeb

Browse files
committed
Add Timestamp module + supporting modules and tests
1 parent 11e5e28 commit 6dacbeb

11 files changed

+2628
-0
lines changed

docs/intro_2.md

+31
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,37 @@ python semantics is its treament of integers. For performance and memory reasons
6666
this won't be a problem, but if you attempt to place an integer larger than 64 bits into a
6767
`typed_python` container, you'll see the integer get cast down to 64 bits.
6868

69+
### Timestamp
70+
71+
`typed_python` provides the Timestamp type that wraps useful datetime functionality around a
72+
unix timestamp.
73+
74+
For e.g, you can create a Timestamp from a unixtime with the following:
75+
76+
```
77+
ts1 = Timestamp.make(1654615145)
78+
ts2 = Timestamp(ts=1654615145)
79+
```
80+
81+
You can also create Timestamps from datestrings. The parser supports ISO 8601 along with variety
82+
of non-iso formats. E.g:
83+
```
84+
ts1 = Timestamp.parse("2022-01-05T10:11:12+00:15")
85+
ts2 = Timestamp.parse("2022-01-05T10:11:12NYC")
86+
ts3 = Timestamp.parse("January 1, 2022")
87+
ts4 = Timestamp.parse("January/1/2022")
88+
ts5 = Timestamp.parse("Jan-1-2022")
89+
```
90+
91+
You can format Timestamps as strings using standard time format directives. E.g:
92+
93+
```
94+
timestamp = Timestamp.make(1654615145)
95+
print(timestamp.format(utc_offset=144000)) # 2022-06-09T07:19:05
96+
print(timestamp.format(format="%Y-%m-%d")) # 2022-06-09
97+
```
98+
99+
69100
### Object
70101

71102
In some cases, you may have types that need to hold regular python objects. For these cases, you may

typed_python/lib/datetime/chrono.py

+237
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
# Copyright 2017-2020 typed_python Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typed_python import Entrypoint
16+
17+
# This file implements some useful low level algorithms for processing dates and times.
18+
# Many of the algorithms are described here https://howardhinnant.github.io/date_algorithms.html
19+
20+
21+
@Entrypoint
22+
def days_from_civil(year: int = 0, month: int = 0, day: int = 0) -> int:
23+
'''
24+
Creates a unix timestamp from date values.
25+
Parameters:
26+
year (int): The year
27+
month (int): The month. January: 1, February: 2, ....
28+
day (int): The day
29+
Returns:
30+
seconds(float): The number of seconds
31+
32+
Implements the low level days_from_civil algorithm
33+
'''
34+
year -= month <= 2
35+
era = (year if year >= 0 else year - 399) // 400
36+
yoe = (year - era * 400)
37+
doy = (153 * ( month - 3 if month > 2 else month + 9) + 2) // 5 + day - 1
38+
doe = yoe * 365 + yoe // 4 - yoe // 100 + doy
39+
days = era * 146097 + doe - 719468
40+
41+
return days
42+
43+
44+
@Entrypoint
45+
def date_to_seconds(year: int = 0, month: int = 0, day: int = 0) -> float:
46+
'''
47+
Creates a unix timestamp from date values.
48+
Parameters:
49+
year (int): The year
50+
month (int): The month. January: 1, February: 2, ....
51+
day (int): The day
52+
Returns:
53+
seconds(float): The number of seconds
54+
55+
'''
56+
return days_from_civil(year, month, day) * 86400
57+
58+
59+
@Entrypoint
60+
def time_to_seconds(hour: int = 0, minute: int = 0, second: float = 0) -> float:
61+
'''
62+
Converts and hour, min, second combination into seconds
63+
Parameters:
64+
hour (int): The hour (0-23)
65+
minute (int): The minute
66+
second (int): The second
67+
Returns:
68+
(float) the number of seconds
69+
'''
70+
return (hour * 3600) + (minute * 60) + second
71+
72+
73+
@Entrypoint
74+
def weekday_difference(x: int, y: int) -> int:
75+
'''
76+
Gets the difference in days between two weekdays
77+
Parameters:
78+
x (int): The first day
79+
y (int): The second day
80+
81+
Returns:
82+
(int) the difference between the two weekdays
83+
'''
84+
x -= y
85+
return x if x >= 0 and x <= 6 else x + 7
86+
87+
88+
@Entrypoint
89+
def weekday_from_days(z: int) -> int:
90+
'''
91+
Gets the day of week given the number of days from the unix epoch
92+
Parameters:
93+
z (int): The number of days from the epoch
94+
95+
Returns:
96+
(int) the weekday (0-6)
97+
'''
98+
return (z + 4) % 7 if z >= -4 else (z + 5) % 7 + 6
99+
100+
101+
@Entrypoint
102+
def get_nth_dow_of_month_unixtime(n: int, wd: int, month: int, year: int) -> int:
103+
'''
104+
Gets the date of the nth day of the month for a given year. E.g. get 2nd Sat in July 2022
105+
Parameters:
106+
n (int): nth day of week (1-4).
107+
wd (int): the weekday (0-6) where 0 => Sunday
108+
month (int): the month (1-12)
109+
year (int): the year
110+
111+
Returns:
112+
(int): The nth day of the month in unixtime
113+
'''
114+
if n < 1 or n > 4:
115+
raise ValueError('n should be 1-4')
116+
if wd < 0 or wd > 6:
117+
raise ValueError('wd should be 0-6')
118+
if month < 1 or month > 12:
119+
raise ValueError('invalid month')
120+
121+
wd_1st = weekday_from_days(days_from_civil(year, month, 1))
122+
123+
return date_to_seconds(year=year,
124+
month=month,
125+
day=weekday_difference(wd, wd_1st) + 1 + (n - 1) * 7)
126+
127+
128+
@Entrypoint
129+
def get_year_from_unixtime(ts: float) -> int:
130+
'''
131+
Gets the year from a unixtime
132+
Parameters:
133+
ts (float): the unix timestamp
134+
Returns:
135+
(int): The year
136+
'''
137+
z = int(ts // 86400 + 719468)
138+
era = (z if z >= 0 else z - 146096) // 146097
139+
doe = z - era * 146097
140+
yoe = (doe - (doe // 1460) + (doe // 36524) - (doe // 146096)) // 365
141+
y = yoe + era * 400
142+
doy = doe - ((365 * yoe) + (yoe // 4) - (yoe // 100))
143+
mp = (5 * doy + 2) // 153
144+
m = mp + (3 if mp < 10 else -9)
145+
y += (m <= 2)
146+
return y
147+
148+
149+
@Entrypoint
150+
def is_leap_year(year: int):
151+
'''
152+
Tests if a year is a leap year.
153+
Parameters:
154+
year(int): The year
155+
Returns:
156+
True if the year is a leap year, False otherwise
157+
'''
158+
return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0
159+
160+
161+
@Entrypoint
162+
def convert_to_12h(hour: int):
163+
return 12 if (hour == 0 or hour == 12 or hour == 24) else (hour if hour < 12 else hour - 12)
164+
165+
166+
@Entrypoint
167+
def is_date(year: int, month: int, day: int) -> bool:
168+
'''
169+
Tests if a year, month, day combination is a valid date. Year is required.
170+
Month and day are optional. If day is present, month is required.
171+
Parameters:
172+
year (int): The year
173+
month (int): The month (January=1)
174+
day (int): The day of the month
175+
Returns:
176+
True if the date is valid, False otherwise
177+
'''
178+
hasYear, hasMonth, hasDay = year > -1, month > -1, day > -1
179+
180+
if not hasYear:
181+
return False
182+
if hasMonth and not hasYear:
183+
return False
184+
if hasDay and not hasMonth:
185+
return False
186+
if hasMonth and (month < 1 or month > 12):
187+
return False
188+
189+
if hasDay:
190+
if day < 1:
191+
return False
192+
elif month == 1 or month == 3 or month == 5 or month == 7 or month == 8 or month == 10 or month == 12:
193+
return day < 32
194+
elif month == 4 or month == 6 or month == 9 or month == 11:
195+
return day < 31
196+
elif month == 2:
197+
if ((year % 4 == 0 and year % 100 != 0) or year % 400 == 0):
198+
return day < 30
199+
return day < 29
200+
201+
return True
202+
203+
204+
@Entrypoint
205+
def is_time(hour: int, min: int, sec: float) -> bool:
206+
'''
207+
Tests if a hour, min, sec combination is a valid time.
208+
Parameters:
209+
hour(int): The hour
210+
min(int): The min
211+
sec(float): The second
212+
Returns:
213+
True if the time is valid, False otherwise
214+
'''
215+
# '24' is valid alternative to '0' but only when min and sec are both 0
216+
if hour < 0 or hour > 24 or (hour == 24 and (min != 0 or sec != 0)):
217+
return False
218+
elif min < 0 or min > 59 or sec < 0 or sec >= 60:
219+
return False
220+
return True
221+
222+
223+
@Entrypoint
224+
def is_datetime(year: int, month: int, day: int, hour: float, min: float, sec: float) -> bool:
225+
'''
226+
Tests if a year, month, day hour, min, sec combination is a valid date time.
227+
Parameters:
228+
year (int): The year
229+
month (int): The month (January=>1)
230+
day (int): The day of the month
231+
hour(int): The hour
232+
min(int): The min
233+
sec(float): The second
234+
Returns:
235+
True if the datetime is valid, False otherwise
236+
'''
237+
return is_date(year, month, day) and is_time(hour, min, sec)
+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Copyright 2017-2020 typed_python Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
from typed_python.lib.datetime.chrono import is_leap_year, is_date, is_time
17+
18+
19+
class TestChrono(unittest.TestCase):
20+
21+
def test_is_leap_year_valid(self):
22+
leap_years = [
23+
2000, 2004, 2008, 2012, 2016, 2020, 2024, 2028, 2032, 2036, 2040, 2044, 2048
24+
]
25+
26+
for year in leap_years:
27+
assert is_leap_year(year), year
28+
29+
def test_is_leap_year_invalid(self):
30+
not_leap_years = [
31+
1700, 1800, 1900, 1997, 1999, 2100, 2022
32+
]
33+
34+
for year in not_leap_years:
35+
assert not is_leap_year(year), year
36+
37+
def test_is_date_valid(self):
38+
# y, m, d
39+
dates = [
40+
(1997, 1, 1), # random date
41+
(2020, 2, 29) # Feb 29 on leap year
42+
]
43+
44+
for date in dates:
45+
assert is_date(date[0], date[1], date[2]), date
46+
47+
def test_is_date_invalid(self):
48+
# y, m, d
49+
dates = [
50+
(1997, 0, 1), # Month < 1
51+
(1997, 13, 1), # Month > 12
52+
(1997, 1, 0), # Day < 1
53+
(1997, 1, 32), # Day > 31 in Jan
54+
(1997, 2, 29), # Day > 28 in non-leap-year Feb,
55+
(2100, 2, 29), # Day > 28 in non-leap-year Feb,
56+
(1997, 0, 25), # Month < 1
57+
(2020, 2, 30), # Day > 29 in Feb (leap year)
58+
(2020, 4, 31), # Day > 30 in Apr (leap year)
59+
(2020, 6, 31), # Day > 30 in June (leap year)
60+
(2020, 9, 31), # Day > 30 in Sept (leap year)
61+
(2020, 11, 31) # Day > 30 in Nov (leap year)
62+
]
63+
64+
for date in dates:
65+
assert not is_date(date[0], date[1], date[2]), date
66+
67+
def test_is_time_valid(self):
68+
# h, m, s
69+
times = [
70+
(0, 0, 0), # 00:00:00
71+
(24, 0, 0), # 24:00:00
72+
(1, 1, 1), # random time
73+
(12, 59, 59) # random time
74+
]
75+
for time in times:
76+
assert is_time(time[0], time[1], time[2]), time
77+
78+
def test_is_time_invalid(self):
79+
# h, m, s
80+
times = [
81+
(24, 1, 0), # m and s must be 0 if hour is 24
82+
(25, 0, 0), # hour greater than 24
83+
(-1, 0, 0), # hour less than 0
84+
(1, 0, -1), # second < 1
85+
(1, -1, 0), # min < 1
86+
(1, 0, 60), # second > 59
87+
(1, 60, 0) # min > 59
88+
]
89+
for time in times:
90+
assert not is_time(time[0], time[1], time[2]), time

0 commit comments

Comments
 (0)