Skip to content

Commit b4ab8bd

Browse files
committed
Added Timestamp module along with supporting modules and tests
1 parent 11e5e28 commit b4ab8bd

11 files changed

+2580
-0
lines changed

Diff for: 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

Diff for: typed_python/lib/datetime/chrono.py

+264
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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(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, int, int): a tuple of (day, month, year)
113+
'''
114+
if n > 4:
115+
raise ValueError('n should be 1-4')
116+
if 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+
day = weekday_difference(wd, wd_1st) + 1 + (n - 1) * 7
123+
124+
return (day, month, year)
125+
126+
127+
@Entrypoint
128+
def get_nth_dow_of_month_unixtime(n: int, wd: int, month: int, year: int) -> int:
129+
'''
130+
Gets the date of the nth day of the month for a given year. E.g. get 2nd Sat in July 2022
131+
Parameters:
132+
n (int): nth day of week (1-4).
133+
wd (int): the weekday (0-6) where 0 => Sunday
134+
month (int): the month (1-12)
135+
year (int): the year
136+
137+
Returns:
138+
(int): The nth day of the month in unixtime
139+
'''
140+
if n > 4:
141+
raise ValueError('n should be 1-4')
142+
if wd > 6:
143+
raise ValueError('wd should be 0-6')
144+
if month < 1 or month > 12:
145+
raise ValueError('invalid month')
146+
147+
wd_1st = weekday_from_days(days_from_civil(year, month, 1))
148+
149+
return date_to_seconds(year=year,
150+
month=month,
151+
day=weekday_difference(wd, wd_1st) + 1 + (n - 1) * 7)
152+
153+
154+
@Entrypoint
155+
def get_year_from_unixtime(ts: float) -> int:
156+
'''
157+
Gets the year from a unixtime
158+
Parameters:
159+
ts (float): the unix timestamp
160+
Returns:
161+
(int): The year
162+
'''
163+
z = ts // 86400 + 719468
164+
era = (z if z >= 0 else z - 146096) // 146097
165+
doe = z - era * 146097
166+
yoe = (doe - (doe // 1460) + (doe // 36524) - (doe // 146096)) // 365
167+
y = int(yoe + era * 400)
168+
doy = int(doe - ((365 * yoe) + (yoe // 4) - (yoe // 100)))
169+
mp = (5 * doy + 2) // 153
170+
m = int(mp + (3 if mp < 10 else -9))
171+
y += (m <= 2)
172+
return y
173+
174+
175+
@Entrypoint
176+
def is_leap_year(year: int):
177+
'''
178+
Tests if a year is a leap year.
179+
Parameters:
180+
year(int): The year
181+
Returns:
182+
True if the year is a leap year, False otherwise
183+
'''
184+
return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0
185+
186+
187+
@Entrypoint
188+
def convert_to_12h(hour: int):
189+
if hour == 0 or hour == 12 or hour == 24:
190+
return 12
191+
elif hour < 12:
192+
return hour
193+
else:
194+
return hour - 12
195+
196+
197+
@Entrypoint
198+
def is_date(year: int, month: int, day: int) -> bool:
199+
'''
200+
Tests if a year, month, day combination is a valid date. Year is required.
201+
Month and day are optional. If day is present, month is required.
202+
Parameters:
203+
year (int): The year
204+
month (int): The month (January=1)
205+
day (int): The day of the month
206+
Returns:
207+
True if the date is valid, False otherwise
208+
'''
209+
if year is None:
210+
return False
211+
if month is None and day is not None:
212+
return False
213+
if month is not None:
214+
if month > 12 or month < 1:
215+
return False
216+
if month == 2 and day is not None:
217+
# is leap year?
218+
if (year % 4 == 0 and year % 100 != 0) or year % 400 == 0:
219+
if (day > 29):
220+
return False
221+
elif day > 28:
222+
return False
223+
if (month == 9 or month == 4 or month == 6 or month == 11) and day is not None and day > 30:
224+
return False
225+
226+
if day is not None and (day > 31 or day < 1):
227+
return False
228+
return True
229+
230+
231+
@Entrypoint
232+
def is_time(hour: int, min: int, sec: float) -> bool:
233+
'''
234+
Tests if a hour, min, sec combination is a valid time.
235+
Parameters:
236+
hour(int): The hour
237+
min(int): The min
238+
sec(float): The second
239+
Returns:
240+
True if the time is valid, False otherwise
241+
'''
242+
# '24' is valid alternative to '0' but only when min and sec are both 0
243+
if hour < 0 or hour > 24 or (hour == 24 and (min != 0 or sec != 0)):
244+
return False
245+
elif min < 0 or min > 59 or sec < 0 or sec >= 60:
246+
return False
247+
return True
248+
249+
250+
@Entrypoint
251+
def is_datetime(year: int, month: int, day: int, hour: float, min: float, sec: float) -> bool:
252+
'''
253+
Tests if a year, month, day hour, min, sec combination is a valid date time.
254+
Parameters:
255+
year (int): The year
256+
month (int): The month (January=>1)
257+
day (int): The day of the month
258+
hour(int): The hour
259+
min(int): The min
260+
sec(float): The second
261+
Returns:
262+
True if the datetime is valid, False otherwise
263+
'''
264+
return is_date(year, month, day) and is_time(hour, min, sec)

Diff for: typed_python/lib/datetime/chrono_test.py

+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)