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_dateParse 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.relativedeltainstead.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