Date Utilities#

High-performance date manipulation and actuarial age/duration functions following Spanish and international conventions. Key capabilities:

  • Actuarial age — Age Last Birthday (ALB), Age Nearest Birthday (ANB), Age Next Birthday (ANEXT) from calendar birthdays (m=1), plus exact fractional age. FormatDates length-1 (e.g. make_date) re-enters as a scalar.

  • Duration calculations — days, complete calendar months, and fractional years (Actual/Actual ISDA or days/365.25 approximation).

  • Anniversary dates — generation of payment or projection date grids at any frequency (\(m\) = 1, 2, 3, 4, 6, 12, 24, 26, 52, 365; m=14 raises ValueError — use m=12 with separate extraordinary cash flows for the Spanish “14 pagas” scheme).

  • Date construction and arithmetic — end-of-month dates, durations, formatting, and robust parsing of integers (YYYYMMDD), ISO strings, European and US slash formats, and third-party types (datetime.date, pandas.Timestamp, numpy.datetime64, Polars).

  • Calendar utilities — leap year detection, days in month, days in year.

The active date_format setting in Config controls which input representations are accepted and how output is formatted.

See also

Date Utilities Guide — Full guide to date parsing, format configuration, and actuarial age conventions.

Actuarial age#

lactuca.age_last_birthday(birth_date: DateLike | Sequence[DateLike], valuation_date: DateLike | Sequence[DateLike], *, day_count: DayCountMethodLiteral = 'act_act') float | NDArray[np.float64]#

Compute Age Last Birthday (ALB) - whole completed years.

Convenience wrapper for act_age(..., m=1, method='last') implementing the Age Last Birthday convention: the integer number of calendar birthdays completed on the valuation date. This is the standard age definition used in life insurance and pension actuarial work throughout international practice.

ALB increments on the exact birthday and remains constant until the next anniversary. For example, a person born on June 15, 1990 has ALB = 33 from June 15, 2023 through June 14, 2024, then ALB = 34 on June 15, 2024.

Parameters:
  • birth_date (DateLike or sequence of DateLike) – Birth date(s). Accepts integers (YYYYMMDD), strings, date/datetime objects, and pandas/Polars date types.

  • valuation_date (DateLike or sequence of DateLike) – Valuation or reference date(s). Must not precede corresponding birth date.

  • day_count ({'act_act', 'exact'}, default='act_act') – Accepted for API consistency with act_age(). Annual ALB uses calendar birthday boundaries, so day_count does not change the result.

Returns:

Age(s) at last birthday (whole years, always non-negative integers represented as float):

  • Python float for scalar inputs

  • NumPy ndarray of dtype float64 for sequence inputs

Return type:

float or numpy.ndarray

Notes

The returned values are floats (not integers) to maintain consistency with other age calculation functions and to support vectorized NumPy arrays. Values are guaranteed to be non-negative whole numbers (0.0, 1.0, 2.0, …).

ALB counts completed calendar birthdays (Feb-29 births use Feb-28 in non-leap years), not floor of a day-count fractional age. That avoids leap-year shortfalls where Actual/Actual can be slightly under an integer on an exact birthday.

Available in the top-level namespace as the compact alias lactuca.alb.

Examples

Standard ALB calculation:

>>> from lactuca import age_last_birthday
>>> from datetime import date
>>> age_last_birthday(date(1990, 6, 15), date(2024, 3, 1))
33.0

On exact birthday (increments to new age):

>>> age_last_birthday(date(1990, 6, 15), date(2024, 6, 15))
34.0

One day before birthday (still previous age):

>>> age_last_birthday(date(1990, 6, 15), date(2024, 6, 14))
33.0

Vectorized computation:

>>> births = [date(1990, 1, 1), date(1985, 6, 15), date(2000, 12, 31)]
>>> age_last_birthday(births, date(2024, 1, 1))
array([34., 38., 23.])

See also

act_age

General actuarial age function with configurable rounding.

age_nearest_birthday

ANB convention (nearest calendar birthday).

age_next_birthday

ANEXT convention (next calendar birthday).

age_exact

Exact fractional age without rounding.

lactuca.age_nearest_birthday(birth_date: DateLike | Sequence[DateLike], valuation_date: DateLike | Sequence[DateLike], *, day_count: DayCountMethodLiteral = 'act_act') float | NDArray[np.float64]#

Compute Age Nearest Birthday (ANB) - nearest calendar birthday.

Convenience wrapper for act_age(..., m=1, method='nearest') implementing the Age Nearest Birthday convention: choose the closer of the last and next calendar birthdays. Used where symmetric rounding around birthdays is preferred.

ANB transitions at the day midpoint between consecutive birthdays. An exact midpoint rounds toward the next birthday (half-up).

Parameters:
  • birth_date (DateLike or sequence of DateLike) – Birth date(s). Accepts integers (YYYYMMDD), strings, date/datetime objects, and pandas/Polars date types.

  • valuation_date (DateLike or sequence of DateLike) – Valuation or reference date(s). Must not precede corresponding birth date.

  • day_count ({'act_act', 'exact'}, default='act_act') – Accepted for API consistency with act_age(). Annual ANB uses calendar birthday distances, so day_count does not change the result.

Returns:

Age(s) at the nearest calendar birthday (as float):

  • Python float for scalar inputs

  • NumPy ndarray of dtype float64 for sequence inputs

Return type:

float or numpy.ndarray

Notes

Distances are measured in calendar days to the last and next birthday anniversaries (Feb-29 → Feb-28 in non-leap years). Equal distances round up to the next birthday. This is not np.rint of a day-count fractional age.

Available in the top-level namespace as the compact alias lactuca.anb.

Examples

Before the midpoint (closer to last birthday):

>>> from lactuca import age_nearest_birthday
>>> from datetime import date
>>> age_nearest_birthday(date(1990, 6, 15), date(2023, 9, 1))
33.0

After the midpoint (closer to next birthday):

>>> age_nearest_birthday(date(1990, 6, 15), date(2024, 3, 1))
34.0

Exactly on birthday:

>>> age_nearest_birthday(date(1990, 6, 15), date(2024, 6, 15))
34.0

Vectorized computation:

>>> births = [date(1990, 1, 1), date(1985, 6, 15)]
>>> age_nearest_birthday(births, date(2024, 1, 1))
array([34., 39.])

See also

act_age

General actuarial age function with configurable rounding.

age_last_birthday

ALB convention (last completed birthday).

age_next_birthday

ANEXT convention (next calendar birthday).

age_exact

Exact fractional age without rounding.

lactuca.age_next_birthday(birth_date: DateLike | Sequence[DateLike], valuation_date: DateLike | Sequence[DateLike], *, day_count: DayCountMethodLiteral = 'act_act') float | NDArray[np.float64]#

Compute Age Next Birthday (ANEXT) - age at next calendar birthday.

Convenience wrapper for act_age(..., m=1, method='next') implementing the Age Next Birthday convention: the integer age attained at the next upcoming birthday. Common in health insurance underwriting and some life pricing contexts that prefer a slightly older age.

On an exact birthday, ANEXT equals ALB (the birthday just attained), not ALB + 1. The day after the birthday, ANEXT advances to the following age.

Parameters:
  • birth_date (DateLike or sequence of DateLike) – Birth date(s). Accepts integers (YYYYMMDD), strings, date/datetime objects, and pandas/Polars date types.

  • valuation_date (DateLike or sequence of DateLike) – Valuation or reference date(s). Must not precede corresponding birth date.

  • day_count ({'act_act', 'exact'}, default='act_act') – Accepted for API consistency with act_age(). Annual ANEXT uses calendar birthday boundaries, so day_count does not change the result.

Returns:

Age(s) at next birthday (as float), equal to ALB on the birthday itself:

  • Python float for scalar inputs

  • NumPy ndarray of dtype float64 for sequence inputs

Return type:

float or numpy.ndarray

Notes

ANEXT is the calendar age at the next birthday anniversary, except that on the birthday date it equals the age just attained (same as ALB). It is not ceil of a day-count fractional age (which can disagree on leap years).

Available in the top-level namespace as the compact alias lactuca.anextb.

Examples

On exact birthday, ANEXT equals ALB (not +1):

>>> from lactuca import age_next_birthday
>>> from datetime import date
>>> age_next_birthday(date(1990, 6, 15), date(2024, 6, 15))
34.0

One day after birthday (age at the next upcoming birthday):

>>> age_next_birthday(date(1990, 6, 15), date(2024, 6, 16))
35.0

Before birthday (rounds up to age at next birthday):

>>> age_next_birthday(date(1990, 6, 15), date(2024, 3, 1))
34.0

Vectorized computation:

>>> births = [date(1990, 1, 1), date(1985, 6, 15), date(2000, 12, 31)]
>>> age_next_birthday(births, date(2024, 1, 1))
array([34., 39., 24.])

See also

act_age

General actuarial age function with configurable rounding.

age_last_birthday

ALB convention (last completed birthday).

age_nearest_birthday

ANB convention (nearest calendar birthday).

age_exact

Exact fractional age without rounding.

lactuca.age_exact(birth_date: DateLike | Sequence[DateLike], valuation_date: DateLike | Sequence[DateLike], *, day_count: DayCountMethodLiteral = 'act_act') float | NDArray[np.float64]#

Compute exact fractional age in years without rounding.

Convenience wrapper for act_age(..., m=365, method='exact') that returns the precise age with daily resolution and no rounding applied. Use the day_count parameter to choose between full Actual/Actual ISDA precision ('act_act', default) or the faster days/365.25 approximation ('exact'). The result includes decimal precision reflecting partial years down to the day level.

Parameters:
  • birth_date (DateLike or sequence of DateLike) – Birth date(s). Accepts integers (YYYYMMDD), strings, date/datetime objects, and pandas/Polars date types.

  • valuation_date (DateLike or sequence of DateLike) – Valuation or reference date(s). Must not precede corresponding birth date.

  • day_count ({'act_act', 'exact'}, default='act_act') –

    Day-count convention for fractional year computation:

    • 'act_act': Actual/Actual ISDA convention with year-by-year leap year handling (maximum actuarial precision, required for regulatory compliance in many jurisdictions).

    • 'exact': Fast approximation using total_days / 365.25 (faster computation, maximum error ~0.003 years, acceptable for non-critical calculations or performance-sensitive applications).

Returns:

Exact fractional age(s) in years with full decimal precision:

  • Python float for scalar inputs

  • NumPy ndarray of dtype float64 for sequence inputs

Return type:

float or numpy.ndarray

Raises:
  • ValueError – If valuation_date precedes birth_date for any input pair, or if input sequences have incompatible lengths for broadcasting.

  • TypeError – If date inputs cannot be normalized to datetime.date objects.

Notes

The Actual/Actual ISDA convention (day_count='act_act') accumulates years using the formula:

\[\text{age}_{\text{exact}} = \sum_{y=y_1}^{y_2} \frac{\text{days in year } y}{\text{days in calendar year } y}\]

where the sum spans all calendar years between birth and valuation dates, properly accounting for leap years (366 days) and non-leap years (365 days).

The fast approximation (day_count='exact') uses:

\[\text{age}_{\text{exact}} \approx \frac{t - x}{365.25}\]

where \(t - x\) is the total number of days between dates. This introduces small rounding errors but executes significantly faster for performance- critical applications.

Examples

Exact age with Actual/Actual ISDA convention:

>>> from lactuca import age_exact
>>> from datetime import date
>>> age_exact(date(1990, 1, 1), date(2024, 1, 1))
34.0

Exact fractional age mid-year:

>>> age_exact(date(1990, 1, 1), date(2024, 7, 1))
34.49726775956284

Vectorized computation:

>>> births = [date(1990, 1, 1), date(1985, 1, 1)]
>>> age_exact(births, date(2024, 1, 1))
array([34., 39.])

See also

act_age

General actuarial age function with configurable rounding.

age_last_birthday

ALB convention (floor to last completed year).

age_nearest_birthday

ANB convention (round to nearest year).

age_next_birthday

ANEXT convention (ceiling to next year).

years_between

General fractional year calculation (no age-specific rounding).

lactuca.act_age(birth_date: DateLike | Sequence[DateLike], valuation_date: DateLike | Sequence[DateLike], *, m: PaymentFrequencyLiteral = 365, method: AgeMethodLiteral = 'exact', day_count: DayCountMethodLiteral = 'act_act') float | NDArray[np.float64]#

Compute actuarial age with selectable rounding and day-count conventions.

Calculates age in years between birth date and valuation date using precise day-count methods and optional frequency-based rounding. The function implements standard actuarial age conventions recognized in international practice: Age Last Birthday (ALB), Age Nearest Birthday (ANB), Age Next Birthday (ANEXT), and exact fractional age with daily precision.

The computation follows a two-stage process:

  1. Base fractional age: Computed using the specified day_count convention ('act_act' for Actual/Actual ISDA precision or 'exact' for days/365.25 approximation).

  2. Optional rounding: Applied based on method parameter, with granularity controlled by frequency parameter m.

Parameters:
  • birth_date (DateLike or sequence of DateLike) – Birth date(s). Supports integers (YYYYMMDD), strings (ISO, DD/MM/YYYY), date/datetime objects, and pandas/Polars date types.

  • valuation_date (DateLike or sequence of DateLike) – Valuation or reference date(s). Must not be before corresponding birth date.

  • m ({1, 2, 3, 4, 6, 12, 14, 24, 26, 52, 365}, default=365) –

    Payment frequency per year controlling rounding granularity:

    • 1: Annual (rounds to whole years)

    • 2: Semi-annual (rounds to 1/2 year increments)

    • 3: Triannual (rounds to 1/3 year increments)

    • 4: Quarterly (rounds to 1/4 year increments)

    • 6: Bimonthly (rounds to 1/6 year increments)

    • 12: Monthly (rounds to 1/12 year increments)

    • 14: 14-payment (rounds to 1/14 year increments)

    • 24: Semi-monthly (rounds to 1/24 year increments)

    • 26: Biweekly (rounds to 1/26 year increments)

    • 52: Weekly (rounds to 1/52 year increments)

    • 365: Daily precision (exact; no rounding when method='exact')

  • method ({'nearest', 'last', 'next', 'exact'}, default='exact') –

    Rounding method applied after computing fractional age:

    • 'last' (ALB): Floor to last completed period (standard in life insurance)

    • 'nearest' (ANB): Round to nearest period

    • 'next' (ANEXT): Ceiling to next period (common in health insurance)

    • 'exact': No rounding (requires m=365 for consistency)

  • day_count ({'act_act', 'exact'}, default='act_act') –

    Day-count convention for base fractional age:

    • 'act_act': Actual/Actual ISDA (year-by-year leap year handling)

    • 'exact': Approximation using total_days / 365.25 (faster)

Returns:

Actuarial age(s) in years:

  • Python float for scalar inputs

  • NumPy ndarray of dtype float64 for sequence inputs

Return type:

float or numpy.ndarray

Raises:
  • ValueError – If valuation_date is before birth_date for any input pair, if method='exact' is requested with m != 365 (exact method requires daily precision), if m is not a valid payment frequency, or if input sequences have incompatible lengths for broadcasting.

  • TypeError – If date inputs cannot be normalized to datetime.date objects.

Notes

For m=1 and method in {'last', 'nearest', 'next'}, ages use calendar birthday boundaries (ALB / ANB / ANEXT), not floor / rint / ceil of the fractional day-count age. This avoids leap-year shortfalls where Actual/Actual is slightly under an integer on an exact birthday. method='exact' still uses the selected day_count.

For m>1, rounding is applied to the fractional day-count age:

  • 'last': \(\lfloor \text{age} \cdot m \rfloor / m\)

  • 'nearest': \(\operatorname{round}(\text{age} \cdot m) / m\) (IEEE 754 round-half-to-even)

  • 'next': \(\lceil \text{age} \cdot m \rceil / m\)

For annual ANB (m=1, method='nearest'), an exact midpoint between consecutive birthdays rounds toward the next birthday (half-up).

The Actual/Actual ISDA convention (day_count='act_act') is the recognized standard for actuarial fractional ages. The 'exact' approximation may be used for performance-sensitive applications where precision differences of ~0.003 years are acceptable.

Broadcasting rules follow NumPy semantics: scalars and length-1 sequences are replicated to match the longest input sequence.

Examples

Age Last Birthday (ALB) - standard life insurance convention:

>>> from lactuca import act_age
>>> from datetime import date
>>> act_age(date(1990, 6, 15), date(2024, 3, 1), m=1, method='last')
33.0

Age Nearest Birthday (ANB) - rounds to nearest year:

>>> act_age(date(1990, 6, 15), date(2024, 3, 1), m=1, method='nearest')
34.0

Monthly precision (m=12) with nearest rounding:

>>> act_age(date(1990, 6, 15), date(2024, 3, 1), m=12, method='nearest')
33.75

Exact fractional age without rounding:

>>> age_exact_val = act_age(date(1990, 1, 1), date(2024, 1, 1), method='exact')
>>> age_exact_val
34.0

Vectorized computation with broadcasting:

>>> births = [date(1990, 1, 1), date(1985, 6, 15)]
>>> val = date(2024, 1, 1)
>>> act_age(births, val, m=1, method='last')
array([34., 38.])

See also

age_last_birthday

Convenience wrapper for ALB (m=1, method=’last’).

age_nearest_birthday

Convenience wrapper for ANB (m=1, method=’nearest’).

age_next_birthday

Convenience wrapper for ANEXT (m=1, method=’next’).

age_exact

Convenience wrapper for exact age (m=365, method=’exact’).

years_between

General fractional year calculation without age-specific rounding.

Compact aliases#

Short-form aliases are exported directly in the top-level lactuca namespace:

Alias

Equivalent

Convention

lactuca.alb

age_last_birthday

ALB — completed calendar birthdays

lactuca.anb

age_nearest_birthday

ANB — nearest calendar birthday (midpoint → next)

lactuca.anextb

age_next_birthday

ANEXT — next calendar birthday; on birthday = ALB

The name anextb (not anext) is used to avoid shadowing builtins.anext (Python 3.10+, used for async iteration).

Duration calculations#

lactuca.days_between(date1: DateLike | Sequence[DateLike], date2: DateLike | Sequence[DateLike]) int | NDArray[np.int32]#

Return the integer number of days (date2 - date1).

Thin wrapper around time_diff() equivalent to time_diff(..., unit='days'). Accepts scalar or sequence inputs and follows module broadcasting rules.

Parameters:
  • date1 (DateLike or sequence of DateLike) – Start and end date(s).

  • date2 (DateLike or sequence of DateLike) – Start and end date(s).

Returns:

Integer day counts as a Python int for scalar inputs or an np.int32 array for vectorized inputs.

Return type:

int or numpy.ndarray

Examples

>>> from lactuca import days_between
>>> from datetime import date
>>> days_between(date(2024, 1, 1), date(2024, 12, 31))
365
>>> days_between([date(2024, 1, 1), date(2023, 1, 1)], [date(2024, 12, 31), date(2023, 12, 31)])
array([365, 364], dtype=int32)

See also

time_diff

General time difference with configurable unit.

months_between

Complete calendar months between dates.

years_between

Fractional years between dates.

lactuca.months_between(date1: DateLike | Sequence[DateLike], date2: DateLike | Sequence[DateLike]) int | NDArray[np.int32]#

Return the number of complete calendar months between dates.

Thin wrapper around time_diff() with unit='months'. A complete month is counted when the destination day-of-month is greater than or equal to the start day-of-month.

Parameters:
  • date1 (DateLike or sequence of DateLike) – Start and end date(s).

  • date2 (DateLike or sequence of DateLike) – Start and end date(s).

Returns:

Complete calendar months as int or np.int32 array.

Return type:

int or numpy.ndarray

Examples

>>> from lactuca import months_between
>>> from datetime import date
>>> months_between(date(2024, 1, 15), date(2024, 3, 15))
2
>>> months_between(date(2024, 1, 31), date(2024, 3, 1))  # day criterion
1

See also

time_diff

General time difference with configurable unit.

days_between

Integer day count.

years_between

Fractional years between dates.

lactuca.years_between(date1: DateLike | Sequence[DateLike], date2: DateLike | Sequence[DateLike], *, method: DayCountMethodLiteral = 'act_act') float | NDArray[np.float64]#

Return fractional years between dates using actuarial day-counts.

Thin wrapper around time_diff() with unit='years'. Two day-count conventions are supported:

  • 'act_act': Actual/Actual ISDA, performs year-by-year accumulation and accounts for leap years with maximum precision.

  • 'exact': Approximation using days / 365.25 (faster, slightly different results for spans that include leap days).

Parameters:
  • date1 (DateLike or sequence of DateLike) – Start and end date(s).

  • date2 (DateLike or sequence of DateLike) – Start and end date(s).

  • method ({'act_act', 'exact'}, default='act_act') – Day-count convention for fractional years.

Returns:

Fractional years as a Python float for scalar inputs or an np.float64 array for vectorized inputs.

Return type:

float or numpy.ndarray

Raises:

ValueError – If method is not one of 'act_act' or 'exact', or if input sequences have incompatible lengths for broadcasting.

Examples

>>> from lactuca import years_between
>>> from datetime import date
>>> years_between(date(2024, 1, 1), date(2025, 1, 1))
1.0
>>> years_between(date(2024, 1, 1), date(2024, 7, 1), method='act_act')
0.4972677595628415

See also

time_diff

General time difference with configurable unit.

days_between

Integer day count.

months_between

Complete calendar months between dates.

act_age

Actuarial age with rounding conventions.

lactuca.time_diff(date1: DateLike | Sequence[DateLike], date2: DateLike | Sequence[DateLike], *, unit: TimeUnitLiteral = 'years', method: DayCountMethodLiteral | None = None) int | float | NDArray[np.int32] | NDArray[np.float64]#

Compute the time difference between two date(s) in the requested unit.

This single entry point implements days, complete calendar months, and fractional years (using actuarial day-count conventions). The function accepts scalar or sequence inputs and applies the module’s broadcasting rules: scalars and length-1 sequences are replicated to match the longest sequence; multiple sequences with incompatible lengths raise ValueError.

Parameters:
  • date1 (DateLike or sequence of DateLike) – Start and end date(s). Each may be a scalar or a (possibly nested) sequence; returned values follow input broadcasting.

  • date2 (DateLike or sequence of DateLike) – Start and end date(s). Each may be a scalar or a (possibly nested) sequence; returned values follow input broadcasting.

  • unit ({'days', 'months', 'years'}, default='years') – Unit for the result. 'days' returns integer day counts, 'months' returns complete calendar months, 'years' returns fractional years according to method.

  • method ({'act_act', 'exact'} or None, optional) – Day-count convention used only when unit='years'. If None defaults to 'act_act'. 'act_act' performs a year-by-year Actual/Actual ISDA accumulation; 'exact' uses the approximation days / 365.25.

Returns:

  • For unit='days': Python int (scalar) or np.int32 array (vectorized).

  • For unit='months': Python int or np.int32 array.

  • For unit='years': Python float or np.float64 array.

Return type:

int, float or numpy.ndarray

Raises:

ValueError – If unit is invalid or if method is supplied for a non-years unit, or when input sequences have incompatible lengths for broadcasting.

Examples

>>> from lactuca import time_diff
>>> from datetime import date
>>> time_diff(date(2024,1,1), date(2024,12,31), unit='days')
365
>>> time_diff(date(2024,1,15), date(2024,3,15), unit='months')
2
>>> time_diff(date(2024,1,1), date(2024,7,1), unit='years', method='act_act')
0.4972677595628415

See also

days_between

Integer day count between two dates.

months_between

Complete calendar months between two dates.

years_between

Fractional years between two dates.

Anniversary dates#

lactuca.anniversary_dates(start_date: DateLike, end_date: DateLike, *, m: PaymentFrequencyLiteral = 1, selected_periods: Sequence[int] | None = None) FormatDates#

Generate anniversary dates between start_date (inclusive) and end_date (exclusive).

The function supports annual and sub-annual frequencies and returns actual date objects suitable for scheduling or cohort projections.

Parameters:
  • start_date (DateLike) – Start (inclusive) and end (exclusive) dates. If end_date is not after start_date a ValueError is raised.

  • end_date (DateLike) – Start (inclusive) and end (exclusive) dates. If end_date is not after start_date a ValueError is raised.

  • m ({1, 2, 3, 4, 6, 12, 24, 26, 52, 365}, default=1) – Payment frequency per year (allowed values: 1, 2, 3, 4, 6, 12, 24, 26, 52, 365). m=24 uses a 15-day delta (360-day convention: 24 × 15 = 360 days). m=26 uses a 14-day delta (364-day convention: 26 × 14 = 364 days). m=14 is not supported and raises ValueError; use m=12 with separate extraordinary cash flows for the Spanish “14 pagas” scheme.

  • selected_periods (Union[sequence of int, None], optional) – Specific 1-indexed periods to include within each year/frequency.

Returns:

Flat list of anniversary dates for a single pair (or after broadcast when N=1). For N>1 pairs, a list-of-lists (one grid per pair). Annual m=1 re-anchors the original day-of-month each year.

Return type:

FormatDates

Raises:

ValueError – If m is not a supported payment frequency, if m=14 is passed (not supported for date grids; use m=12 with extra cash flows), if selected_periods contains out-of-range entries, or if end_date <= start_date.

Notes

Annual grids (m=1) re-anchor the original day-of-month each year (a Feb-29 start keeps Feb-29 on leap years and uses Feb-28 otherwise). Scalar or N=1 inputs return a flat date list; N>1 returns a list-of-lists.

Examples

>>> from lactuca import anniversary_dates
>>> from datetime import date
>>> anniversary_dates(date(2024,1,1), date(2026,1,1), m=1)
FormatDates([datetime.date(2024, 1, 1), datetime.date(2025, 1, 1)])

See also

next_anniversary

Next single anniversary date after a reference date.

time_diff

General time difference with configurable unit and day-count.

lactuca.next_anniversary(birth_date: DateLike | Sequence[DateLike], ref_date: DateLike | Sequence[DateLike]) FormatDates#

Return the next anniversary date(s) after a reference date.

If the reference date equals the anniversary, the function returns the anniversary in the following year.

Parameters:
  • birth_date (DateLike or sequence of DateLike) – Birth date(s).

  • ref_date (DateLike or sequence of DateLike) – Reference date(s).

Returns:

Wrapper containing the next anniversary date(s). Broadcasting rules apply when mixing scalars and sequences.

Return type:

FormatDates

Raises:
  • TypeError – If any input value cannot be converted to datetime.date.

  • ValueError – If date strings or integers are rejected by the active Config().date_format.

Notes

For birth dates of February 29, the anniversary falls back to February 28 in non-leap years.

Examples

>>> from lactuca import next_anniversary
>>> from datetime import date
>>> next_anniversary(date(1990, 6, 15), date(2024, 3, 1))
FormatDates([datetime.date(2024, 6, 15)])
>>> next_anniversary(date(1990, 6, 15), date(2024, 6, 15))
FormatDates([datetime.date(2025, 6, 15)])

See also

anniversary_dates

Generate a full grid of anniversary dates between two dates.

Date construction and arithmetic#

Date input types#

lactuca.dates.DateLike: int | str | datetime.date | datetime.datetime | pandas.Timestamp | numpy.datetime64 | polars.Date | polars.Datetime#

Union type alias for all date-like inputs accepted by the public API.

Supported types:

  • int — 8-digit integer YYYYMMDD (e.g. 20240115)

  • str — ISO YYYY-MM-DD, European slash DD/MM/YYYY, European dash DD-MM-YYYY, or YMD slash YYYY/MM/DD

  • datetime.date or datetime.datetime

  • pandas.Timestamp

  • numpy.datetime64

  • Polars Date / Datetime scalars

The active date_format setting controls which string representations are accepted (e.g. ISO-only, European formats, etc.).

lactuca.make_date(year: int | Sequence[int], month: int | Sequence[int], day: int | Sequence[int]) FormatDates#

Construct date objects from year, month, and day components.

Public constructor function that builds one or more datetime.date objects from integer year, month, and day components. Validates calendar rules (including leap years) and supports broadcasting of scalar components to match sequence inputs. Returned values are wrapped in FormatDates which behaves like a list and supports the .format() method for output formatting.

Parameters:
  • year (int or sequence of int) – Year component(s). Accepts Python int, NumPy integer scalars, or sequences (list/tuple/ndarray).

  • month (int or sequence of int) – Month component(s) in range 1..12 (1=January, 12=December).

  • day (int or sequence of int) – Day component(s) validated against the corresponding month/year. Must be in the valid range for the given month (1..28/29/30/31).

Returns:

A FormatDates wrapper containing:

  • If all inputs are scalars: single datetime.date.

  • If any input is a sequence: list of datetime.date objects with length equal to the broadcasted length of all inputs.

Return type:

FormatDates

Raises:

ValueError – If provided components are invalid (e.g., month out of range 1..12, day exceeds number of days in month, invalid leap year date like Feb 29 in non-leap year), or if sequence lengths are incompatible for broadcasting (all sequences must have the same length, or one must be scalar).

Notes

Broadcasting follows NumPy-like semantics: scalar inputs are replicated to match the longest sequence input. For example, make_date(2024, [1, 2, 3], 15) broadcasts year and day to create three dates: 2024-01-15, 2024-02-15, 2024-03-15.

The returned FormatDates wrapper compares equal to a single datetime.date when it contains exactly one element, simplifying scalar-result comparisons in tests and assertions.

Examples

Construct single date from scalar components:

>>> from lactuca import make_date
>>> make_date(2024, 1, 15)
FormatDates([datetime.date(2024, 1, 15)])

Construct multiple dates from sequence components:

>>> make_date([2024, 2023], [1, 12], [15, 31])
FormatDates([datetime.date(2024, 1, 15), datetime.date(2023, 12, 31)])

Broadcasting scalar month and day to sequence of years:

>>> make_date([2023, 2024, 2025], 6, 15)
FormatDates([datetime.date(2023, 6, 15), datetime.date(2024, 6, 15), datetime.date(2025, 6, 15)])

See also

FormatDates

Wrapper class for date lists with formatting support.

format_date

Format existing dates according to configuration.

lactuca.add_duration(start_date: DateLike | Sequence[DateLike], *, years: int | Sequence[int] = 0, months: int | Sequence[int] = 0, days: int | Sequence[int] = 0) FormatDates#

Add years, months and days to date(s) with month-end adjustment.

When adding months/years the function adjusts day-of-month to the last valid day of the resulting month if the original day does not exist in the target month (e.g. Jan 31 + 1 month -> Feb 28/29).

Parameters:
  • start_date (DateLike or sequence of DateLike) – Start date or sequence of start dates.

  • years (int or sequence of int, optional) – Offsets to add. Scalars are broadcast to match sequence inputs.

  • months (int or sequence of int, optional) – Offsets to add. Scalars are broadcast to match sequence inputs.

  • days (int or sequence of int, optional) – Offsets to add. Scalars are broadcast to match sequence inputs.

Returns:

Wrapper containing one or more resulting datetime.date objects. Scalar inputs return a single-element wrapper.

Return type:

FormatDates

Raises:

ValueError – If component sequence lengths are incompatible for broadcasting.

Notes

Month-end clamping: when the resulting month has fewer days than the original day-of-month, the day is clamped to the last valid day of that month (e.g. Jan 31 + 1 month: Feb 28/29; Mar 31 + 1 month: Apr 30).

Prefer years=N (or months/days) from the original anchor in one call. Chaining repeated +1 year from a clamped February date can permanently lose day 29; use anniversary_dates() for annual policy grids instead.

Broadcasting follows NumPy-like semantics: scalar start_date and scalar duration components replicate to match the longest sequence input.

Examples

Month-end clamping (February, leap year):

>>> from lactuca import add_duration
>>> from datetime import date
>>> add_duration(date(2024, 1, 31), months=1)
FormatDates([datetime.date(2024, 2, 29)])

Add years, months and days combined:

>>> add_duration(date(2024, 6, 15), years=1, months=6, days=3)
FormatDates([datetime.date(2025, 12, 18)])

Vectorized with month-end clamping:

>>> add_duration([date(2024, 1, 31), date(2024, 3, 31)], months=1)
FormatDates([datetime.date(2024, 2, 29), datetime.date(2024, 4, 30)])

See also

end_of_month

Last calendar date of a given month.

make_date

Construct dates from year, month and day components.

lactuca.end_of_month(input_date: DateLike | Sequence[DateLike]) FormatDates#

Return month-end date(s) for the provided date(s).

Parameters:

input_date (DateLike or sequence of DateLike) – Single date-like value or sequence/array of date-like values.

Returns:

Wrapper containing one or more datetime.date objects set to the last valid day of the corresponding month.

Return type:

FormatDates

Raises:
  • TypeError – If an input value cannot be converted to a datetime.date.

  • ValueError – If an input string or integer cannot be parsed under the active Config().date_format.

Notes

The day component of the input is ignored; only the year and month are used to compute the last calendar day of the month. Accounts for leap years (February returns 29 in leap years, 28 otherwise).

Examples

>>> from lactuca import end_of_month
>>> from datetime import date
>>> end_of_month(date(2024, 2, 15))
FormatDates([datetime.date(2024, 2, 29)])

See also

days_in_month

Number of days in a specific month and year.

add_duration

Add duration offsets to date(s) with month-end clamping.

lactuca.format_date(input_date: DateLike | Sequence[DateLike] | list, date_format: DatesFormatsLiteral | None = None) object#

Format date(s) into the requested representation.

Parse any DateLike input (or sequence of them) to datetime.date objects using the global Config().date_format setting, then convert the result to the representation chosen by date_format.

Important

date_format controls the output representation only. Input parsing always follows Config().date_format. This means you can pass an ISO string and request integer output without changing the global configuration:

>>> format_date("2024-07-15", date_format="ymd_int")
20240715

Output format mappings#

  • 'ymd_int': integer YYYYMMDD

  • 'ymd': ISO string 'YYYY-MM-DD'

  • 'dmy': string 'DD/MM/YYYY'

  • 'mdy': string 'MM/DD/YYYY'

param input_date:

A single date-like value or a (possibly nested) sequence of date-like values. Supported types: int (YYYYMMDD), ISO/Spanish/US strings, datetime.date, datetime.datetime, pandas.Timestamp, numpy.datetime64, Polars Date/Datetime scalars or series. Input parsing always uses Config().date_format (see Notes).

type input_date:

DateLike or sequence of DateLike

param date_format:

Output format override. Controls only how the result is represented, not how the input is parsed. When None the global Config().date_format is used for both parsing and output.

type date_format:

{‘ymd_int’, ‘ymd’, ‘dmy’, ‘mdy’} or None, optional

returns:
  • Scalar input: scalar result (int for 'ymd_int', otherwise str).

  • Sequence input: nested list preserving input shape, with each leaf formatted according to date_format.

rtype:

int, str, or list

raises TypeError:

If an input value cannot be converted to a datetime.date (e.g., a float, a generator, or an unsupported object).

raises ValueError:

If a string or integer cannot be parsed under the active Config().date_format (e.g., an integer is supplied but Config().date_format != 'ymd_int').

Notes

Input parsing is governed exclusively by Config().date_format:

  • Under 'ymd' (default): ISO strings ('YYYY-MM-DD') and 'YYYY/MM/DD' forms are accepted. Integers and DMY/MDY-only strings are rejected.

  • Under 'dmy': 'DD/MM/YYYY' and 'DD-MM-YYYY' strings are accepted.

  • Under 'mdy': 'MM/DD/YYYY' strings are accepted.

  • Under 'ymd_int': 8-digit integers and numeric strings ('YYYYMMDD') are accepted; separated forms are rejected.

datetime.date / datetime.datetime objects, pandas.Timestamp, numpy.datetime64, and Polars date scalars/series are accepted under all format configurations.

Examples

Convert an ISO string to integer YYYYMMDD output (format conversion):

>>> from lactuca import format_date
>>> format_date("2024-07-15", date_format="ymd_int")
20240715

Convert an ISO string to DMY string output:

>>> format_date("2024-07-15", date_format="dmy")
'15/07/2024'

Format a datetime.date object:

>>> from datetime import date
>>> format_date(date(2024, 7, 15), date_format="ymd")
'2024-07-15'

Vectorized input with mixed types (default Config 'ymd'):

>>> import numpy as np
>>> format_date(["2024-01-15", date(2024, 7, 15), np.datetime64("2024-12-31")],
...             date_format="ymd_int")
[20240115, 20240715, 20241231]

Parse a European DMY date string and convert to integer output (requires Config().date_format = 'dmy'):

>>> from lactuca import Config
>>> Config().date_format = 'dmy'
>>> format_date("15/07/2024", date_format="ymd_int")
20240715
>>> Config().reset()

See also

FormatDates

Wrapper class returned by date constructors with a .format() method for on-demand output formatting.

lactuca.config.Config

Global configuration; date_format controls which input string representations are accepted.

class lactuca.FormatDates(input_date: DateLike | Sequence[DateLike] | None = None)#

Lightweight wrapper around list of datetime.date objects with formatting.

FormatDates behaves as a normal Python list but provides a convenience format(date_format=None) method that returns items formatted according to the project’s global Config().date_format setting or an optional override.

This wrapper is returned by date construction functions (make_date, add_duration, end_of_month, anniversary_dates, etc.) to provide consistent formatting capabilities across the module’s public API.

Parameters:

input_date (DateLike or sequence of DateLike, optional) – A single date-like value or an iterable of date-like values. When omitted constructs an empty wrapper. Input values are normalized according to Config().date_format.

Notes

The wrapper preserves list semantics (indexing, iteration, slicing) while adding actuarial-specific formatting behavior. Equality comparison with a scalar datetime.date is supported: a FormatDates containing a single date compares equal to that scalar date, simplifying test assertions.

Length-1 wrappers re-enter age and duration APIs as scalars (same as a bare date). Plain list / Series / ndarray of length 1 remain sequences. Nested list-of-lists from multi-pair anniversary_dates are preserved by format().

All date-like input values are normalized to datetime.date objects during construction using the global Config().date_format setting.

Examples

Construct from single date:

>>> from lactuca import FormatDates
>>> from datetime import date
>>> fd = FormatDates(date(2024, 1, 15))
>>> fd
FormatDates([datetime.date(2024, 1, 15)])

Format output according to configuration:

>>> fd.format()  # When Config().date_format == 'ymd'
'2024-01-15'

Construct from sequence:

>>> fd_seq = FormatDates([date(2024, 1, 15), date(2024, 6, 30)])
>>> fd_seq.format()
['2024-01-15', '2024-06-30']

Equality comparison with scalar date (single-element wrapper):

>>> FormatDates(date(2024, 1, 15)) == date(2024, 1, 15)
True

See also

make_date

Construct dates from components returning FormatDates.

format_date

Format dates without wrapper construction.

lactuca.config.Config

Global configuration controlling date formats.

format(date_format: Literal['ymd', 'dmy', 'mdy', 'ymd_int'] | None = None)#

Format contained date(s) into the requested string or integer representation.

Parameters:

date_format ({'ymd_int', 'ymd', 'dmy', 'mdy'} or None, optional) – Output format override. When None the global Config().date_format is used.

Returns:

  • Single-element wrapper with a date leaf: scalar result (int for 'ymd_int', str otherwise).

  • Multi-element or nested list-of-lists: nested list preserving structure (list[list[str|int]] for multi-policy grids).

Return type:

int, str, or list

Examples

>>> from lactuca import FormatDates
>>> from datetime import date
>>> fd = FormatDates([date(2024, 1, 15), date(2024, 6, 30)])
>>> fd.format(date_format='ymd')
['2024-01-15', '2024-06-30']

See also

format_date

Module-level formatting function for arbitrary date inputs.

lactuca.config.Config

Global configuration controlling date_format.

Calendar utilities#

lactuca.is_leap_year(year: int | Sequence[int]) bool | NDArray[bool]#

Return whether year(s) are leap years (Gregorian calendar).

Parameters:

year (int or sequence of int) – Single year or sequence/array of years. Accepts Python int, list, tuple, numpy.ndarray, and Pandas/Polars Series containing integer year values.

Returns:

  • Python bool for scalar input.

  • NumPy ndarray of dtype bool for sequence input.

Return type:

bool or numpy.ndarray

Notes

Uses the Gregorian rule: a year is a leap year if it is divisible by 4, except century years which must also be divisible by 400.

Pandas and Polars Series are accepted without .to_numpy(); each element is processed individually and the result is returned as NDArray[np.bool_].

Examples

>>> from lactuca import is_leap_year
>>> is_leap_year(2024)
True
>>> is_leap_year(2023)
False
>>> is_leap_year([2020, 2021, 2024])
array([ True, False,  True])

See also

days_in_year

Number of days in a year (365 or 366).

lactuca.days_in_year(year: int | Sequence[int]) int | NDArray[int32]#

Return number of days in year(s) (365 or 366).

Parameters:

year (int or sequence of int) – Year or sequence/array of years. Accepts Python int, list, tuple, numpy.ndarray, and Pandas/Polars Series containing integer year values.

Returns:

  • Python int for scalar input.

  • NumPy ndarray of dtype int32 for sequence input.

Return type:

int or numpy.ndarray

Notes

Returns 366 for leap years and 365 for non-leap years under the Gregorian calendar rules. Used internally as the denominator in Actual/Actual ISDA fractional year calculations.

Pandas and Polars Series are accepted without .to_numpy(); each element is processed individually and the result is returned as NDArray[np.int32].

Examples

>>> from lactuca import days_in_year
>>> days_in_year(2024)
366
>>> days_in_year(2023)
365
>>> days_in_year([2020, 2021, 2024])
array([366, 365, 366], dtype=int32)

See also

is_leap_year

Boolean leap year detection.

days_in_month

Days in a specific month.

lactuca.days_in_month(year: int | Sequence[int], month: int | Sequence[int]) int | NDArray[int32]#

Return number of days in given month(s) for year(s), accounting for leap years.

Parameters:
  • year (int or sequence of int) – Year(s). Accepts Python int, list, tuple, numpy.ndarray, and Pandas/Polars Series of integer years.

  • month (int or sequence of int) – Month(s) in 1..12. Accepts the same types as year.

Returns:

  • Python int for scalar inputs.

  • NumPy ndarray of dtype int32 for sequence inputs.

Return type:

int or numpy.ndarray

Raises:

ValueError – If a month is out of range 1..12 or sequence lengths are incompatible.

Notes

Accounts for leap years: February returns 29 in leap years and 28 in non-leap years. All other months follow fixed calendar day counts.

Pandas and Polars Series are accepted without .to_numpy() for both year and month; each element is processed individually.

Examples

>>> from lactuca import days_in_month
>>> days_in_month(2024, 2)
29
>>> days_in_month(2023, 2)
28
>>> days_in_month([2024, 2024, 2024], [1, 2, 12])
array([31, 29, 31], dtype=int32)

See also

is_leap_year

Leap year detection.

days_in_year

Total days in a given year.

end_of_month

Last calendar date of a given month.

Date component extraction#

lactuca.year(input_date: DateLike | Sequence[DateLike]) int | NDArray[np.int32]#

Extract the calendar year from date-like input.

Public extractor function that returns the 4-digit year component from scalar or vectorized date inputs. Follows module broadcasting semantics: scalar inputs return Python int, sequence inputs return NumPy int32 array.

Parameters:

input_date (Union[DateLike, Sequence[DateLike]]) – A single date-like value or a sequence/array of date-like values. Supported types include integers (YYYYMMDD), strings (ISO, DD/MM/YYYY), date/datetime objects, and pandas/Polars date types.

Returns:

  • If input_date is scalar, returns a Python int year.

  • If input_date is a sequence, returns a NumPy ndarray of dtype int32 with one year per input element.

Return type:

int or numpy.ndarray

Notes

Accepts pandas.Series and polars.Series.

Examples

Extract year from a date string:

>>> from lactuca import year
>>> year("2024-01-15")
2024

Extract years from a sequence of dates:

>>> year(["2024-01-15", "2023-12-31"])
array([2024, 2023], dtype=int32)

See also

month

Extract month component (1-12).

day

Extract day-of-month component (1-31).

quarter

Compute fiscal quarter (1-4).

lactuca.month(input_date: DateLike | Sequence[DateLike]) int | NDArray[np.int32]#

Extract the calendar month (1-12) from date-like input.

Public extractor function that returns the month component (1=January, 12=December) from scalar or vectorized date inputs. Follows module broadcasting semantics: scalar inputs return Python int, sequence inputs return NumPy int32 array.

Parameters:

input_date (Union[DateLike, Sequence[DateLike]]) – Single date-like value or sequence/array of date-like values.

Returns:

  • Python int for scalar input (1-12).

  • NumPy ndarray of dtype int32 for sequence input.

Return type:

int or numpy.ndarray

Notes

Accepts pandas.Series and polars.Series.

Examples

Extract month from a date string:

>>> from lactuca import month
>>> month("2024-01-15")
1

Extract months from a sequence of dates:

>>> month(["2024-01-15", "2024-12-31"])
array([ 1, 12], dtype=int32)

See also

year

Extract year component.

day

Extract day-of-month component.

quarter

Compute fiscal quarter from month.

lactuca.day(input_date: DateLike | Sequence[DateLike]) int | NDArray[np.int32]#

Extract the day-of-month (1-31) from date-like input.

Public extractor function that returns the day component from scalar or vectorized date inputs. Follows module broadcasting semantics: scalar inputs return Python int, sequence inputs return NumPy int32 array.

Parameters:

input_date (Union[DateLike, Sequence[DateLike]]) – Single date-like value or sequence/array of date-like values.

Returns:

  • Python int for scalar input (1-31).

  • NumPy ndarray of dtype int32 for sequence input.

Return type:

int or numpy.ndarray

Notes

Accepts pandas.Series and polars.Series.

Examples

Extract day from a date string:

>>> from lactuca import day
>>> day("2024-01-15")
15

Extract days from a sequence of dates:

>>> day(["2024-01-15", "2024-12-31"])
array([15, 31], dtype=int32)

See also

year

Extract year component.

month

Extract month component.

end_of_month

Get last day of month for date(s).

lactuca.quarter(input_date: DateLike | Sequence[DateLike], *, prefix: str | None = None, suffix: str | None = None) int | str | list[int] | list[str]#

Compute fiscal quarter (1-4) or formatted label from date-like input.

Public extractor function that determines the fiscal quarter from the month component of date inputs. By default returns numeric quarter values (1-4). When prefix and/or suffix are provided, returns formatted string labels following standard actuarial and financial reporting conventions.

Parameters:
  • input_date (DateLike or sequence of DateLike) – Single date-like value or sequence/array of date-like values.

  • prefix (str or None, default=None) –

    Optional string prepended to quarter number. Common conventions:

    • 'Q' produces Q1, Q2, Q3, Q4 (Anglo-Saxon convention)

    • 'T' produces T1, T2, T3, T4 (Spanish/Latin convention)

    • '' (empty string) produces bare numeric with suffix only

  • suffix (str or None, default=None) –

    Optional string appended to quarter number. Examples:

    • 'T' produces 1T, 2T, 3T, 4T (Spanish convention)

    • ' trimestre' produces 1 trimestre, etc.

    • Year suffix like ' 2024' for period labeling

Returns:

  • If both prefix and suffix are None (default): returns Python int (1-4) for scalar input or NDArray[int32] for sequence input.

  • If either prefix or suffix is provided: returns formatted string(s) combining prefix + quarter_number + suffix.

Return type:

int, str, numpy.ndarray or list[str]

Notes

Accepts pandas.Series and polars.Series; each element is processed individually.

Quarter calculation uses the formula: quarter = (month - 1) // 3 + 1, mapping months as follows:

  • Q1: January, February, March

  • Q2: April, May, June

  • Q3: July, August, September

  • Q4: October, November, December

Examples

Numeric quarter (default):

>>> from lactuca import quarter
>>> quarter("2024-01-15")
1

Sequence of numeric quarters:

>>> quarter(["2024-01-15", "2024-07-15", "2024-12-31"])
array([1, 3, 4], dtype=int32)

Formatted with prefix (Anglo-Saxon Q notation):

>>> quarter("2024-07-15", prefix="Q")
'Q3'

Formatted with suffix (Spanish T notation):

>>> from datetime import date
>>> quarter([date(2024, 1, 15), date(2024, 7, 15)], suffix="T")
['1T', '3T']

Both prefix and suffix:

>>> quarter(date(2024, 4, 1), prefix="Q", suffix=" 2024")
'Q2 2024'

See also

month

Extract month component (1-12).

year

Extract year component.