functions to parse datetime objects we re using regular expressions rather than time strptime because they provide both validation and parsing they re more flexible for datetimes the datedatetimetime constructors produce friendlier error messages import datetime from django utils regexhelper import lazyrecompile from django utils timezone import getfixedtimezone datere lazyrecompiler pyeard4 pmonthd1 2 pdayd1 2 timere lazyrecompile r phourd1 2 pminuted1 2 r psecondd1 2 pmicrosecondd1 6d0 6 datetimere lazyrecompile r pyeard4 pmonthd1 2 pdayd1 2 rt phourd1 2 pminuted1 2 r psecondd1 2 pmicrosecondd1 6d0 6 rs ptzinfozd2 d2 standarddurationre lazyrecompile r r pdays d days r psign r phoursd d d r pminutesd r psecondsd r pmicrosecondsd1 6d0 6 r support the sections of iso 8601 date representation that are accepted by timedelta iso8601durationre lazyrecompile r psign rp r pdaysd d d r t r phoursd d h r pminutesd d m r psecondsd d s r r support postgresql s daytime interval format e g 3 days 04 05 06 the yearmonth and mixed intervals cannot be converted to a timedelta and thus aren t accepted postgresintervalre lazyrecompile r r pdays d days r psign r phoursd r pminutesdd r psecondsdd r pmicrosecondsd1 6 r def parsedatevalue try return datetime date fromisoformatvalue except valueerror if match datere matchvalue kw k intv for k v in match groupdict items return datetime datekw def parsetimevalue try the fromisoformat method takes time zone info into account and returns a time with a tzinfo component if possible however there are no circumstances where aware datetime time objects make sense so remove the time zone offset return datetime time fromisoformatvalue replacetzinfonone except valueerror if match timere matchvalue kw match groupdict kwmicrosecond kwmicrosecond and kwmicrosecond ljust6 0 kw k intv for k v in kw items if v is not none return datetime timekw def parsedatetimevalue try return datetime datetime fromisoformatvalue except valueerror if match datetimere matchvalue kw match groupdict kwmicrosecond kwmicrosecond and kwmicrosecond ljust6 0 tzinfo kw poptzinfo if tzinfo z tzinfo datetime timezone utc elif tzinfo is not none offsetmins inttzinfo2 if lentzinfo 3 else 0 offset 60 inttzinfo1 3 offsetmins if tzinfo0 offset offset tzinfo getfixedtimezoneoffset kw k intv for k v in kw items if v is not none return datetime datetimekw tzinfotzinfo def parsedurationvalue match standarddurationre matchvalue or iso8601durationre matchvalue or postgresintervalre matchvalue if match kw match groupdict sign 1 if kw popsign else 1 if kw getmicroseconds kwmicroseconds kwmicroseconds ljust6 0 kw k floatv replace for k v in kw items if v is not none days datetime timedeltakw popdays 0 0 or 0 0 if match re iso8601durationre days sign return days sign datetime timedeltakw we re using regular expressions rather than time strptime because they provide both validation and parsing they re more flexible for datetimes the date datetime time constructors produce friendlier error messages support the sections of iso 8601 date representation that are accepted by timedelta support postgresql s day time interval format e g 3 days 04 05 06 the year month and mixed intervals cannot be converted to a timedelta and thus aren t accepted parse a string and return a datetime date raise valueerror if the input is well formatted but not a valid date return none if the input isn t well formatted parse a string and return a datetime time this function doesn t support time zone offsets raise valueerror if the input is well formatted but not a valid time return none if the input isn t well formatted in particular if it contains an offset the fromisoformat method takes time zone info into account and returns a time with a tzinfo component if possible however there are no circumstances where aware datetime time objects make sense so remove the time zone offset parse a string and return a datetime datetime this function supports time zone offsets when the input contains one the output uses a timezone with a fixed offset from utc raise valueerror if the input is well formatted but not a valid datetime return none if the input isn t well formatted parse a duration string and return a datetime timedelta the preferred format for durations in django is d h m s f also supports iso 8601 representation and postgresql s day time interval format
import datetime

from django.utils.regex_helper import _lazy_re_compile
from django.utils.timezone import get_fixed_timezone

date_re = _lazy_re_compile(r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$")

time_re = _lazy_re_compile(
    r"(?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
    r"(?::(?P<second>\d{1,2})(?:[.,](?P<microsecond>\d{1,6})\d{0,6})?)?$"
)

datetime_re = _lazy_re_compile(
    r"(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})"
    r"[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})"
    r"(?::(?P<second>\d{1,2})(?:[.,](?P<microsecond>\d{1,6})\d{0,6})?)?"
    r"\s*(?P<tzinfo>Z|[+-]\d{2}(?::?\d{2})?)?$"
)

standard_duration_re = _lazy_re_compile(
    r"^"
    r"(?:(?P<days>-?\d+) (days?, )?)?"
    r"(?P<sign>-?)"
    r"((?:(?P<hours>\d+):)(?=\d+:\d+))?"
    r"(?:(?P<minutes>\d+):)?"
    r"(?P<seconds>\d+)"
    r"(?:[.,](?P<microseconds>\d{1,6})\d{0,6})?"
    r"$"
)



iso8601_duration_re = _lazy_re_compile(
    r"^(?P<sign>[-+]?)"
    r"P"
    r"(?:(?P<days>\d+([.,]\d+)?)D)?"
    r"(?:T"
    r"(?:(?P<hours>\d+([.,]\d+)?)H)?"
    r"(?:(?P<minutes>\d+([.,]\d+)?)M)?"
    r"(?:(?P<seconds>\d+([.,]\d+)?)S)?"
    r")?"
    r"$"
)




postgres_interval_re = _lazy_re_compile(
    r"^"
    r"(?:(?P<days>-?\d+) (days? ?))?"
    r"(?:(?P<sign>[-+])?"
    r"(?P<hours>\d+):"
    r"(?P<minutes>\d\d):"
    r"(?P<seconds>\d\d)"
    r"(?:\.(?P<microseconds>\d{1,6}))?"
    r")?$"
)


def parse_date(value):
    
    try:
        return datetime.date.fromisoformat(value)
    except ValueError:
        if match := date_re.match(value):
            kw = {k: int(v) for k, v in match.groupdict().items()}
            return datetime.date(**kw)


def parse_time(value):
    
    try:
        
        
        
        
        return datetime.time.fromisoformat(value).replace(tzinfo=None)
    except ValueError:
        if match := time_re.match(value):
            kw = match.groupdict()
            kw["microsecond"] = kw["microsecond"] and kw["microsecond"].ljust(6, "0")
            kw = {k: int(v) for k, v in kw.items() if v is not None}
            return datetime.time(**kw)


def parse_datetime(value):
    
    try:
        return datetime.datetime.fromisoformat(value)
    except ValueError:
        if match := datetime_re.match(value):
            kw = match.groupdict()
            kw["microsecond"] = kw["microsecond"] and kw["microsecond"].ljust(6, "0")
            tzinfo = kw.pop("tzinfo")
            if tzinfo == "Z":
                tzinfo = datetime.timezone.utc
            elif tzinfo is not None:
                offset_mins = int(tzinfo[-2:]) if len(tzinfo) > 3 else 0
                offset = 60 * int(tzinfo[1:3]) + offset_mins
                if tzinfo[0] == "-":
                    offset = -offset
                tzinfo = get_fixed_timezone(offset)
            kw = {k: int(v) for k, v in kw.items() if v is not None}
            return datetime.datetime(**kw, tzinfo=tzinfo)


def parse_duration(value):
    
    match = (
        standard_duration_re.match(value)
        or iso8601_duration_re.match(value)
        or postgres_interval_re.match(value)
    )
    if match:
        kw = match.groupdict()
        sign = -1 if kw.pop("sign", "+") == "-" else 1
        if kw.get("microseconds"):
            kw["microseconds"] = kw["microseconds"].ljust(6, "0")
        kw = {k: float(v.replace(",", ".")) for k, v in kw.items() if v is not None}
        days = datetime.timedelta(kw.pop("days", 0.0) or 0.0)
        if match.re == iso8601_duration_re:
            days *= sign
        return days + sign * datetime.timedelta(**kw)