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.FormatDateslength-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=14raisesValueError— usem=12with 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/datetimeobjects, 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, soday_countdoes not change the result.
- Returns:
Age(s) at last birthday (whole years, always non-negative integers represented as float):
Python
floatfor scalar inputsNumPy
ndarrayof dtypefloat64for 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
floorof 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_ageGeneral actuarial age function with configurable rounding.
age_nearest_birthdayANB convention (nearest calendar birthday).
age_next_birthdayANEXT convention (next calendar birthday).
age_exactExact 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/datetimeobjects, 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, soday_countdoes not change the result.
- Returns:
Age(s) at the nearest calendar birthday (as float):
Python
floatfor scalar inputsNumPy
ndarrayof dtypefloat64for 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.rintof 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_ageGeneral actuarial age function with configurable rounding.
age_last_birthdayALB convention (last completed birthday).
age_next_birthdayANEXT convention (next calendar birthday).
age_exactExact 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/datetimeobjects, 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, soday_countdoes not change the result.
- Returns:
Age(s) at next birthday (as float), equal to ALB on the birthday itself:
Python
floatfor scalar inputsNumPy
ndarrayof dtypefloat64for 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
ceilof 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_ageGeneral actuarial age function with configurable rounding.
age_last_birthdayALB convention (last completed birthday).
age_nearest_birthdayANB convention (nearest calendar birthday).
age_exactExact 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 theday_countparameter 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/datetimeobjects, 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
floatfor scalar inputsNumPy
ndarrayof dtypefloat64for sequence inputs
- Return type:
float or numpy.ndarray
- Raises:
ValueError – If
valuation_dateprecedesbirth_datefor any input pair, or if input sequences have incompatible lengths for broadcasting.TypeError – If date inputs cannot be normalized to
datetime.dateobjects.
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_ageGeneral actuarial age function with configurable rounding.
age_last_birthdayALB convention (floor to last completed year).
age_nearest_birthdayANB convention (round to nearest year).
age_next_birthdayANEXT convention (ceiling to next year).
years_betweenGeneral 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:
Base fractional age: Computed using the specified
day_countconvention ('act_act'for Actual/Actual ISDA precision or'exact'for days/365.25 approximation).Optional rounding: Applied based on
methodparameter, with granularity controlled by frequency parameterm.
- Parameters:
birth_date (DateLike or sequence of DateLike) – Birth date(s). Supports integers (YYYYMMDD), strings (ISO, DD/MM/YYYY),
date/datetimeobjects, 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 whenmethod='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 (requiresm=365for 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
floatfor scalar inputsNumPy
ndarrayof dtypefloat64for sequence inputs
- Return type:
float or numpy.ndarray
- Raises:
ValueError – If
valuation_dateis beforebirth_datefor any input pair, ifmethod='exact'is requested withm != 365(exact method requires daily precision), ifmis not a valid payment frequency, or if input sequences have incompatible lengths for broadcasting.TypeError – If date inputs cannot be normalized to
datetime.dateobjects.
Notes
For
m=1andmethodin{'last', 'nearest', 'next'}, ages use calendar birthday boundaries (ALB / ANB / ANEXT), notfloor/rint/ceilof 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 selectedday_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_birthdayConvenience wrapper for ALB (m=1, method=’last’).
age_nearest_birthdayConvenience wrapper for ANB (m=1, method=’nearest’).
age_next_birthdayConvenience wrapper for ANEXT (m=1, method=’next’).
age_exactConvenience wrapper for exact age (m=365, method=’exact’).
years_betweenGeneral fractional year calculation without age-specific rounding.
Compact aliases#
Short-form aliases are exported directly in the top-level lactuca namespace:
Alias |
Equivalent |
Convention |
|---|---|---|
|
|
ALB — completed calendar birthdays |
|
|
ANB — nearest calendar birthday (midpoint → next) |
|
|
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 totime_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
intfor scalar inputs or annp.int32array 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_diffGeneral time difference with configurable unit.
months_betweenComplete calendar months between dates.
years_betweenFractional 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()withunit='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
intornp.int32array.- 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_diffGeneral time difference with configurable unit.
days_betweenInteger day count.
years_betweenFractional 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()withunit='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 usingdays / 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
floatfor scalar inputs or annp.float64array for vectorized inputs.- Return type:
float or numpy.ndarray
- Raises:
ValueError – If
methodis 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_diffGeneral time difference with configurable unit.
days_betweenInteger day count.
months_betweenComplete calendar months between dates.
act_ageActuarial 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 tomethod.method ({'act_act', 'exact'} or None, optional) – Day-count convention used only when
unit='years'. IfNonedefaults 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': Pythonint(scalar) ornp.int32array (vectorized).For
unit='months': Pythonintornp.int32array.For
unit='years': Pythonfloatornp.float64array.
- Return type:
int, float or numpy.ndarray
- Raises:
ValueError – If
unitis invalid or ifmethodis 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_betweenInteger day count between two dates.
months_betweenComplete calendar months between two dates.
years_betweenFractional 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) andend_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_dateis not afterstart_dateaValueErroris raised.end_date (DateLike) – Start (inclusive) and end (exclusive) dates. If
end_dateis not afterstart_dateaValueErroris 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=24uses a 15-day delta (360-day convention: 24 × 15 = 360 days).m=26uses a 14-day delta (364-day convention: 26 × 14 = 364 days).m=14is not supported and raisesValueError; usem=12with 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=1re-anchors the original day-of-month each year.- Return type:
- Raises:
ValueError – If
mis not a supported payment frequency, ifm=14is passed (not supported for date grids; usem=12with extra cash flows), ifselected_periodscontains out-of-range entries, or ifend_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_anniversaryNext single anniversary date after a reference date.
time_diffGeneral 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:
- 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_datesGenerate 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 integerYYYYMMDD(e.g.20240115)str— ISOYYYY-MM-DD, European slashDD/MM/YYYY, European dashDD-MM-YYYY, or YMD slashYYYY/MM/DDdatetime.dateordatetime.datetimepandas.Timestampnumpy.datetime64Polars
Date/Datetimescalars
The active
date_formatsetting 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
dateobjects from year, month, and day components.Public constructor function that builds one or more
datetime.dateobjects 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 inFormatDateswhich 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
FormatDateswrapper containing:If all inputs are scalars: single
datetime.date.If any input is a sequence: list of
datetime.dateobjects with length equal to the broadcasted length of all inputs.
- Return type:
- 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
FormatDateswrapper compares equal to a singledatetime.datewhen 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
FormatDatesWrapper class for date lists with formatting support.
format_dateFormat 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.dateobjects. Scalar inputs return a single-element wrapper.- Return type:
- 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 yearfrom a clamped February date can permanently lose day 29; useanniversary_dates()for annual policy grids instead.Broadcasting follows NumPy-like semantics: scalar
start_dateand 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_monthLast calendar date of a given month.
make_dateConstruct 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.dateobjects set to the last valid day of the corresponding month.- Return type:
- 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_monthNumber of days in a specific month and year.
add_durationAdd 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
DateLikeinput (or sequence of them) todatetime.dateobjects using the globalConfig().date_formatsetting, then convert the result to the representation chosen bydate_format.Important
date_formatcontrols the output representation only. Input parsing always followsConfig().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': integerYYYYMMDD'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, PolarsDate/Datetimescalars or series. Input parsing always usesConfig().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
Nonethe globalConfig().date_formatis used for both parsing and output.- type date_format:
{‘ymd_int’, ‘ymd’, ‘dmy’, ‘mdy’} or None, optional
- returns:
Scalar input: scalar result (
intfor'ymd_int', otherwisestr).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 butConfig().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.datetimeobjects,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.dateobject:>>> 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
FormatDatesWrapper class returned by date constructors with a
.format()method for on-demand output formatting.lactuca.config.ConfigGlobal configuration;
date_formatcontrols which input string representations are accepted.
- class lactuca.FormatDates(input_date: DateLike | Sequence[DateLike] | None = None)#
Lightweight wrapper around list of
datetime.dateobjects with formatting.FormatDatesbehaves as a normal Pythonlistbut provides a convenienceformat(date_format=None)method that returns items formatted according to the project’s globalConfig().date_formatsetting 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.dateis supported: aFormatDatescontaining 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). Plainlist/Series/ndarrayof length 1 remain sequences. Nested list-of-lists from multi-pairanniversary_datesare preserved byformat().All date-like input values are normalized to
datetime.dateobjects during construction using the globalConfig().date_formatsetting.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_dateConstruct dates from components returning FormatDates.
format_dateFormat dates without wrapper construction.
lactuca.config.ConfigGlobal 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
Nonethe globalConfig().date_formatis used.- Returns:
Single-element wrapper with a
dateleaf: scalar result (intfor'ymd_int',strotherwise).Multi-element or nested list-of-lists: nested
listpreserving 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_dateModule-level formatting function for arbitrary date inputs.
lactuca.config.ConfigGlobal 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
boolfor scalar input.NumPy
ndarrayof dtypeboolfor 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 asNDArray[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_yearNumber 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
intfor scalar input.NumPy
ndarrayof dtypeint32for 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 asNDArray[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_yearBoolean leap year detection.
days_in_monthDays 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
intfor scalar inputs.NumPy
ndarrayof dtypeint32for 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 bothyearandmonth; 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_yearLeap year detection.
days_in_yearTotal days in a given year.
end_of_monthLast 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 NumPyint32array.- 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/datetimeobjects, and pandas/Polars date types.- Returns:
If
input_dateis scalar, returns a Pythonintyear.If
input_dateis a sequence, returns a NumPyndarrayof dtypeint32with one year per input element.
- Return type:
int or numpy.ndarray
Notes
Accepts
pandas.Seriesandpolars.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)
- 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 NumPyint32array.- Parameters:
input_date (Union[DateLike, Sequence[DateLike]]) – Single date-like value or sequence/array of date-like values.
- Returns:
Python
intfor scalar input (1-12).NumPy
ndarrayof dtypeint32for sequence input.
- Return type:
int or numpy.ndarray
Notes
Accepts
pandas.Seriesandpolars.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)
- 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 NumPyint32array.- Parameters:
input_date (Union[DateLike, Sequence[DateLike]]) – Single date-like value or sequence/array of date-like values.
- Returns:
Python
intfor scalar input (1-31).NumPy
ndarrayof dtypeint32for sequence input.
- Return type:
int or numpy.ndarray
Notes
Accepts
pandas.Seriesandpolars.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
yearExtract year component.
monthExtract month component.
end_of_monthGet 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
prefixand/orsuffixare 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'produces1 trimestre, etc.Year suffix like
' 2024'for period labeling
- Returns:
If both
prefixandsuffixareNone(default): returns Pythonint(1-4) for scalar input orNDArray[int32]for sequence input.If either
prefixorsuffixis 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'