Last active
March 15, 2021 06:07
-
-
Save Hypro999/042582678582315a0a6ffac49d6c5e49 to your computer and use it in GitHub Desktop.
A simple library/class for handling conversion between python datetime objects and strings following RFC 3339.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import re | |
from datetime import datetime, timedelta, timezone | |
class RFC3339: | |
class patterns: | |
# date-fullyear = 4DIGIT | |
date_fullyear: str = r"[0-9]{4}" | |
# date-month = 2DIGIT ; 01-12 | |
date_month: str = r"0[0-9]|1[0-2]" | |
# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 (based on month/year) | |
date_mday: str = r"0[1-9]|[1-2][0-9]|3[0-1]" | |
# To keep these regular expressions simpler and independent of each other, we will not enforce the | |
# "based on month/year" part here despite the fact that regular expressions are Turing complete. | |
# time-hour = 2DIGIT ; 00-23 | |
time_hour: str = r"[0-1][0-9]|2[0-3]" | |
# time-minute = 2DIGIT ; 00-59 | |
time_minute: str = r"[0-5][0-9]" | |
# time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second rules | |
time_second: str = r"[0-5][0-9]|60" | |
# time-secfrac = "." 1*DIGIT | |
time_secfrac: str = r"\.[0-9]+" | |
# time-numoffset = ("+" / "-") time-hour ":" time-minute | |
time_numoffset: str = f"[+-](?P<offset_hour>{time_hour}):(?P<offset_minute>{time_minute})" | |
# time-numoffset = ("+" / "-") time-hour ":" time-minute | |
time_offset: str = f"[Zz]|({time_numoffset})" | |
# partial-time = time-hour ":" time-minute ":" time-second [time-secfrac] | |
partial_time: str = f"(?P<hour>{time_hour}):(?P<minute>{time_minute}):(?P<second>{time_second})(?P<secfrac>{time_secfrac})?" | |
# full-date = date-fullyear "-" date-month "-" date-mday | |
full_date: str = f"(?P<year>{date_fullyear})-(?P<month>{date_month})-(?P<day>{date_mday})" | |
# full-time = partial-time time-offset | |
# full_time: str = f"(?P<time>{partial_time})(?P<offset>{time_offset})" | |
full_time: str = f"(?P<time>{partial_time})(?P<offset>{time_offset})" | |
# date-time = full-date "T" full-time | |
date_time: str = f"^(?P<date>{full_date})[Tt]({full_time})$" | |
def __init__(self): | |
self.pattern = re.compile(self.patterns.date_time) | |
def is_valid(self, timestamp: str) -> bool: | |
return bool(self.pattern.match(timestamp)) | |
def extract_datetime(self, timestamp: str) -> datetime: | |
match: re.Match = self.pattern.match(timestamp) | |
if match == None: | |
raise ValueError("Invalid format. The string \"{timestamp}\" Does not follow RFC 3339.") | |
second = int(match["second"]) | |
if second == 60: | |
# Python's datetime library doesn't support leap seconds so we | |
# will handle this by rounding down by one second if needed. | |
# https://docs.python.org/3.6/library/datetime.html#available-types | |
second = 59 | |
secfrac = match["secfrac"] | |
microsecond = 0 | |
if secfrac: | |
microsecond = int(float(match["secfrac"]) * 10**6) | |
factor = 1 | |
tz_offset = match["offset"] | |
if tz_offset in ["Z", "z"]: | |
hours = 0 | |
minutes = 0 | |
else: | |
if tz_offset.startswith("-"): | |
factor = -1 | |
hours = int(match["offset_hour"]) | |
minutes = int(match["offset_minute"]) | |
tz = timezone(factor * timedelta(hours=hours, minutes=minutes)) | |
return datetime( | |
year=int(match["year"]), | |
month=int(match["month"]), | |
day=int(match["day"]), | |
hour=int(match["hour"]), | |
minute=int(match["minute"]), | |
second=second, | |
microsecond=microsecond, | |
tzinfo=tz, | |
) | |
def encode_datetime(self, dt: datetime) -> str: | |
# ISO 8601 strings are compatible with RFC 3339. | |
timestamp = dt.isoformat() | |
if timestamp.endswith("+00:00"): | |
timestamp = timestamp[:-6] + "Z" | |
return timestamp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from rfc3339 import RFC3339 | |
from datetime import datetime, timedelta, timezone | |
UTC = timezone(timedelta(hours=0, minutes=0)) | |
PST = timezone(-1 * timedelta(hours=8, minutes=0)) | |
IND = timezone(timedelta(hours=5, minutes=30)) | |
AMS = timezone(timedelta(hours=0, minutes=20)) | |
testcases = [ | |
("1985-04-12T23:20:50.52Z", datetime(1985, 4, 12, 23, 20, 50, 520000, tzinfo=UTC)), | |
("1985-04-12t23:20:50.52z", datetime(1985, 4, 12, 23, 20, 50, 520000, tzinfo=UTC)), | |
("1996-12-19T16:39:57-08:00", datetime(1996, 12 ,19, 16, 39, 57, 000000, tzinfo=PST)), | |
("1996-12-19T16:39:57+05:30", datetime(1996, 12 ,19, 16, 39, 57, 000000, tzinfo=IND)), | |
("1990-12-31T23:59:60Z", datetime(1990, 12, 31, 23, 59, 59, tzinfo=UTC)), # avoid leap seconds. | |
("1990-12-31T15:59:60-08:00", datetime(1990, 12, 31, 15, 59, 59, tzinfo=PST)), | |
("1937-01-01T12:00:27.87+00:20", datetime(1937, 1, 1, 12, 0, 27, 870000, tzinfo=AMS)), | |
] | |
for testcase in testcases: | |
validator = RFC3339() | |
got, want = validator.extract_datetime(testcase[0]), testcase[1] | |
if got != want: | |
assert(got == want) # To raise an appropriate exception. | |
else: | |
print("{} passed.".format(validator.encode_datetime(got))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment