Skip to content

Calendar and money rules

What is one month after January 31st? Is a 12-month lease that starts on March 1st over on March 1st or on the last day of February? Does 2.625 round to 2.62 or 2.63? Documents rarely say, and different systems answer differently. This page describes how Tickmarks answers, for every value a relation computes.

The rules are fixed for each schema version, so the same document and schema always give the same answer. Where a rule has a setting, such as eom or end, you can change it in the schema’s defaults or on a single relation.

Tickmarks stores a duration as a number of months and a number of days, plus a time part for date-times. When it reads a duration, a year becomes 12 months and a week becomes 7 days, so P1Y equals P12M. Months and days are never converted into each other, though, because a month isn’t a fixed number of days: P1M is not P30D.

Tickmarks adds the months first, then the days. The order matters: 2027-01-30 + P1M2D first adds one month, which lands on 2027-02-28 because February has no 30th (see below), and then two days, giving 2027-03-02.

Adding a month to January 31st lands on a date that doesn’t exist. The eom setting decides what happens: clamp moves to the last day of the month, and strict gives no result.

clamp (default) strict
2027-01-31 + P1M 2027-02-28 no result
2028-01-31 + P1M 2028-02-29 no result
2028-02-29 + P12M 2029-02-28 no result
2028-02-29 + P48M 2032-02-29 2032-02-29

“No result” means that a document stating that combination contradicts the relation.

Clamping has a side effect when Tickmarks works backwards. Four start dates give 2027-02-28 after adding P1M under clamp: 2027-01-28, -29, -30 and -31. Under strict only 2027-01-28 does. If the document doesn’t state the start date and several are possible, the field is underdetermined until other evidence decides.

Leases and contracts disagree about what “end date” means. With end: exclusive, the default, it’s the first day after the period, so endDate == startDate + duration holds exactly. With inclusive, it’s the last day of the period.

12 months from 2027-03-01 End
exclusive 2028-03-01
inclusive 2028-02-29

Use inclusive when your documents say “until and including”.

endDate - startDate gives a duration, and the basis of the field it’s compared with decides how that duration is counted. The same two dates can give quite different answers:

2027-01-31 → 2027-03-15 clamp strict
calendar: whole months, then the days left 1 month 15 days 43 days
months: exact whole months only no result no result
days 43 days 43 days
2027-01-31 → 2027-02-28 clamp strict
calendar 1 month 28 days
months 1 month no result
days 28 days 28 days

A months difference is never rounded, so if the dates aren’t a whole number of months apart, there is no result. If the end is before the start, both parts of a calendar difference are negative, counted from the start date.

Durations of different bases can’t be compared (E-BASIS). Two calendar durations are compared only when the answer is the same whatever date you start from. P1M3D > P1M works, but P1M against P30D is refused, since a month is 28 to 31 days, and so is P2M against P1M5D.

Dates run from 0001-01-01 to 9999-12-31. A result outside that range is an error, and that includes the intermediate date after adding the months.

You can only add whole days to a date. A date-time takes exact durations only (hours, minutes, seconds) and keeps its UTC offset: 2027-02-28T23:00:00+01:00 + PT2H is 2027-03-01T01:00:00+01:00. Named time zones and daylight saving aren’t supported, so the offset stays the same even when a period crosses a clock change.

Arithmetic in Tickmarks is exact, with no floating point anywhere, so 0.1 + 0.2 == 0.3 is true. Integers are 64-bit, and an overflow is an error rather than a silent wrap. Decimals hold up to 28 digits after the point, and a result that needs more is an error too.

Because arithmetic is exact, equality has no tolerance either. If a document shows rounded figures, round explicitly in the relation.

Given total == net + vat and a known total and net, Tickmarks derives vat by subtraction. It only accepts such an inverse if putting the result back into the relation reproduces the known value exactly:

Relation and known values Derived
total == net + vat, total 150.00, net 100.00 vat 50.00
12.10 == amount * 1.21 amount 10
10.00 == amount * 4 amount 2.5
10.00 == amount * 3 nothing: 10 / 3 is not exact

The default rounding mode, half_even, sends a tie to the even digit, which surprises people who expect ties to round up. Both of these round to 2 places:

half_even (default) half_up
12.50 × 21 / 100 = 2.625 2.62 2.63
1.005 1.00 1.01

1.005 is a tie that half_even rounds down to 1.00. Many invoicing systems round half up instead, and if yours do, say so in the relation: round(net * rate / 100, 2, half_up).

A money field’s currency is its own currency if it sets one, otherwise defaults.currency, otherwise EUR. An amount in the document without a currency marker is read in the field’s currency, while an amount with a different marker ($1,200.00 on a euro field) fails to normalize. Currencies never mix and are never converted: 100.00 EUR + 50.00 USD is a compile error.

A value fits a field’s scale when it has no more digits after the point, ignoring trailing zeros. With scale 1, 1.50 fits and 1.55 doesn’t.

A duration read from the document keeps the units it was written in, so “1 year” shows as P1Y and “2 weeks” as P2W. A duration Tickmarks computes is shown in months and days (P12M, P14D). That’s only display: P1Y == P12M is still true.