Skip to main content

Calendar-based time window filtering, age calculations, and business logic for dates and times.

Project description

Frist: Unified Age and Calendar Logic

Frist is a modern Python library for property-based calendar, age, and time window calculations. Most operations are performed by accessing properties or calling simple methods—no manual math or low-level datetime manipulation required.


Chrono: Age and Calendar in One Place

The Chrono class is the central interface for time-based logic in Frist. You provide a target_time (the date/time you want to analyze) and a reference_time (often just "now"). Chrono lets you ask both age-related and calendar window questions using simple properties and methods.

Timeline and Window Boundaries for Half-Open Intervals

Suppose you want to know if your target date falls within a window around your reference date. Frist makes this easy:

  |-----------|------------|------------|------------|
  2024-05-06   2024-05-07   2024-05-08   2024-05-09   Date
   2DaysAgo     YesterDay     Target      Ref          Description
    -2           -1           0           +1         Window Index

With Chrono, you specify both dates and the window:

```python
from frist import Chrono
import datetime as dt

target = dt.datetime(2024, 5, 8)
reference = dt.datetime(2024, 5, 9)
chrono = Chrono(target_time=target, reference_time=reference)

print(chrono.cal.in_days(-1))   # True: target is 1 day before reference
print(chrono.cal.in_days(0))    # False: target is not the same day as reference
print(chrono.cal.in_days(-2, 0)) # True: target is in the window from 2 days ago up to reference

In the diagram above, the window for in_days(-2, 0) covers 2024-05-06, 2024-05-07, and 2024-05-08 (target), but not 2024-05-09 (reference). The API answers: is the target in the window? Yes or no.

This windowing works for any time scale—days, weeks, months, etc.—and always uses half-open intervals: the left boundary is inclusive, the right is exclusive.

print(chrono.cal.in_days(-1, 1)) # Is target within 1 day before/after reference?


```text

# Timeline and Window Boundaries for Half Open Intervals*
# Calendar Functions for in_day(start,end)
#
#       |-----------|------------|------------|------------|
#        2024-05-06   2024-05-07   2024-05-08   2024-05-09   Date
#         2DaysAgo     YesterDay     Today       Tomorrow    Description
#                       Target        Ref                    Var Names
#            -2           -1           0           +1        Start/End Indexes
#         
c = Chrono(target_date=may_05,ref_date=05)
print(c.in_days(-1))   #  True,  the target day is 1 day ago
print(c.in_Days(0))    #  False, the target time did not happen on day 0
print(c.in_days(-2,0)) #  True,  the target day is in the range
  • What is a half open interval? A half open inverval is used for setting up ranges along a number line. By having one boundary <= and the other being ==, you are guarenteed that sequential window checks won't be true for the same value in sequential windows. This has the side effect that if you ask for data in the range -1,0 it only returns data for 1 day ago

Explicit Reference Time

You can always specify a custom reference time if you want to compare two specific dates:

target = dt.datetime(2000, 1, 1)
reference = dt.datetime(2024, 1, 1)
chrono = Chrono(target_time=target, reference_time=reference)
print(chrono.age.years)   # Years between 2000 and 2024
# Example output: 24.0
print(chrono.cal.in_days(0))        # Is target the same day as reference?
# Example output: False

Age and Cal Standalone

You can also use Age and Cal directly, but Chrono is recommended for unified logic:

from frist import Age, Cal

age = Age(target_time=target, ref_time=reference)
print(age.years)  # Example output: 24.0
print(age.days)   # Example output: 8766

cal = Cal(target_dt=target, ref_dt=reference)
print(cal.in_days(0))      # Example output: False
print(cal.in_days(-2, 2))  # Example output: False

Calendar Policy: The calendar object can calculate workdays, business hours, holidays, and more (details in a later section). By default, the policy is:

  • Workdays: Monday–Friday
  • Work hours: 9AM–5PM
  • Holidays: none

This policy can easily be changed to fit your needs.


Key Features

  • Property-based API: Access date and time information through properties and high-level methods.
  • Calendar windows: Easily check if a date falls within a day, week, month, quarter, fiscal period, or custom working day window.
  • Working day and holiday logic: Built-in support for excluding weekends and holidays from all calendar calculations. Simply provide a set of holiday dates and Frist will automatically skip them in working day windows and related queries.
  • Customizable business calendars: Define your own holiday sets and fiscal year start months for precise business logic.
  • Age calculations: Compute age spans and durations using flexible input types.
  • No manual math required: Most operations are declarative and require no arithmetic or direct datetime handling.

Installation

pip install frist

Or clone the repository and install locally:

git clone https://github.com/hucker/frist.git
cd frist
pip install .

Usage

Calendar Operations

from frist import Cal
import datetime as dt

cal = Cal(target_dt=dt.datetime(2024, 5, 8), ref_dt=dt.datetime(2024, 5, 6), holidays={"2024-05-08"})
print(cal.in_workdays(-2, 2))  # True/False
print(cal.in_months(0))        # True/False
print(cal.in_fiscal_quarters(0)) # True/False

Age Calculations

from frist import Age
import datetime as dt

age = Age(dt.datetime(2000, 1, 1), dt.datetime(2024, 1, 1))
print(age.years)  # Property: number of years
print(age.days)   # Property: number of days

age_now = Age(dt.datetime(2000, 1, 1))
print(age_now.years)

API Highlights

Cal

  • target_dt, ref_dt: Properties for target and reference datetimes
  • in_days, in_weeks, in_months, in_quarters, in_years, in_workdays, in_fiscal_quarters, in_fiscal_years: Methods to check if the target date falls within various calendar windows
  • holiday, fiscal_year, fiscal_quarter: Properties for holiday and fiscal calculations

Age

  • years, months, days, seconds: Properties for age span
  • working_days: Property for fractional working days between two datetimes, fully respects custom calendar policies
  • Flexible initialization: accepts datetimes, timestamps, or protocols

Years Calculation: Approximation Note

The years property uses an approximate value of 365.25 days per year, averaging leap and non-leap years for simplicity. If you require exact calendar year calculations (counting 365-day and 366-day years precisely), you will need to implement custom logic to count regular and leap years and handle fractional years carefully. This is left as an exercise for the reader, as it complicates the implementation and is rarely needed for most business use cases.

Example:

age = Age(dt.datetime(2000, 1, 1), dt.datetime(2024, 1, 1))
print(age.years)  # Uses 365.25 days/year for approximation

Arbitrary Calendar Policy Support

The Age.working_days property supports arbitrary calendar policies:

  • Workdays can be any combination of weekdays (e.g., Mon, Wed, Fri, Sun)
  • Holidays can be irregular and non-contiguous
  • Business hours can vary per day
  • No assumptions about contiguous workweeks or regular schedules

Correctness is prioritized over efficiency. The algorithm iterates day-by-day, checking each date against the calendar policy for workdays, holidays, and business hours. This ensures accurate results for any custom business calendar, even if workdays, holidays, or hours are highly irregular. Optimization is possible, but correctness is preferred unless efficiency is shown to be a bottleneck.


Configuration

  • Holidays: Pass a set of date strings (YYYY-MM-DD) to exclude from working day calculations
  • Fiscal year start: Set fy_start_month for fiscal calculations

Testing

  • Comprehensive test suite covers edge cases, holidays, weekends, and exception handling
  • Run tests with:
pytest

Contributing

Pull requests and issues are welcome! See the repository for guidelines.


License

MIT License


Acknowledgments

Inspired by real-world business calendar needs and designed for clarity and ease of use.

Chrono objects support fiscal year and quarter calculations with customizable fiscal year start months. For example:

# Fiscal year starts in April (fy_start_month=4)
meeting = Chrono(target_time=dt.datetime(2025, 7, 15), fy_start_month=4)
print(meeting.fiscal_year)      # 2025 (fiscal year for July 15, 2025)
print(meeting.fiscal_quarter)   # 2 (Q2: July–September for April start)

# Check if a date is in a fiscal quarter or year window
if meeting.cal.in_fiscal_quarters(0):
  print("Meeting is in the current fiscal quarter.")
if meeting.cal.in_fiscal_years(0):
  print("Meeting is in the current fiscal year.")

Holiday Detection Example

Frist can instantly check if a date is a holiday using a set of holiday dates:

holidays = {
  '2025-12-25',  # Christmas
  '2025-01-01',  # New Year's Day
if project.holiday:
  print("Project start date is a holiday!")

Short Examples

Age Calculation

person = Chrono(target_time=dt.datetime(1990, 5, 1), reference_time=dt.datetime(2025, 5, 1))
print(f"Age in days: {person.age.days}, Age in years: {person.age.years:.2f}")

Calendar Windows

meeting = Chrono(target_time=dt.datetime(2025, 12, 25))
if meeting.cal.in_days(0):
  print("Meeting is today!")
if meeting.cal.in_weeks(-1):
  print("Meeting was last week.")

API Reference

Frist

Chrono(target_time: datetime, reference_time: datetime = None, fy_start_month: int = 1, holidays: set[str] = None)

  • Properties:
    • age: Age object with properties for .days, .hours, .minutes, .seconds, .weeks, .months, .quarters, .years, .fiscal_year, .fiscal_quarter. *- cal: Cal object for calendar window logic.
    • fiscal_year: Fiscal year for the target time.
    • fiscal_quarter: Fiscal quarter for the target time.
    • holiday: True if target time is a holiday (if holidays set provided).

Cal

  • Properties:

    • dt_val: Target datetime.
    • base_time: Reference datetime.
    • fiscal_year: Fiscal year for dt_val.
    • fiscal_quarter: Fiscal quarter for dt_val.
    • holiday: True if dt_val is a holiday.
  • Interval Methods:

    • in_minutes(start: int = 0, end: int | None = None) -> bool
    • in_hours(start: int = 0, end: int | None = None) -> bool
    • in_days(start: int = 0, end: int | None = None) -> bool
    • in_weeks(start: int = 0, end: int | None = None, week_start: str = "monday") -> bool
    • in_months(start: int = 0, end: int | None = None) -> bool
    • in_quarters(start: int = 0, end: int | None = None) -> bool
    • in_years(start: int = 0, end: int | None = None) -> bool
    • in_fiscal_quarters(start: int = 0, end: int | None = None) -> bool
    • in_fiscal_years(start: int = 0, end: int | None = None) -> bool
  • Static Methods:

    • get_fiscal_year(dt: datetime, fy_start_month: int) -> int
    • get_fiscal_quarter(dt: datetime, fy_start_month: int) -> int
  • Exceptions:

  • All interval methods raise ValueError if start > end.

  • normalize_weekday(day_spec: str) -> int raises ValueError for invalid day specifications, with detailed error messages.

Age

Age(target_time: datetime, reference_time: datetime)

  • Properties:

    • days, hours, minutes, seconds, weeks, months, quarters, years, fiscal_year, fiscal_quarter
  • Properties:

    • target_dt: Target datetime.
    • ref_dt: Reference datetime.

Testing and Support

Python Coverage Ruff

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

frist-0.10.0.tar.gz (38.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

frist-0.10.0-py3-none-any.whl (19.1 kB view details)

Uploaded Python 3

File details

Details for the file frist-0.10.0.tar.gz.

File metadata

  • Download URL: frist-0.10.0.tar.gz
  • Upload date:
  • Size: 38.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.4

File hashes

Hashes for frist-0.10.0.tar.gz
Algorithm Hash digest
SHA256 8fa90b8bcf2ea80086c2e5985f37ce5cbedfb143fcd93f80cc87f58ad3bc9016
MD5 9bf85d77144b2edb568b18946dbc7b38
BLAKE2b-256 35008b0718373b075e7b6c72bab0846d557325aaa72fe275e99dca01c8619e0a

See more details on using hashes here.

File details

Details for the file frist-0.10.0-py3-none-any.whl.

File metadata

  • Download URL: frist-0.10.0-py3-none-any.whl
  • Upload date:
  • Size: 19.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.5.4

File hashes

Hashes for frist-0.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6624c400e4eb6837a9bce4a6fcc4479222d94aae8e775bb698cf695d8c595555
MD5 071e8ffa1ff7b21116759d60f3516513
BLAKE2b-256 70d43bd9508f8ae94da3f9ed08db2ec796243ebe829ed67b8bc2a5bc38286581

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page