# datemath/v1 — Calendar date arithmetic

Deterministic calendar-date math. On the "Test of Time" benchmark, GPT-4
scores 88% on timezone questions but only 16% on date-difference/duration
questions — these errors tend to be off-by-one-day, "structurally close to
correct," and easy to miss by eye.

This module is pure calendar-date arithmetic — no time-of-day, no
timezone. Dates are `"YYYY-MM-DD"` strings (ISO 8601, date-only); anything
else, or a calendar-invalid date like `"2026-02-30"`, is rejected with a
400.

**Not covered here:** timezone conversion or "current time" — see
Anthropic's official `mcp-server-time` for that; duplicating it isn't
useful.

## Two deliberate, non-obvious choices

1. **`/add` clamps to the end of the month**, it does not overflow. Go's
   own `time.Time.AddDate` computes `2026-01-31` + 1 month as "day 31 of
   February," which doesn't exist, and silently rolls over to
   `2026-03-03`. That's exactly the kind of subtle, structurally-plausible
   bug this module exists to avoid, so `/add` clamps instead:
   `2026-01-31` + 1 month = `2026-02-28`. Verify it yourself with the
   example below.
2. **`/businessdays` is inclusive on both ends** — `from` and `to` are
   both counted if they land on a business day. If you want the days
   *between* two dates exclusive of the endpoints, subtract 1 from the
   result (or 2 if neither endpoint should count).

## `POST /v1/datemath/diff`

`to` minus `from`, in whole days. Negative if `to` is before `from`.

```bash
curl -s -X POST https://apished.com/v1/datemath/diff \
  -H 'Content-Type: application/json' \
  -d '{"from": "2026-01-01", "to": "2026-03-01"}'
# {"days":59}
```

## `POST /v1/datemath/businessdays`

Counts days in `[from, to]` (inclusive) whose weekday is in `weekdays`
(optional, default `["mon","tue","wed","thu","fri"]`) and which aren't in
`holidays` (optional, `"YYYY-MM-DD"` strings).

```bash
curl -s -X POST https://apished.com/v1/datemath/businessdays \
  -H 'Content-Type: application/json' \
  -d '{"from": "2026-08-17", "to": "2026-08-23"}'
# {"businessDays":5}
```

## `POST /v1/datemath/leapyear`

```bash
curl -s -X POST https://apished.com/v1/datemath/leapyear \
  -H 'Content-Type: application/json' \
  -d '{"year": 2024}'
# {"leapYear":true}
```

## `POST /v1/datemath/age`

`asOf` is optional and defaults to the current UTC date. A `birthDate` of
Feb 29 is treated as falling on March 1 in a non-leap `asOf` year — the
standard convention.

```bash
curl -s -X POST https://apished.com/v1/datemath/age \
  -H 'Content-Type: application/json' \
  -d '{"birthDate": "2000-02-29", "asOf": "2026-02-28"}'
# {"age":25}
```

## `POST /v1/datemath/dayofweek`

Returns both the weekday name and its ISO 8601 number (1=Monday..7=Sunday).

```bash
curl -s -X POST https://apished.com/v1/datemath/dayofweek \
  -H 'Content-Type: application/json' \
  -d '{"date": "2026-08-17"}'
# {"dayOfWeek":"Monday","isoWeekday":1}
```

## `POST /v1/datemath/add`

Adds `years`, `months`, and `days` (each optional, default 0, may be
negative) to `date`. `years`/`months` are applied together with
end-of-month clamping (see above); `days` is applied afterward as plain
calendar-day arithmetic.

```bash
curl -s -X POST https://apished.com/v1/datemath/add \
  -H 'Content-Type: application/json' \
  -d '{"date": "2026-01-31", "months": 1}'
# {"result":"2026-02-28"}
```

## Files

- `datemathmodule.go` — module wiring (routes, HTTP handlers)
- `datemath.go` — pure date-math logic
- `datemath_test.go` — test vectors, including the Jan-31 clamping case
