Package datetimecalc

Natural language datetime and timedelta calculator.

This library provides functions for parsing and computing with datetime and timedelta expressions using natural language syntax.

Example usage::

from datetimecalc import parse_temporal_expr

parse_temporal_expr('2024-01-01 00:00 + 1 week')
# datetime.datetime(2024, 1, 8, 0, 0)

parse_temporal_expr('2025-01-01 - 2024-01-01')
# datetime.timedelta(days=366)

parse_temporal_expr('1 day == 24 hours')
# True

Main functions: parse_temporal_expr: Parse and evaluate expressions like "2024-01-01 + 1 day" parse_datetime_str: Parse natural language dates like "tomorrow at 3pm" parse_timedelta_str: Parse durations like "1 day 2 hours" parse_temporal_str: Parse either datetime or timedelta strings

Sub-modules

datetimecalc.functions

functions for working with simple temporal expressions

datetimecalc.timedelta

utilities for working with timedeltas

datetimecalc.tz

helper functions for working with timezones

Functions

def is_datetime(obj: Any) ‑> bool
Expand source code
def is_datetime(obj: Any) -> bool:
    """
    Check if the given object is a datetime.

    Args:
        obj: The object to be checked.

    Returns:
        True if the object is a datetime, False otherwise.

    Examples:
        >>> is_datetime(datetime.now())
        True

        >>> is_datetime(timedelta(days=1))
        False
    """
    return isinstance(obj, datetime)

Check if the given object is a datetime.

Args

obj
The object to be checked.

Returns

True if the object is a datetime, False otherwise.

Examples

>>> is_datetime(datetime.now())
True
>>> is_datetime(timedelta(days=1))
False
def is_timedelta(obj: Any) ‑> bool
Expand source code
def is_timedelta(obj: Any) -> bool:
    """
    Check if the given object is a timedelta.

    Args:
        obj: The object to be checked.

    Returns:
        True if the object is a timedelta, False otherwise.

    Examples:
        >>> is_timedelta(timedelta(days=1))
        True

        >>> is_timedelta(datetime.now())
        False
    """
    return isinstance(obj, timedelta)

Check if the given object is a timedelta.

Args

obj
The object to be checked.

Returns

True if the object is a timedelta, False otherwise.

Examples

>>> is_timedelta(timedelta(days=1))
True
>>> is_timedelta(datetime.now())
False
def parse_datetime_str(input_str: str) ‑> datetime.datetime
Expand source code
def parse_datetime_str(input_str: str) -> datetime:
    """
    Parse the input string for a natural language reference to a date and
    return a datetime object.

    Args:
        input_str: The input string containing the natural language reference.

    Returns:
        A datetime object representing the parsed date.

    Raises:
        ValueError: If the input string cannot be parsed.

    Examples:
        >>> parse_datetime_str('tomorrow')
        datetime.datetime(...)
        >>> parse_datetime_str('next week')
        datetime.datetime(...)
        >>> parse_datetime_str("jan 6th 2022 9:00 AM UTC")
        datetime.datetime(2022, 1, 6, 9, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC'))
        >>> parse_datetime_str("2022-01-01 00:00 UTC")
        datetime.datetime(2022, 1, 1, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC'))
    """
    # scan for timezone
    tzinfo_obj, filtered_input = search_tz(input_str)
    if tzinfo_obj:
        logging.debug(
            'recognized timezone "%s"; proceeding to parse: "%s"',
            str(tzinfo_obj),
            filtered_input,
        )
    else:
        logging.debug("no recognized timezone")

    # Create a parsedatetime.Calendar instance
    cal = parsedatetime.Calendar(version=parsedatetime.VERSION_CONTEXT_STYLE)

    # Use parsedatetime.Calendar.parseDT to parse the input string and obtain a
    # datetime object
    parsed_date, parse_status = cal.parseDT(
        filtered_input if tzinfo_obj else input_str,
        tzinfo=tzinfo_obj,
        # sourceTime=datetime(1, 1, 1),
    )
    logging.debug("parsed_date: %s, accuracy: %s", parsed_date, parse_status.accuracy)
    if not parse_status or parse_status.accuracy == 0:
        raise ValueError("Unable to parse the input string.")

    # Make the datetime object timezone aware
    # parsed_date = parsed_date.replace(tzinfo=dateutil.tz.gettz())

    # Return the datetime object
    return parsed_date

Parse the input string for a natural language reference to a date and return a datetime object.

Args

input_str
The input string containing the natural language reference.

Returns

A datetime object representing the parsed date.

Raises

ValueError
If the input string cannot be parsed.

Examples

>>> parse_datetime_str('tomorrow')
datetime.datetime(...)
>>> parse_datetime_str('next week')
datetime.datetime(...)
>>> parse_datetime_str("jan 6th 2022 9:00 AM UTC")
datetime.datetime(2022, 1, 6, 9, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC'))
>>> parse_datetime_str("2022-01-01 00:00 UTC")
datetime.datetime(2022, 1, 1, 0, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC'))
def parse_temporal_expr(expression: str) ‑> datetime.datetime | datetime.timedelta | datetime.tzinfo
Expand source code
def parse_temporal_expr(expression: str) -> TemporalObject:
    """
    Parse a temporal expression and perform the corresponding operation.

    The temporal expression should be in the format "<term> <operator> <term>",
    where <term> can be either a datetime or a timedelta object, and <operator>
    can be one of the following: '+', '-', '<', '<=', '>', '>=', '==', '!='.

    Args:
        expression: The temporal expression to parse.

    Returns:
        The result of the operation, which can be a datetime or a timedelta
        object.

    Raises:
        ValueError: If the expression is invalid or if the operation is not
        supported.

    Examples:
        >>> parse_temporal_expr('2022-01-01 12:00 UTC  + 1 day')
        datetime.datetime(2022, 1, 2, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC'))
        >>> parse_temporal_expr('2022-01-01 00:00 - 1 week')
        datetime.datetime(2021, 12, 25, 0, 0)
        >>> parse_temporal_expr('2022-01-01 UTC < 2023-01-01 UTC')
        True
        >>> parse_temporal_expr('1 day == 24 hours')
        True
        >>> parse_temporal_expr('2025-01-02 - 2023-01-01')
        datetime.timedelta(days=732)
        >>> parse_temporal_expr('2022-01-01 + 2023-01-01')  # Raises ValueError
        Traceback (most recent call last):
            ...
        ValueError: Unsupported operation: datetime + datetime
    """
    match = _TEMPORAL_EXPR_PATTERN.match(expression)
    if not match:
        logging.debug("no match on time_op, trying as temporal_str")
        try:
            return parse_temporal_str(expression)
        except ValueError:
            raise ValueError("unable to parse the expression, generally") from None
    logging.debug("capture groups: %s", match.groupdict())
    a_term = parse_temporal_str(match.group("a").strip())
    b_term = parse_temporal_str(match.group("b").strip())
    op_string = match.group("op").strip()
    op_func = {
        "+": operator.add,
        "-": operator.sub,
        "<": operator.lt,
        "<=": operator.le,
        ">": operator.gt,
        ">=": operator.ge,
        "==": operator.eq,
        "!=": operator.ne,
        "@": lambda dt, tz: dt.astimezone(tz),
    }[op_string]

    logging.debug("%s %s %s", repr(a_term), repr(op_string), repr(b_term))

    match (a_term, op_string, b_term):
        # datetime +/- timedelta == datetime
        case (datetime(), "+" | "-", timedelta()):
            return op_func(a_term, b_term)

        # datetime - datetime == timedelta
        # datetime cmp datetime == bool
        case (datetime(), "-" | "<" | "<=" | ">" | ">=" | "==" | "!=", datetime()):
            return op_func(a_term, b_term)

        # datetime @ timezone == datetime (timezone conversion)
        case (datetime(), "@", tzinfo()):
            return op_func(a_term, b_term)

        # timedelta +/- timedelta == timedelta
        # timedelta cmp timedelta == bool
        case (timedelta(), _, timedelta()):
            return op_func(a_term, b_term)

        # timezone cmp timezone == bool (compare by UTC offset)
        case (tzinfo(), "-" | "<" | "<=" | ">" | ">=" | "==" | "!=", tzinfo()):
            now = datetime.now(timezone.utc)
            return op_func(a_term.utcoffset(now), b_term.utcoffset(now))

        case _:
            raise ValueError(
                f"Unsupported operation: "
                f"{type(a_term).__name__} {op_string} {type(b_term).__name__}"
            )

Parse a temporal expression and perform the corresponding operation.

The temporal expression should be in the format " ", where can be either a datetime or a timedelta object, and can be one of the following: '+', '-', '<', '<=', '>', '>=', '==', '!='.

Args

expression
The temporal expression to parse.

Returns

The result of the operation, which can be a datetime or a timedelta object.

Raises

ValueError
If the expression is invalid or if the operation is not

supported.

Examples

>>> parse_temporal_expr('2022-01-01 12:00 UTC  + 1 day')
datetime.datetime(2022, 1, 2, 12, 0, tzinfo=zoneinfo.ZoneInfo(key='UTC'))
>>> parse_temporal_expr('2022-01-01 00:00 - 1 week')
datetime.datetime(2021, 12, 25, 0, 0)
>>> parse_temporal_expr('2022-01-01 UTC < 2023-01-01 UTC')
True
>>> parse_temporal_expr('1 day == 24 hours')
True
>>> parse_temporal_expr('2025-01-02 - 2023-01-01')
datetime.timedelta(days=732)
>>> parse_temporal_expr('2022-01-01 + 2023-01-01')  # Raises ValueError
Traceback (most recent call last):
    ...
ValueError: Unsupported operation: datetime + datetime
def parse_temporal_str(input_str: str) ‑> datetime.datetime | datetime.timedelta | datetime.tzinfo
Expand source code
def parse_temporal_str(input_str: str) -> TemporalObject:
    """
    Parse a string as a datetime, timedelta, or timezone

    Args:
        input_str: The input string to parse.

    Returns:
        A datetime object if the string represents a date, or a timedelta
        object if it represents a time delta.

    Raises:
        ValueError: If the input string cannot be parsed as either a datetime
        or a timedelta.

    Examples:
        >>> parse_temporal_str('1d')
        datetime.timedelta(...)
        >>> parse_temporal_str('tomorrow')
        datetime.datetime(...)
    """
    logging.debug('trying "%s" as timezone', input_str)
    try:
        return parse_timezone_str(input_str)
    except ValueError:
        pass

    logging.debug('trying "%s" as timedelta', input_str)
    try:
        return parse_timedelta_str(input_str)
    except ValueError:
        pass

    logging.debug('trying "%s" as datetime', input_str)
    try:
        return parse_datetime_str(input_str)
    except ValueError:
        pass

    logging.debug("no matches found")
    raise ValueError(
        f'unable to parse "{input_str}" as either a datetime nor a timedelta'
    )

Parse a string as a datetime, timedelta, or timezone

Args

input_str
The input string to parse.

Returns

A datetime object if the string represents a date, or a timedelta object if it represents a time delta.

Raises

ValueError
If the input string cannot be parsed as either a datetime

or a timedelta.

Examples

>>> parse_temporal_str('1d')
datetime.timedelta(...)
>>> parse_temporal_str('tomorrow')
datetime.datetime(...)
def parse_timedelta_str(input_str: str) ‑> datetime.timedelta
Expand source code
def parse_timedelta_str(input_str: str) -> timedelta:
    """
    Parses a string containing time delta information and returns a timedelta object.

    Args:
        input_str: A string containing the time delta information.

    Returns:
        A timedelta object representing the parsed time delta.

    Raises:
        ValueError: If the input_str is not a valid time delta string.

    Note:
        Years and months use fixed approximations, not calendar-accurate values:

        - 1 year = 365 days (ignores leap years)
        - 1 month = 30 days (ignores actual month lengths)

        This means "1 year" always equals exactly 365 days, regardless of
        whether it spans a leap year. Similarly, "1 month" is always 30 days,
        not the actual number of days in any particular month.

        For calendar-accurate arithmetic (e.g., "add 1 month" meaning the same
        day next month), use ``dateutil.relativedelta`` instead.

    Examples:
        Basic usage:

        >>> parse_timedelta_str('1d')
        datetime.timedelta(days=1)
        >>> parse_timedelta_str('1 day')
        datetime.timedelta(days=1)
        >>> parse_timedelta_str('2h 30m')
        datetime.timedelta(seconds=9000)
        >>> parse_timedelta_str('1.5h')
        datetime.timedelta(seconds=5400)
        >>> parse_timedelta_str('500ms')
        datetime.timedelta(microseconds=500000)

        Combining multiple units:

        >>> parse_timedelta_str('1 day 6.5 hours, 10 min 33s 3 year')
        datetime.timedelta(days=1096, seconds=24033)

        Year and month approximations:

        >>> parse_timedelta_str('1 year')
        datetime.timedelta(days=365)
        >>> parse_timedelta_str('1 month')
        datetime.timedelta(days=30)
        >>> parse_timedelta_str('12 months')  # Not exactly 1 year!
        datetime.timedelta(days=360)
        >>> parse_timedelta_str('1 year') == parse_timedelta_str('365 days')
        True
        >>> parse_timedelta_str('1 year') == parse_timedelta_str('12 months')
        False
    """
    match = _TIMEDELTA_PATTERN.match(input_str)
    if not match:
        raise ValueError("Invalid time delta string")
    parsed = match.groupdict()

    def fget(key: str) -> float:
        """
        return the float value corresponding to the given key in the parsed
        dict
        """
        val = parsed.get(key, "0")
        if val is None:
            val = "0"
        return float(val)

    return timedelta(
        days=(fget("years") * 365)
        + (fget("months") * 30)
        + (fget("weeks") * 7)
        + fget("days"),
        hours=fget("hours"),
        minutes=fget("minutes"),
        seconds=fget("seconds"),
        microseconds=fget("microseconds"),
        milliseconds=fget("milliseconds"),
    )

Parses a string containing time delta information and returns a timedelta object.

Args

input_str
A string containing the time delta information.

Returns

A timedelta object representing the parsed time delta.

Raises

ValueError
If the input_str is not a valid time delta string.

Note

Years and months use fixed approximations, not calendar-accurate values:

  • 1 year = 365 days (ignores leap years)
  • 1 month = 30 days (ignores actual month lengths)

This means "1 year" always equals exactly 365 days, regardless of whether it spans a leap year. Similarly, "1 month" is always 30 days, not the actual number of days in any particular month.

For calendar-accurate arithmetic (e.g., "add 1 month" meaning the same day next month), use dateutil.relativedelta instead.

Examples

Basic usage:

>>> parse_timedelta_str('1d')
datetime.timedelta(days=1)
>>> parse_timedelta_str('1 day')
datetime.timedelta(days=1)
>>> parse_timedelta_str('2h 30m')
datetime.timedelta(seconds=9000)
>>> parse_timedelta_str('1.5h')
datetime.timedelta(seconds=5400)
>>> parse_timedelta_str('500ms')
datetime.timedelta(microseconds=500000)

Combining multiple units:

>>> parse_timedelta_str('1 day 6.5 hours, 10 min 33s 3 year')
datetime.timedelta(days=1096, seconds=24033)

Year and month approximations:

>>> parse_timedelta_str('1 year')
datetime.timedelta(days=365)
>>> parse_timedelta_str('1 month')
datetime.timedelta(days=30)
>>> parse_timedelta_str('12 months')  # Not exactly 1 year!
datetime.timedelta(days=360)
>>> parse_timedelta_str('1 year') == parse_timedelta_str('365 days')
True
>>> parse_timedelta_str('1 year') == parse_timedelta_str('12 months')
False