# Dates and intervals

Why fiscalclock takes only calendar dates and rejects timestamps, why every period has an exclusive end, and why it never reads the clock.

Three rules decide how dates go in and out of fiscalclock. Each one is a design rule of the project ([`AGENTS.md`](https://github.com/Karthik-beta/fiscalclock/blob/main/AGENTS.md#design-rules)).

## 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](https://fiscalclock.karthik.nz/reference/spec/api-v1/#conventions)).

**Python**

*timestamps.py*

```python
import datetime as dt
from 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))
```

*Output*

```txt
TIMESTAMP_REJECTED date
FY2023 W53
```

**JavaScript**

*timestamps.mjs*

```js
import { 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));
```

*Output*

```txt
TIMESTAMP_REJECTED date
FY2023 W53
```

**Rust**

*timestamps.rs*

```rust
use 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(())
}
```

*Output*

```txt
None
FY2023 W53
```

## 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](https://fiscalclock.karthik.nz/reference/spec/api-v1/#period)).

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.

**Python**

*half_open.py*

```python
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:])))
```

*Output*

```txt
2023-01-29 2023-04-30 91
2023-04-30 2023-07-30 91
2023-07-30 2023-10-29 91
2023-10-29 2024-02-04 98
True
```

**JavaScript**

*half_open.mjs*

```js
import { 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));
```

*Output*

```txt
2023-01-29 2023-04-30 91
2023-04-30 2023-07-30 91
2023-07-30 2023-10-29 91
2023-10-29 2024-02-04 98
true
```

**Rust**

*half_open.rs*

```rust
use 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(())
}
```

*Output*

```txt
2023-01-29 2023-04-30 91
2023-04-30 2023-07-30 91
2023-07-30 2023-10-29 91
2023-10-29 2024-02-04 98
true
```

## 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

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](https://fiscalclock.karthik.nz/reference/spec/definition-v1/#supported-range)).
