Skip to content

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.

  1. c.date_parse(main_format, *other_formats, default=_none) and date_parse method parse dates
  2. c.datetime_parse(main_format, *other_formats, default=_none) and datetime_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: year
  • mo: month
  • sun/mon/tue/wed/thu/fri/sat: days of week
  • d: day
  • h: hour
  • m: minute
  • s: second
  • ms: millisecond
  • us: microsecond

Step examples:

  • -2d8h10us means minus 2 days 8 hours and 10 microseconds
  • 2tue every other Tuesday
  • 3mo 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.

  1. c.date_trunc and date_trunc truncate dates
  2. c.datetime_trunc and datetime_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 truncating
  • offset (optional step) defines the offset of the date/datetime grid to be applied
  • mode (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.

  1. DateGrid generates date grids
  2. DateTimeGrid generates datetime 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 grid
  • mode (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),
]