Dates¶
Please, make sure you've covered Basics first.
Formatting dates¶
Performance
Many format codes are optimized for speed: %% %A %a %B %H %I %M %S %Y %b %d %f %m %p %u %w %y.
If any other are passed, the implementation falls back to datetime.strftime.
See performance section of Benefits page for details.
c.format_dt(fmt) accepts same format codes as
datetime.strftime does.
from datetime import date, datetime
from convtools import conversion as c
# optimized
converter = c.format_dt("%m/%d/%Y %H:%M %p").gen_converter(debug=True)
assert converter(datetime(2023, 7, 27, 12, 13)) == "07/27/2023 12:13 PM"
# falls back to even faster standard isoformat()
converter = c.format_dt("%Y-%m-%d").gen_converter(debug=True)
assert converter(date(2020, 12, 31)) == "2020-12-31"
# falls back to standard strftime()
converter = c.format_dt("%Y-%j").gen_converter(debug=True)
assert converter(date(2020, 12, 31)) == "2020-366"
Parsing dates¶
Performance
Many format codes are optimized for speed: %% %Y %m %d %H %I %p %M %S %f.
If any other are passed, the implementation falls back to datetime.strptime.
See performance section of Benefits page for details.
c.date_parse(main_format, *other_formats, default=_none)anddate_parsemethod parse datesc.datetime_parse(main_format, *other_formats, default=_none)anddatetime_parsemethod parse datetimes
Both accept one or more formats, supported by datetime.strptime.
from datetime import date, datetime
from convtools import conversion as c
# SINGLE FORMAT
converter = c.date_parse("%m/%d/%Y").gen_converter(debug=True)
assert converter("12/31/2020") == date(2020, 12, 31)
# MULTIPLE FORMATS
converter = (
c.iter(c.date_parse("%m/%d/%Y", "%Y-%m-%d"))
.as_type(list)
.gen_converter(debug=True)
)
assert converter(["12/31/2020", "2021-01-01"]) == [
date(2020, 12, 31),
date(2021, 1, 1),
]
# SAME FOR DATETIMES, BUT LET'S USE A METHOD
converter = (
c.item("dt").datetime_parse("%m/%d/%Y %H:%M").gen_converter(debug=True)
)
assert converter({"dt": "12/31/2020 15:40"}) == datetime(
2020, 12, 31, 15, 40
)
Let's use default, so it is returned in cases where neither of provided
formats fit:
from datetime import date, datetime
from convtools import conversion as c
# SIMPLE DEFAULT
converter = c.date_parse("%m/%d/%Y", default=None).gen_converter(debug=True)
assert converter("some str") is None
# DEFAULT AS CONVERSION
converter = c.date_parse(
"%m/%d/%Y", default=c.call_func(date.today)
).gen_converter(debug=True)
assert converter("some str") == date.today()
Date/Time Step¶
For the sake of convenience (kudos to Polars for the idea), let's introduce a definition of date/time step, to be used below. It's a concatenated string, which may contain an optional negative sign and then numbers and suffixes:
y: yearmo: monthsun/mon/tue/wed/thu/fri/sat: days of weekd: dayh: hourm: minutes: secondms: millisecondus: microsecond
Step examples:
-2d8h10usmeans minus 2 days 8 hours and 10 microseconds2tueevery other Tuesday3moevery quarter
It also accepts datetime.timedelta.
Truncating dates¶
Dates and datetimes can be truncated by applying a date/datetime grid, which is
defined by step and offset.
c.date_truncanddate_trunctruncate datesc.datetime_truncanddatetime_trunctruncate datetimes, preserving timezone of the passed datetime
The methods above have the parameters:
step(step) defines the period of date/datetime grid to be used when truncatingoffset(optional step) defines the offset of the date/datetime grid to be appliedmode(default is"start") defines which part of a date/datetime grid period is to be returned"start"returns the beginning of a grid period"end_inclusive"returns the inclusive end of a grid period (e.g. for a monthly grid for Jan: it's Jan 31st)"end"returns the exclusive end of a grid period (e.g. for a monthly grid for Jan: it's Feb 1st)
Warning
- y/mo steps support only y/mo offsets
- days of week don't support offsets (otherwise we would get undesired days of week)
- when truncating dates, not datetimes, it is possible for whole number of days only
- any steps defined as deterministic units (d, h, m, s, ms, us) can only be used with offsets defined by deterministic units too
from datetime import date, datetime
from convtools import conversion as c
# TRUNCATE TO MONTHS
converter = c.iter(c.date_trunc("mo")).as_type(list).gen_converter(debug=True)
assert converter(
[
date(1999, 12, 31),
date(2000, 1, 10),
date(2000, 2, 20),
]
) == [date(1999, 12, 1), date(2000, 1, 1), date(2000, 2, 1)]
# TRUNCATE TO MONTHS, RETURNS INCLUSIVE ENDS
converter = (
c.iter(c.date_trunc("mo", mode="end_inclusive"))
.as_type(list)
.gen_converter(debug=True)
)
assert converter(
[
date(1999, 12, 31),
date(2000, 1, 10),
date(2000, 2, 20),
]
) == [date(1999, 12, 31), date(2000, 1, 31), date(2000, 2, 29)]
# TRUNCATE TO 8h GRID, SHIFTED 6h FORWARD
converter = (
c.iter(
{
"start": c.datetime_trunc("8h", "6h"),
"end_inclusive": c.datetime_trunc(
"8h", "6h", mode="end_inclusive"
),
}
)
.as_type(list)
.gen_converter(debug=True)
)
assert converter(
[
datetime(1999, 12, 31, 21, 1),
datetime(2000, 1, 1, 2, 2),
datetime(2000, 1, 1, 9, 3),
datetime(2000, 1, 1, 15, 4),
]
) == [
{
"start": datetime(1999, 12, 31, 14, 0),
"end_inclusive": datetime(1999, 12, 31, 21, 59, 59, 999999),
},
{
"start": datetime(1999, 12, 31, 22, 0),
"end_inclusive": datetime(2000, 1, 1, 5, 59, 59, 999999),
},
{
"start": datetime(2000, 1, 1, 6, 0),
"end_inclusive": datetime(2000, 1, 1, 13, 59, 59, 999999),
},
{
"start": datetime(2000, 1, 1, 14, 0),
"end_inclusive": datetime(2000, 1, 1, 21, 59, 59, 999999),
},
]
Date grids¶
Date grids are not conversions, these are just helper functions which generate gap free series of dates/datetimes.
e.g. DateGrid("mo").around(dt_start, dt_end) returns an iterator of dates of
the monthly grid, which contains the provided period.
Note
It is intentionally different from Postgres' generate_series(start, end,
interval) because it is not convenient in some cases, where you need to
truncate start and end to a required precision first, otherwise you
risk missing the very first period.
DateGridgeneratesdategridsDateTimeGridgeneratesdatetimegrids
Arguments are:
step(step) defines grid period length (see how STEP-STRING is defined above)offset(optional step) defines the offset of the date/datetime gridmode(default is"start") defines which part of a date/datetime grid period is to be returned"start"returns the beginning of a grid period"end_inclusive"returns the inclusive end of a grid period (e.g. for a monthly grid for Jan: it's Jan 31st)"end"returns the exclusive end of a grid period (e.g. for a monthly grid for Jan: it's Feb 1st)
from datetime import date, datetime
from convtools import DateGrid, DateTimeGrid
from convtools import conversion as c
# MONTHS
assert list(DateGrid("mo").around(date(1999, 12, 20), date(2000, 1, 3))) == [
date(1999, 12, 1),
date(2000, 1, 1),
]
# ENDS OF QUARTERS
assert list(
DateGrid("3mo", mode="end_inclusive").around(
date(1999, 12, 20), date(2000, 12, 3)
)
) == [
date(1999, 12, 31),
date(2000, 3, 31),
date(2000, 6, 30),
date(2000, 9, 30),
date(2000, 12, 31),
]
# EVERY 4TH THURSDAY
assert list(DateGrid("4thu").around(date(1999, 12, 20), date(2000, 5, 3))) == [
date(1999, 12, 16),
date(2000, 1, 13),
date(2000, 2, 10),
date(2000, 3, 9),
date(2000, 4, 6),
]
# EVERY 8 HOURS SHIFTED BY 6 HOURS
assert list(
DateTimeGrid("8h", "6h").around(
datetime(2000, 2, 28, 15, 0), datetime(2000, 3, 1, 15, 0)
)
) == [
datetime(2000, 2, 28, 14, 0),
datetime(2000, 2, 28, 22, 0),
datetime(2000, 2, 29, 6, 0),
datetime(2000, 2, 29, 14, 0),
datetime(2000, 2, 29, 22, 0),
datetime(2000, 3, 1, 6, 0),
datetime(2000, 3, 1, 14, 0),
]