timelength

A flexible python duration parser designed for human readable lengths of time, including long form durations such as 1 day, 5 hours, and 30 seconds
, short form durations such as 1d5h30s
, numerals such as twelve hours
, HHMMSS such as 12:30:15
, and a mix thereof such as 1 day 5h 30s
.
Installation
timelength
can be installed via pip:
pip install timelength
Or added to your project via poetry:
poetry add timelength
Usage
Basic
Import TimeLength
and pass it a string to evaluate. As long as a single valid result is found the parsing will be considered a success regardless of any invalid content included in the input.
from datetime import datetime, timezone
from timelength import TimeLength
tl = TimeLength("1.27 hours and 23 miles")
print(tl.result.success)
print(tl.result.seconds)
print(tl.to_minutes(max_precision=3))
print(tl.result.delta)
print(tl.ago(base=datetime(2025, 1, 1, 0, 0, 0, 0, timezone.utc)))
print(tl.hence(base=datetime(2025, 1, 1, 0, 0, 0, 0, timezone.utc)))
print(tl.result.invalid)
print(tl.result.valid)
Advanced
Control can be increased by making use of FailureFlags
and ParserSettings
. Both are passed to whichever Locale
you are parsing with which is then passed to the TimeLength
to be parsed.
FailureFlags
is an IntFlag enum which holds all the currently possible reasons for a parse to fail. The default is FailureFlags.NONE
which means as long as a single value is parsed, it will be considered a success. To achieve the opposite behavior, FailureFlags.ALL
will cause the parsing to be considered a failure if any FailureFlags
are detected, even if a valid value was parsed. See below for a full list.
ParserSettings
is an object with various parsing settings. See below for a full list of options along with the default values.
In the first part of the example below, the flags are set to (FailureFlags.LONELY_VALUE | FailureFlags.UNKNOWN_TERM)
, which means as long as a single valid item is found, the parsing will only be considered a failure if FailureFlags.LONELY_VALUE
or FailureFlags.UNKNOWN_TERM
show up in the invalid items. As 19
is included in the input without an accompanying scale, it is considered a LONELY_VALUE
and thus invalid.
In the second part, the flags are reset, meaning nothing will cause parsing to be considered a failure as long as a single valid item is found. Additionally, the settings are updated such that duplicate scales are not allowed to be parsed. Due to this, 5m
from the input is added to the invalid items as a DUPLICATE_SCALE
due to the preceding 35m
. Also, 19
is still in the invalid items as a LONELY_VALUE
. Despite this, since the flags is set to FailureFlags.NONE
and since 3.5d, 35m
is successfully parsed, the overall parsing is considered a success.
from timelength import English, FailureFlags, ParserSettings, TimeLength
flags = (FailureFlags.LONELY_VALUE | FailureFlags.UNKNOWN_TERM)
locale = English(flags=flags)
tl = TimeLength("3.5d, 35m, 5m, 19", locale=locale)
print(tl.result.success)
print(tl.result.invalid)
print(tl.result.valid)
flags = FailureFlags.NONE
settings = ParserSettings(allow_duplicate_scales=False)
locale.flags = flags
locale.settings = settings
tl.parse()
print(tl.result.success)
print(tl.result.invalid)
print(tl.result.valid)
To put it simply, FailureFlags
is used to determine if an item that is in the invalid items tuple should invalidate the parsing as a whole, whereas ParserSettings
is used to determine if certain customizable situations should even result in an item being added to the invalid items tuple to begin with.
FailureFlags
The members of the FailureFlags
IntEnum are:
NONE
— No failures will cause parsing to fail.ALL
— Any failure will cause parsing to fail.MALFORMED_CONTENT
— The fallback when something that shouldn't have happened, happened.UNKNOWN_TERM
— The parsed value was not recognized from the terms or symbols in the config.
MALFORMED_DECIMAL
— Multiple decimals were attempted within a singular decimal segment.
MALFORMED_THOUSAND
— A thousand segment was attempted but did not have a leading number or a proper number of digits following a thousand separator.
- Ex:
,234
, 1,23
, or 1,2345
MALFORMED_FRACTION
— A fraction was attempted but had more than 2 values, a missing value, or was not formatted correctly.
- Ex:
1/2/3
, /2
, or 1+ / +2
MALFORMED_HHMMSS
— An HH:MM:SS was attempted but had more segments than enabled scales or was not formatted correctly.
- Ex:
1:2:3:4:5:6:7:8:9:10:11:12:13:14:15
or 1:15:26:
LONELY_VALUE
— A value was parsed with no paired scale.
- Ex:
one minute and twenty
LONELY_SCALE
— A scale was parsed with no paired value.
DUPLICATE_SCALE
— The same scale was parsed multiple times.
- Ex:
1min 5:23
or 1min 5 minutes and 23 seconds
CONSECUTIVE_CONNECTOR
— More than the allowed number of connectors were parsed in a row.
CONSECUTIVE_SEGMENTOR
— More than the allowed number of segmentors were parsed in a row.
CONSECUTIVE_SPECIAL
— More than the allowed number of special characters were parsed in a row.
MISPLACED_ALLOWED_TERM
— An allowed term was found in the middle of a segment/sentence.
MISPLACED_SPECIAL
— A special character was found in the middle of a segment/sentence.
UNUSED_OPERATION
— A term of the operation numeral was parsed but unused on any values.
AMBIGUOUS_MULTIPLIERS
— More than one multiplier was parsed for a single segment which may be ambiguous.
- Ex:
half of a quarter of two minutes and 30s
ParserSettings
- assume_scale:
Literal["LAST", "SINGLE", "NEVER"] = "SINGLE"
— How to handle no scale being provided.
LAST
will assume seconds only for the last value if no scale is provided for it.SINGLE
will only assume seconds when a single input is provided.NEVER
will never assume seconds when no scale is provided.
- limit_allowed_terms:
bool = True
— Prevent terms from the allowed_terms
list in
the config from being used in the middle of a segment, thus interrupting a value/scale pair.
- The affected segment will become abandoned and added to the invalid list.
- The terms may still be used at the beginning or end of a segment/sentence.
- If
False
, The terms will be ignored (within other limitations) and not effect parsing.
- allow_duplicate_scales:
bool = True
— Allow scales to be parsed multiple times, stacking their values.
- If
False
, the first scale will be used and subsequent duplicates will be added to the invalid list.
- allow_thousands_extra_digits:
bool = False
— Allow thousands to be parsed with more than three digits following a thousand delimiter.
- Ex:
1,2345
will be interpreted as 12,345
.
- allow_thousands_lacking_digits:
bool = False
— Allow a number to be parsed with less than three digits following a thousand delimiter.
- Ex:
1,23
will be interpreted as 123
.
- allow_decimals_lacking_digits:
bool = True
— Allow decimals to be parsed with no number following the
decimal delimiter.
- Ex:
1.
will be interpreted as 1.0
.
Guess the Locale
If you're unsure of which locale the input will belong to, you can attempt to guess the locale from the available options.
from timelength import English, Spanish, Guess, TimeLength
guess = Guess()
tl = TimeLength("5 minutos", locale=guess)
print(tl.locale)
print(tl.result.success)
print(tl.result.valid)
tl.content = "5 minutes"
tl.parse(guess_locale=guess)
print(tl.locale)
print(tl.result.success)
print(tl.result.valid)
To guess the locale you can pass in an instantiated Guess
either on creation of the TimeLength
or to the parse()
function on subsequent calls. Doing so will save locale instantiations as with each new Guess
created, each locale available is instantiated in Guess().locales
. Alternatively, for parse()
, you can pass True
for guess_locale
and a new Guess
will be instantiated automatically.
Guessing works by parsing with each locale. The one with the least invalid results is considered the correct locale. Ties are broken by most valid followed by alphabetically.
Any flags or settings passed to the Guess
will be passed on to each locale. If none are provided, the defaults in each locale's configs are used. If you need to specify flags or settings per locale, you can directly access Guess().locales
, which is a list of instantiated versions of all of the currently available locales.
If you have any custom locales you would like to be included in the possible results, append to Guess().locales
before passing it to the TimeLength
or parse()
function.
Operations & Comparisons
TimeLength
objects support various arithmetic operations and comparisons between each other, datetimes, timedelta, and numbers. The supported options are:
-
Addition
TimeLength
+ TimeLength
or timedelta
or number -> TimeLength
datetime
or timedelta
+ TimeLength
-> datetime
or timedelta
-
Subtraction
TimeLength
- TimeLength
or timedelta
or number -> TimeLength
datetime
or timedelta
- TimeLength
-> datetime
or timedelta
-
Multiplication
TimeLength
* number -> TimeLength
- number *
TimeLength
-> TimeLength
-
Division
TimeLength
/ TimeLength
or timedelta
or number -> TimeLength
or float
timedelta
/ TimeLength
-> float
-
Floor Division
TimeLength
// TimeLength
or timedelta
or number -> TimeLength
or float
timedelta
// TimeLength
-> float
-
Modulo
TimeLength
% TimeLength
or timedelta
-> TimeLength
timedelta
% TimeLength
-> timedelta
-
Divmod
divmod(TimeLength, TimeLength or timedelta)
-> tuple[float, TimeLength]
divmod(timedelta, TimeLength)
-> tuple[float, timedelta]
-
Power
TimeLength
** number -> TimeLength
(optionally moduloed by TimeLength
or timedelta
)
-
Comparisons
TimeLength
> TimeLength
or timedelta
-> bool
TimeLength
>= TimeLength
or timedelta
-> bool
TimeLength
< TimeLength
or timedelta
-> bool
TimeLength
<= TimeLength
or timedelta
-> bool
TimeLength
== TimeLength
or timedelta
-> bool
TimeLength
!= TimeLength
or timedelta
-> bool
-
Other Operations
abs(TimeLength)
-> TimeLength
(returns self
unchanged as TimeLength
is an absolute measurement)+TimeLength
-> TimeLength
(returns self
unchanged)-TimeLength
-> TimeLength
(returns self
unchanged)bool(TimeLength)
-> bool
(returns True
if parsing succeeded, otherwise False
)len(TimeLength)
-> int
(returns the length of self.content
)
Example Inputs
1m
1min
1 Minute
1m and 2 SECONDS
3h, 2 min, 3sec
1.2d
1,234s
one hour
twenty-two hours and thirty five minutes
half of a day
1/2 of a day
1/4 hour
1 Day, 2:34:12
1:2:34:12
1:5:13:27:22
Usage Notes
- Numerals
- Segmentors are ignored when parsing numerals in order to achieve consistency over ambiguity.
- Multipliers
- A single multiplier (ex:
half
) is allowed per segment (value + scale). This is due to the ambiguity introduced when more than one multiplier is used. May be revisited in the future if a good way to handle this ambiguity is found.
- HHMMSS
- It is not strictly adherent to typical
HH:MM:SS
standards. Any parsable numbers work in each slot, whether they are single digits, multiple digits, or include decimals.
- For example,
2.56:27/3:270:19.2231
is a valid input in place of 2.56 days, 9 hours, 270 minutes, and 19.2231 seconds
. - It also accepts a single connector, such as a space, between deliminators. Example:
2: 6: 3
.
- Supports up to as many segments as there are scales defined, including custom scales (10 default,
Millisecond
to Century
).
- The segments are parsed in reverse order, so smallest to largest (the order defined in the config and therefore the order loaded into the
Locale
, may differ for custom defined Scale
s) to ensure the correct scales are applied.
- EXCEPTION: The default base from which
HH:MM:SS
starts at is Second
. Any scales (typically of lesser value) listed prior in the config, or appended to the scales list before Second
in the case of custom scales, will not be utilized unless Second
is disabled or as many HH:MM:SS
segments are parsed as there are scales defined. 12:30:15
is 12 hours, 30 minutes, and 15 seconds
, NOT 12 minutes, 30 seconds, and 15 milliseconds
.1:10:100:12:52:30:24:60:60:1000
will make use of Century
to Millisecond
.
Supported Locales
- English
- Spanish
- Basic Custom
- Copy & modify an existing config with new terms as long as your new
Locale
follows the existing config parser's grammar structure
- Advanced Custom
- Write your own parsing logic if your
Locale
's grammar structure differs too drastically (PRs welcome)
Customization
timelength
allows for customizing the parsing behavior through JSON configuration. To get started, copy an existing locale JSON in timelength/locales/
. The custom JSON may be placed anywhere.
Ensure the JSON being used is from a trusted source, as the parser is loaded dynamically based on the file specified in the JSON. This could allow for unintended code execution if an unsafe config is loaded.
Valid JSONs must include the following keys:
connectors
- Characters/words that join two parts of the same segment.
- Must have at least one value.
segmentors
- Characters/words that separate an input into segments.
- Must have at least one value.
allowed_terms
- Characters or terms that won't be categorized as an invalid input. If sent multiple times in a row (ex:
!!
), they will still be marked as invalid. If ParserSettings().limit_allowed_terms
is set then these characters won't be allowed in the middle of a segment (ex: 1!min
) and will cause the segment progress to reset.
hhmmss_delimiters
- Characters used to form
HH:MM:SS
segments. Can't have overlap with decimal_delimiters
, thousand_delimiters
, or fraction_delimiters
.
decimal_delimiters
- Characters used to separate decimals from digits. Can't have overlap with
hhmmss_delimiters
, thousand_delimiters
, or fraction_delimiters
.
thousand_delimiters
- Characters used to break up large numbers. Can't have overlap with
hhmmss_delimiters
, decimal_delimiters
, or fraction_delimiters
.
fraction_delimiters
- Characters used to form fractions. Can't have overlap with
hhmmss_delimiters
, decimal_delimiters
, or thousand_delimiters
.
parser_file
- The name of this locale's parser file (extension included) located in
timelength/parsers/
, or the path to the parser file if stored elsewhere. - Ensure only a trusted file is used as this could allow unintended code execution.
- The internal parser method must share a name with the file. Example:
parser_one.py
and def parser_one()
.
scales
- Periods of time. The defaults are
millisecond
, second
, minute
, hour
, day
, week
, month
, year
, decade
, and century
. Default scales must be present. At least one scale must be valid and enabled. Custom scales can be added following the format of the others. The following keys must be present and populated:
- scale
- The number of seconds this scale represents.
- singular
- The lowercase singular form of this scale.
- plural
- The lowercase plural form of this scale.
- terms
- All terms that could be parsed as this scale. Accents and other NFKD markings should not be present as they are filtered from the user input.
- The following key is optional to disable a scale without removing it from the config:
- enabled
- A bool indicating if the scale is enabled or not.
numerals
- Word forms of numbers. May be populated or left empty. Each element must itself have the following keys, even if their contents are not used:
type
value
- The numerical value of this numeral.
terms
- Characters/words that parse to this numeral's value.
flags
- A list of
FailureFlags
which will cause parsing to be considered a failure. Values should be upper cased. This is optional and can be passed to the locale dynamically instead. See a full list of options in the FailureFlags section.
settings
- A dictionary of options to modify parsing behavior. Each key should have a string or boolean value as appropriate. This is optional and can be passed to the locale dynamically instead. See a full list of options in the ParserSettings section.
extra_data
- Any data a parser needs that is not already covered. May be populated or left empty. The locale loads this into a
Locale().extra_data
attribute, leaving the parser to utilize it.
Once your custom JSON is filled out, you can use it as follows:
from timelength import TimeLength, Locale
output = TimeLength("30 minutes", locale = Locale("path/to/config.json"))
If all goes well, the parsing will succeed, and if not, an error will point you in the right direction.