Dates and intervals
Three rules decide how dates go in and out of fiscalclock. Each one is a design rule of the project (AGENTS.md).
Calendar dates only
Section titled “Calendar dates only”Every date argument is a civil date: a year, month and day, with no time and no time zone. Each language accepts:
| Language | Accepted | Rejected with TIMESTAMP_REJECTED |
|---|---|---|
| Python | an object whose type is exactly datetime.date, or an ISO YYYY-MM-DD string |
datetime.datetime, including a pandas Timestamp |
| JavaScript | an ISO YYYY-MM-DD string or a Temporal.PlainDate |
Date, and Temporal PlainDateTime, ZonedDateTime and Instant |
| Rust | fiscalclock::Date |
Not applicable: no function takes a timestamp type |
A timestamp is rejected because its calendar day depends on the time zone: late evening on 3 Feb in one zone is already 4 Feb in a zone a few hours ahead, and 3 Feb 2024 and 4 Feb 2024 are in different fiscal years in the example calendar. The right day depends on a zone that only you know, so convert the timestamp to a date in the zone you mean before you pass it. A string with a time or a zone suffix is not a date either, and returns INVALID_DATE (API spec).
import datetime as dtfrom pathlib import Path
from fiscalclock import FiscalCalendar, FiscalclockError
cal = FiscalCalendar(Path("retail-4-5-4.json").read_text(encoding="utf-8"))
try: cal.locate(dt.datetime(2024, 2, 3, 23, 30))except FiscalclockError as error: print(error.code, error.path)
print(cal.format(cal.locate(dt.date(2024, 2, 3)).week))TIMESTAMP_REJECTED dateFY2023 W53import { readFileSync } from "node:fs";import { FiscalCalendar } from "fiscalclock";
const cal = new FiscalCalendar(readFileSync("retail-4-5-4.json", "utf8"));
try { cal.locate(new Date(2024, 1, 3, 23, 30));} catch (error) { console.log(error.code, error.path);}
console.log(cal.format(cal.locate("2024-02-03").week));TIMESTAMP_REJECTED dateFY2023 W53use std::error::Error;use std::fs;
use fiscalclock::{Date, FiscalCalendar};
fn main() -> Result<(), Box<dyn Error>> { let cal = FiscalCalendar::from_json(&fs::read_to_string("retail-4-5-4.json")?)?;
println!("{:?}", Date::parse_iso("2024-02-03T23:30:00"));
let date = Date::from_ymd(2024, 2, 3).ok_or("no such date")?; println!("{}", cal.format(&cal.locate(date)?.week, None)?);
Ok(())}NoneFY2023 W53Half-open intervals
Section titled “Half-open intervals”Every period is the half-open interval [start, end_exclusive): it contains start and every day up to, but not including, end_exclusive. last_day is the day before end_exclusive, and is given for display (API spec).
With exclusive ends, one period’s end_exclusive is the next period’s start, so periods meet without a gap or an overlap. The number of days in a period is end_exclusive minus start, and a date d is in a period when start <= d < end_exclusive. The same comparison works for ISO date strings, because they sort in date order.
from pathlib import Path
from fiscalclock import FiscalCalendar
cal = FiscalCalendar(Path("retail-4-5-4.json").read_text(encoding="utf-8"))
quarters = cal.children(cal.period("year", 2023), "quarter")for quarter in quarters: print(quarter.start, quarter.end_exclusive, quarter.days)
print(all(a.end_exclusive == b.start for a, b in zip(quarters, quarters[1:])))2023-01-29 2023-04-30 912023-04-30 2023-07-30 912023-07-30 2023-10-29 912023-10-29 2024-02-04 98Trueimport { readFileSync } from "node:fs";import { FiscalCalendar } from "fiscalclock";
const cal = new FiscalCalendar(readFileSync("retail-4-5-4.json", "utf8"));
const quarters = cal.children(cal.period("year", 2023), "quarter");for (const quarter of quarters) { console.log(quarter.start, quarter.end_exclusive, quarter.days);}
console.log(quarters.slice(1).every((quarter, i) => quarters[i].end_exclusive === quarter.start));2023-01-29 2023-04-30 912023-04-30 2023-07-30 912023-07-30 2023-10-29 912023-10-29 2024-02-04 98trueuse std::error::Error;use std::fs;
use fiscalclock::{FiscalCalendar, Level};
fn main() -> Result<(), Box<dyn Error>> { let cal = FiscalCalendar::from_json(&fs::read_to_string("retail-4-5-4.json")?)?;
let quarters = cal.children(&cal.period(Level::Year, 2023, None)?, Level::Quarter)?; for quarter in &quarters { println!("{} {} {}", quarter.start, quarter.end_exclusive, quarter.days); }
println!("{}", quarters.windows(2).all(|pair| pair[0].end_exclusive == pair[1].start));
Ok(())}2023-01-29 2023-04-30 912023-04-30 2023-07-30 912023-07-30 2023-10-29 912023-10-29 2024-02-04 98trueNo clock
Section titled “No clock”No operation reads the clock, the time zone of the machine, the environment or any other global state. A question about “now”, such as the current fiscal week, takes the date as an argument, so the same call gives the same answer on every machine and in every test. To ask about today, pass today’s date in the time zone you mean.
Supported range
Section titled “Supported range”The API covers fiscal years that lie wholly between 2 Jan 0001 and 30 Dec 9999. A date in, or a request for, any other fiscal year returns OUT_OF_RANGE (spec).