Dates¶
Warning
Please, make sure you've covered Reference / 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("%c").gen_converter(debug=True)
assert converter(date(2020, 12, 31)) == "Thu Dec 31 00:00:00 2020"
def converter(data_, *, __datetime=__naive_values__["__datetime"], __v=__naive_values__["__v"]):
try:
is_datetime = isinstance(data_, __datetime)
hour = data_.hour if is_datetime else 0
return f"{data_.month:02}/{data_.day:02}/{data_.year:04} {hour:02}:{(data_.minute if is_datetime else 0):02} {__v[hour // 12]}"
except __exceptions_to_dump_sources:
__convtools__code_storage.dump_sources()
raise
def converter(data_, *, __datetime=__naive_values__["__datetime"]):
try:
return data_.date().isoformat() if isinstance(data_, __datetime) else data_.isoformat()
except __exceptions_to_dump_sources:
__convtools__code_storage.dump_sources()
raise
def converter(data_, *, __datetime=__naive_values__["__datetime"], __strftime=__naive_values__["__strftime"], __v=__naive_values__["__v"]):
try:
return __strftime(data_, __v)
except __exceptions_to_dump_sources:
__convtools__code_storage.dump_sources()
raise
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_parse
method parse datesc.datetime_parse(main_format, *other_formats, default=_none)
anddatetime_parse
method 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
)
def datetime_parse(data_, *, __datetime=__naive_values__["__datetime"], __v=__naive_values__["__v"]):
match = __v.match(data_)
if not match:
raise ValueError("time data %r does not match format %r" % (data_, """%m/%d/%Y"""))
if len(data_) != match.end():
raise ValueError("unconverted data remains: %s" % data_string[match.end() :])
groups_ = match.groups()
return __datetime(int(groups_[2]), int(groups_[0]), int(groups_[1]), 0, 0, 0, 0)
def converter(data_):
try:
return datetime_parse(data_).date()
except __exceptions_to_dump_sources:
__convtools__code_storage.dump_sources()
raise
def datetime_parse(data_, *, __datetime=__naive_values__["__datetime"], __v=__naive_values__["__v"]):
match = __v.match(data_)
if not match:
raise ValueError("time data %r does not match format %r" % (data_, """%m/%d/%Y"""))
if len(data_) != match.end():
raise ValueError("unconverted data remains: %s" % data_string[match.end() :])
groups_ = match.groups()
return __datetime(int(groups_[2]), int(groups_[0]), int(groups_[1]), 0, 0, 0, 0)
def datetime_parse_e(data_, *, __datetime=__naive_values__["__datetime"], __v_q=__naive_values__["__v_q"]):
match = __v_q.match(data_)
if not match:
raise ValueError("time data %r does not match format %r" % (data_, """%Y-%m-%d"""))
if len(data_) != match.end():
raise ValueError("unconverted data remains: %s" % data_string[match.end() :])
groups_ = match.groups()
return __datetime(int(groups_[0]), int(groups_[1]), int(groups_[2]), 0, 0, 0, 0)
def try_multiple(data_, *, __v_i=__naive_values__["__v_i"]):
try:
return datetime_parse(data_).date()
except __v_i:
pass
return datetime_parse_e(data_).date()
def converter(data_):
try:
return [try_multiple(i) for i in data_]
except __exceptions_to_dump_sources:
__convtools__code_storage.dump_sources()
raise
def datetime_parse(data_, *, __datetime=__naive_values__["__datetime"], __v=__naive_values__["__v"]):
match = __v.match(data_)
if not match:
raise ValueError("time data %r does not match format %r" % (data_, """%m/%d/%Y %H:%M"""))
if len(data_) != match.end():
raise ValueError("unconverted data remains: %s" % data_string[match.end() :])
groups_ = match.groups()
return __datetime(int(groups_[2]), int(groups_[0]), int(groups_[1]), int(groups_[3]), int(groups_[4]), 0, 0)
def converter(data_):
try:
return datetime_parse(data_["dt"])
except __exceptions_to_dump_sources:
__convtools__code_storage.dump_sources()
raise
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()
def datetime_parse(data_, *, __datetime=__naive_values__["__datetime"], __v=__naive_values__["__v"]):
match = __v.match(data_)
if not match:
raise ValueError("time data %r does not match format %r" % (data_, """%m/%d/%Y"""))
if len(data_) != match.end():
raise ValueError("unconverted data remains: %s" % data_string[match.end() :])
groups_ = match.groups()
return __datetime(int(groups_[2]), int(groups_[0]), int(groups_[1]), 0, 0, 0, 0)
def try_multiple(data_, *, __v_i=__naive_values__["__v_i"]):
try:
return datetime_parse(data_).date()
except __v_i:
pass
return None
def converter(data_):
try:
return try_multiple(data_)
except __exceptions_to_dump_sources:
__convtools__code_storage.dump_sources()
raise
def datetime_parse(data_, *, __datetime=__naive_values__["__datetime"], __v=__naive_values__["__v"]):
match = __v.match(data_)
if not match:
raise ValueError("time data %r does not match format %r" % (data_, """%m/%d/%Y"""))
if len(data_) != match.end():
raise ValueError("unconverted data remains: %s" % data_string[match.end() :])
groups_ = match.groups()
return __datetime(int(groups_[2]), int(groups_[0]), int(groups_[1]), 0, 0, 0, 0)
def try_multiple(data_, *, __today=__naive_values__["__today"], __v_e=__naive_values__["__v_e"]):
try:
return datetime_parse(data_).date()
except __v_e:
pass
return __today()
def converter(data_):
try:
return try_multiple(data_)
except __exceptions_to_dump_sources:
__convtools__code_storage.dump_sources()
raise
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:
-2d8h10us
means minus 2 days 8 hours and 10 microseconds2tue
every other Tuesday3mo
every 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_trunc
anddate_trunc
truncate datesc.datetime_trunc
anddatetime_trunc
truncate 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"
return 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),
},
]
def converter(data_, *, __date_trunc_to_month=__naive_values__["__date_trunc_to_month"]):
try:
return [__date_trunc_to_month(i, 1, 0, 1) for i in data_]
except __exceptions_to_dump_sources:
__convtools__code_storage.dump_sources()
raise
def converter(data_, *, __date_trunc_to_month=__naive_values__["__date_trunc_to_month"]):
try:
return [__date_trunc_to_month(i, 1, 0, 3) for i in data_]
except __exceptions_to_dump_sources:
__convtools__code_storage.dump_sources()
raise
def converter(data_, *, __datetime_trunc_to_microsecond=__naive_values__["__datetime_trunc_to_microsecond"]):
try:
return [
{
"start": __datetime_trunc_to_microsecond(i, 28800000000, 21600000000, 1),
"end_inclusive": __datetime_trunc_to_microsecond(i, 28800000000, 21600000000, 3),
}
for i in data_
]
except __exceptions_to_dump_sources:
__convtools__code_storage.dump_sources()
raise
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.
DateGrid
generatesdate
gridsDateTimeGrid
generatesdatetime
grids
Arguments are:
step
(step) defines grid period length (see how STEP-STRING is defined above)offset
(optional step) defined 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"
return 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),
]