Skip to content

Commit

Permalink
Added Timestamp module along with supporting modules and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
launeh committed Nov 25, 2022
1 parent 11e5e28 commit b4ab8bd
Show file tree
Hide file tree
Showing 11 changed files with 2,580 additions and 0 deletions.
31 changes: 31 additions & 0 deletions docs/intro_2.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,37 @@ python semantics is its treament of integers. For performance and memory reasons
this won't be a problem, but if you attempt to place an integer larger than 64 bits into a
`typed_python` container, you'll see the integer get cast down to 64 bits.

### Timestamp

`typed_python` provides the Timestamp type that wraps useful datetime functionality around a
unix timestamp.

For e.g, you can create a Timestamp from a unixtime with the following:

```
ts1 = Timestamp.make(1654615145)
ts2 = Timestamp(ts=1654615145)
```

You can also create Timestamps from datestrings. The parser supports ISO 8601 along with variety
of non-iso formats. E.g:
```
ts1 = Timestamp.parse("2022-01-05T10:11:12+00:15")
ts2 = Timestamp.parse("2022-01-05T10:11:12NYC")
ts3 = Timestamp.parse("January 1, 2022")
ts4 = Timestamp.parse("January/1/2022")
ts5 = Timestamp.parse("Jan-1-2022")
```

You can format Timestamps as strings using standard time format directives. E.g:

```
timestamp = Timestamp.make(1654615145)
print(timestamp.format(utc_offset=144000)) # 2022-06-09T07:19:05
print(timestamp.format(format="%Y-%m-%d")) # 2022-06-09
```


### Object

In some cases, you may have types that need to hold regular python objects. For these cases, you may
Expand Down
264 changes: 264 additions & 0 deletions typed_python/lib/datetime/chrono.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
# Copyright 2017-2020 typed_python Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typed_python import Entrypoint

# This file implements some useful low level algorithms for processing dates and times.
# Many of the algorithms are described here https://howardhinnant.github.io/date_algorithms.html


@Entrypoint
def days_from_civil(year: int = 0, month: int = 0, day: int = 0) -> int:
'''
Creates a unix timestamp from date values.
Parameters:
year (int): The year
month (int): The month. January: 1, February: 2, ....
day (int): The day
Returns:
seconds(float): The number of seconds
Implements the low level days_from_civil algorithm
'''
year -= month <= 2
era = (year if year >= 0 else year - 399) // 400
yoe = (year - era * 400)
doy = (153 * ( month - 3 if month > 2 else month + 9) + 2) // 5 + day - 1
doe = yoe * 365 + yoe // 4 - yoe // 100 + doy
days = era * 146097 + doe - 719468

return days


@Entrypoint
def date_to_seconds(year: int = 0, month: int = 0, day: int = 0) -> float:
'''
Creates a unix timestamp from date values.
Parameters:
year (int): The year
month (int): The month. January: 1, February: 2, ....
day (int): The day
Returns:
seconds(float): The number of seconds
'''
return days_from_civil(year, month, day) * 86400


@Entrypoint
def time_to_seconds(hour: int = 0, minute: int = 0, second: float = 0) -> float:
'''
Converts and hour, min, second combination into seconds
Parameters:
hour (int): The hour (0-23)
minute (int): The minute
second (int): The second
Returns:
(float) the number of seconds
'''
return (hour * 3600) + (minute * 60) + second


@Entrypoint
def weekday_difference(x: int, y: int) -> int:
'''
Gets the difference in days between two weekdays
Parameters:
x (int): The first day
y (int): The second day
Returns:
(int) the difference between the two weekdays
'''
x -= y
return x if x >= 0 and x <= 6 else x + 7


@Entrypoint
def weekday_from_days(z: int) -> int:
'''
Gets the day of week given the number of days from the unix epoch
Parameters:
z (int): The number of days from the epoch
Returns:
(int) the weekday (0-6)
'''
return (z + 4) % 7 if z >= -4 else (z + 5) % 7 + 6


@Entrypoint
def get_nth_dow_of_month(n: int, wd: int, month: int, year: int) -> int:
'''
Gets the date of the nth day of the month for a given year. E.g. get 2nd Sat in July 2022
Parameters:
n (int): nth day of week (1-4).
wd (int): the weekday (0-6) where 0 => Sunday
month (int): the month (1-12)
year (int): the year
Returns:
(int, int, int): a tuple of (day, month, year)
'''
if n > 4:
raise ValueError('n should be 1-4')
if wd > 6:
raise ValueError('wd should be 0-6')
if month < 1 or month > 12:
raise ValueError('invalid month')

wd_1st = weekday_from_days(days_from_civil(year, month, 1))
day = weekday_difference(wd, wd_1st) + 1 + (n - 1) * 7

return (day, month, year)


@Entrypoint
def get_nth_dow_of_month_unixtime(n: int, wd: int, month: int, year: int) -> int:
'''
Gets the date of the nth day of the month for a given year. E.g. get 2nd Sat in July 2022
Parameters:
n (int): nth day of week (1-4).
wd (int): the weekday (0-6) where 0 => Sunday
month (int): the month (1-12)
year (int): the year
Returns:
(int): The nth day of the month in unixtime
'''
if n > 4:
raise ValueError('n should be 1-4')
if wd > 6:
raise ValueError('wd should be 0-6')
if month < 1 or month > 12:
raise ValueError('invalid month')

wd_1st = weekday_from_days(days_from_civil(year, month, 1))

return date_to_seconds(year=year,
month=month,
day=weekday_difference(wd, wd_1st) + 1 + (n - 1) * 7)


@Entrypoint
def get_year_from_unixtime(ts: float) -> int:
'''
Gets the year from a unixtime
Parameters:
ts (float): the unix timestamp
Returns:
(int): The year
'''
z = ts // 86400 + 719468
era = (z if z >= 0 else z - 146096) // 146097
doe = z - era * 146097
yoe = (doe - (doe // 1460) + (doe // 36524) - (doe // 146096)) // 365
y = int(yoe + era * 400)
doy = int(doe - ((365 * yoe) + (yoe // 4) - (yoe // 100)))
mp = (5 * doy + 2) // 153
m = int(mp + (3 if mp < 10 else -9))
y += (m <= 2)
return y


@Entrypoint
def is_leap_year(year: int):
'''
Tests if a year is a leap year.
Parameters:
year(int): The year
Returns:
True if the year is a leap year, False otherwise
'''
return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0


@Entrypoint
def convert_to_12h(hour: int):
if hour == 0 or hour == 12 or hour == 24:
return 12
elif hour < 12:
return hour
else:
return hour - 12


@Entrypoint
def is_date(year: int, month: int, day: int) -> bool:
'''
Tests if a year, month, day combination is a valid date. Year is required.
Month and day are optional. If day is present, month is required.
Parameters:
year (int): The year
month (int): The month (January=1)
day (int): The day of the month
Returns:
True if the date is valid, False otherwise
'''
if year is None:
return False
if month is None and day is not None:
return False
if month is not None:
if month > 12 or month < 1:
return False
if month == 2 and day is not None:
# is leap year?
if (year % 4 == 0 and year % 100 != 0) or year % 400 == 0:
if (day > 29):
return False
elif day > 28:
return False
if (month == 9 or month == 4 or month == 6 or month == 11) and day is not None and day > 30:
return False

if day is not None and (day > 31 or day < 1):
return False
return True


@Entrypoint
def is_time(hour: int, min: int, sec: float) -> bool:
'''
Tests if a hour, min, sec combination is a valid time.
Parameters:
hour(int): The hour
min(int): The min
sec(float): The second
Returns:
True if the time is valid, False otherwise
'''
# '24' is valid alternative to '0' but only when min and sec are both 0
if hour < 0 or hour > 24 or (hour == 24 and (min != 0 or sec != 0)):
return False
elif min < 0 or min > 59 or sec < 0 or sec >= 60:
return False
return True


@Entrypoint
def is_datetime(year: int, month: int, day: int, hour: float, min: float, sec: float) -> bool:
'''
Tests if a year, month, day hour, min, sec combination is a valid date time.
Parameters:
year (int): The year
month (int): The month (January=>1)
day (int): The day of the month
hour(int): The hour
min(int): The min
sec(float): The second
Returns:
True if the datetime is valid, False otherwise
'''
return is_date(year, month, day) and is_time(hour, min, sec)
90 changes: 90 additions & 0 deletions typed_python/lib/datetime/chrono_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Copyright 2017-2020 typed_python Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import unittest
from typed_python.lib.datetime.chrono import is_leap_year, is_date, is_time


class TestChrono(unittest.TestCase):

def test_is_leap_year_valid(self):
leap_years = [
2000, 2004, 2008, 2012, 2016, 2020, 2024, 2028, 2032, 2036, 2040, 2044, 2048
]

for year in leap_years:
assert is_leap_year(year), year

def test_is_leap_year_invalid(self):
not_leap_years = [
1700, 1800, 1900, 1997, 1999, 2100, 2022
]

for year in not_leap_years:
assert not is_leap_year(year), year

def test_is_date_valid(self):
# y, m, d
dates = [
(1997, 1, 1), # random date
(2020, 2, 29) # Feb 29 on leap year
]

for date in dates:
assert is_date(date[0], date[1], date[2]), date

def test_is_date_invalid(self):
# y, m, d
dates = [
(1997, 0, 1), # Month < 1
(1997, 13, 1), # Month > 12
(1997, 1, 0), # Day < 1
(1997, 1, 32), # Day > 31 in Jan
(1997, 2, 29), # Day > 28 in non-leap-year Feb,
(2100, 2, 29), # Day > 28 in non-leap-year Feb,
(1997, 0, 25), # Month < 1
(2020, 2, 30), # Day > 29 in Feb (leap year)
(2020, 4, 31), # Day > 30 in Apr (leap year)
(2020, 6, 31), # Day > 30 in June (leap year)
(2020, 9, 31), # Day > 30 in Sept (leap year)
(2020, 11, 31) # Day > 30 in Nov (leap year)
]

for date in dates:
assert not is_date(date[0], date[1], date[2]), date

def test_is_time_valid(self):
# h, m, s
times = [
(0, 0, 0), # 00:00:00
(24, 0, 0), # 24:00:00
(1, 1, 1), # random time
(12, 59, 59) # random time
]
for time in times:
assert is_time(time[0], time[1], time[2]), time

def test_is_time_invalid(self):
# h, m, s
times = [
(24, 1, 0), # m and s must be 0 if hour is 24
(25, 0, 0), # hour greater than 24
(-1, 0, 0), # hour less than 0
(1, 0, -1), # second < 1
(1, -1, 0), # min < 1
(1, 0, 60), # second > 59
(1, 60, 0) # min > 59
]
for time in times:
assert not is_time(time[0], time[1], time[2]), time
Loading

0 comments on commit b4ab8bd

Please sign in to comment.