regit-daycount 1.0.1

Day-count fractions and business-day calendars in pure Rust — Act/360, Act/365F, ActAct ISDA, ActAct ICMA, 30/360, 30E/360, Bus/252; TARGET2, US, UK, JP, CH, HK, SG holiday calendars; all date-roll conventions. Zero dependencies, no_std, no alloc.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
<!-- Copyright 2026 Regit.io — Nicolas Koenig -->
<!-- SPDX-License-Identifier: Apache-2.0 -->

# SPEC.md — regit-daycount

> The reference specification for every day-count fraction, date-roll
> convention, and holiday calendar this crate computes. For each it states
> the structure (inputs, outputs, denominator), the algorithm in plain-text
> procedure form, one worked example computed by hand against an ISDA / ICMA
> reference case, and the governing standard cited by number.
>
> This document is the public, citable distillation of the crate's internal
> verified algorithm reference. The crate is the executable form of this
> specification — every rule stated here is enforced by the `src/` module
> named in its heading, and a computation never trusts a supplied convention:
> it dispatches on the typed enum and runs the algorithm of the governing
> standard.

---

## Table of contents

1. [Conventions]#conventions
2. [Date primitives — `src/date.rs`]#date-primitives--srcdaters
3. [Act/360 — ISDA 2006 §4.16(e) — `src/day_count/act_360.rs`]#act360--isda-2006-416e--srcday_countact_360rs
4. [Act/365F — ISDA 2006 §4.16(d) — `src/day_count/act_365f.rs`]#act365f--isda-2006-416d--srcday_countact_365frs
5. [ActAct ISDA — ISDA 2006 §4.16(b) — `src/day_count/act_act_isda.rs`]#actact-isda--isda-2006-416b--srcday_countact_act_isdars
6. [ActAct ICMA — ICMA Rule 251 — `src/day_count/act_act_icma.rs`]#actact-icma--icma-rule-251--srcday_countact_act_icmars
7. [30/360 BondBasis — ISDA 2006 §4.16(f) — `src/day_count/thirty_360_bond_basis.rs`]#30360-bondbasis--isda-2006-416f--srcday_countthirty_360_bond_basisrs
8. [30E/360 — ISDA 2006 §4.16(g) — `src/day_count/thirty_e_360.rs`]#30e360--isda-2006-416g--srcday_countthirty_e_360rs
9. [30E/360 ISDA — ISDA 2006 §4.16(h) — `src/day_count/thirty_e_360_isda.rs`]#30e360-isda--isda-2006-416h--srcday_countthirty_e_360_isdars
10. [Act/365L — ICMA — `src/day_count/act_365l.rs`]#act365l--icma--srcday_countact_365lrs
11. [NL/365 — conventional — `src/day_count/nl_365.rs`]#nl365--conventional--srcday_countnl_365rs
12. [Bus/252 — Brazilian convention — `src/day_count/bus_252.rs`]#bus252--brazilian-convention--srcday_countbus_252rs
13. [1/1 — OIS shortcut — `src/day_count/one_one.rs`]#11--ois-shortcut--srcday_countone_oners
14. [Unadjusted date roll — `src/roll.rs`]#unadjusted-date-roll--srcrollrs
15. [Following date roll — `src/roll.rs`]#following-date-roll--srcrollrs
16. [ModifiedFollowing date roll — `src/roll.rs`]#modifiedfollowing-date-roll--srcrollrs
17. [Preceding date roll — `src/roll.rs`]#preceding-date-roll--srcrollrs
18. [ModifiedPreceding date roll — `src/roll.rs`]#modifiedpreceding-date-roll--srcrollrs
19. [Nearest date roll — `src/roll.rs`]#nearest-date-roll--srcrollrs
20. [EndOfMonth date roll — `src/roll.rs`]#endofmonth-date-roll--srcrollrs
21. [TARGET2 calendar — ECB fixed rule — `src/calendar/target2.rs`]#target2-calendar--ecb-fixed-rule--srccalendartarget2rs
22. [Luxembourg calendar — Loi du 21 juillet 1928 — `src/calendar/luxembourg.rs`]#luxembourg-calendar--loi-du-21-juillet-1928--srccalendarluxembourgrs
23. [United States calendar — NYSE / Federal Reserve — `src/calendar/united_states.rs`]#united-states-calendar--nyse--federal-reserve--srccalendarunited_statesrs
24. [United Kingdom calendar — Bank of England — `src/calendar/united_kingdom.rs`]#united-kingdom-calendar--bank-of-england--srccalendarunited_kingdomrs
25. [Japan calendar — JPX — `src/calendar/japan.rs`]#japan-calendar--jpx--srccalendarjapanrs
26. [Switzerland calendar — SIX — `src/calendar/switzerland.rs`]#switzerland-calendar--six--srccalendarswitzerlandrs
27. [Hong Kong calendar — HKEX — `src/calendar/hong_kong.rs`]#hong-kong-calendar--hkex--srccalendarhong_kongrs
28. [Singapore calendar — SGX — `src/calendar/singapore.rs`]#singapore-calendar--sgx--srccalendarsingaporers
29. [Calendar dispatchers and business-day primitives — `src/calendar/mod.rs`]#calendar-dispatchers-and-business-day-primitives--srccalendarmodrs
30. [Composite / JointBusiness combinators — `src/calendar/composite.rs`]#composite--jointbusiness-combinators--srccalendarcompositers
31. [Standards index]#standards-index

---

## Conventions

The conventions below hold for every fraction, roll, and calendar in this
document.

- **Date primitive.** The single date type is `Date`, a proleptic Gregorian
  `(year, month, day)` triple. The accepted year range is
  `1583..=9999` — the Gregorian calendar took effect in October 1582, and
  from 1583 onward every day-count and holiday-calendar rule this crate
  implements is defined uniformly. Construction is validated by
  `Date::ymd`; out-of-range or impossible triples (29 February in a
  non-leap year, day 31 of April) are rejected with the typed
  `ValidationError`. There is no second date type — every fraction, roll
  and calendar takes and returns `Date` values.
- **Half-open interval `[start, end)`.** Every day count in this crate is
  end-exclusive, start-inclusive. `days_between(2026-01-01, 2026-01-02)`
  returns `1`, not `2`. The same convention applies to leap-day inclusion
  (Act/365L, NL/365), to business-day counts (Bus/252,
  `business_days_between`), and to the comparison `start <= ymd(y, 2, 29) <
  end` that decides whether a given 29 February belongs to a period. The
  half-open form is the single convention the standards (ISDA §4.16, ICMA
  Rule 251) themselves use; mixing in a closed-interval count anywhere is a
  bug.
- **Inverted intervals.** `start > end` is not rejected — some callers
  compute reverse-period accruals. Every fraction returns the natural
  negation of the forward result: `fraction(end, start) == -fraction(start,
  end)`, exactly. The two exceptions are `1/1` (which returns `1.0` for
  every input, including the inverted one, because the convention is
  definitional) and the dispatcher arms that return `f64::NAN` (which
  propagate NaN by IEEE-754).
- **Weekday rule.** Saturday and Sunday are the universal weekend; every
  calendar in this crate uses the Western Sat/Sun convention. `is_weekend`
  reports this independently of any calendar; `is_business_day(date, cal)`
  is the documented composition `!is_weekend(date) && !is_holiday(date,
  cal)`. Markets with a Fri/Sat weekend (Saudi Arabia, the UAE pre-2022)
  are out of scope.
- **Holiday-on-weekend reporting.** A fixed-date holiday that falls on a
  Saturday or Sunday is still reported by `is_holiday` for its rule date.
  The schedule does not "shift" it; the weekend test composes with the
  holiday test through `is_business_day`. For the dated-snapshot calendars
  (US, UK, JP, CH, HK, SG) the *observed* shift is encoded into the
  snapshot itself (so e.g. the snapshot lists 2026-07-03 — the observed
  Independence Day — alongside 2026-07-04 only when the published list
  records both).
- **Holiday detection is calendar-only.** `is_holiday(date, cal)` reports
  true *only* for calendar holidays; weekends are out of scope here.
  Combine with `is_weekend` (or use the `is_business_day` wrapper) for the
  full "market open?" predicate.
- **Out-of-coverage calendar lookups.** The six snapshot calendars (US, UK,
  JP, CH, HK, SG) embed a dated table whose coverage range is `(2020,
  2040)` inclusive, with `SNAPSHOT_DATE = "2026-05-23"` recorded in the
  source. A lookup for a date outside the coverage window returns `false`
  — the conservative answer — never a panic and never an extrapolation.
  Auditors should pin the snapshot date when reporting calendar validity,
  because the published schedules are revised continually. TARGET2 is the
  exception: it is fully rule-based and exact for every year in the
  supported range.
- **Verification, not trust.** The `DayCount` enum dispatches a typed
  variant to the algorithm of the governing standard. The caller cannot
  pass a free-form rule and have it executed — every computation runs the
  cited procedure verbatim. A wrong cashflow caused by a silently-misapplied
  convention is the worst possible failure, and this crate is the
  executable form of the specification so that the rule and the code
  cannot drift.
- **NaN as the cannot-compute sentinel.** Two `DayCount` variants need
  information the two-argument dispatcher cannot supply on its own:
  `ActActIcma` needs a reference period and frequency, and `Bus252` needs
  a business-day predicate. For both, `day_count::fraction(start, end,
  basis)` returns `f64::NAN`. NaN is chosen deliberately: it propagates
  loudly through arithmetic — any downstream multiplication produces NaN
  in turn — where a silent `0.0` would absorb the misuse and produce a
  wrong cashflow with no warning. Callers route through
  `year_fraction_with_freq` (for ICMA) or the per-module
  `bus_252::fraction` (for Bus/252) with the missing input.
  `ThirtyE360Isda` similarly takes an extra `end_is_maturity` flag the
  dispatcher cannot infer; the dispatcher defaults it to `false` and the
  full signature lives at `thirty_e_360_isda::fraction`.
- **Tolerance.** The crate's working numeric slack is `1e-12` — loose
  enough that a benign last-bit rounding never causes a spurious test
  failure, tight enough that any drift large enough to mis-state a cashflow
  at the cent level (≥ 1e-9 on a unit notional) is caught. Worked examples
  in this document quote the value to the precision the implementation
  returns, which is the result of one or two `f64` divisions of exact
  rationals — the worst-case relative error is a few ulps of the printed
  number. Cross-oracle vectors transcribed verbatim from QuantLib's
  `daycounters.cpp` are compared at a slightly wider `1e-10` because
  QuantLib prints the expected value with twelve decimal digits and the
  trailing digits are a round-and-truncate of QuantLib's own `f64`.
- **Citation form.** Every section cites the governing standard by number
  and edition (e.g. "ISDA 2006 Definitions §4.16(e)" or "ICMA Rule 251");
  every worked example transcribed from QuantLib also cites the source
  line in `test-suite/daycounters.cpp` at release tag `v1.34`. The Standards
  index in §29 maps each cited standard to the section that references it.
- **No allocation, no `std`.** The crate is `#![no_std]` and
  `#![forbid(unsafe_code)]`. `Date` is a `Copy` triple, every algorithm is
  hand-rolled integer arithmetic, holiday tables are static sorted arrays,
  and there is no runtime dependency. The same audited code runs in a
  backend service, a WASM bundle, or on an embedded device.

---

## Date primitives — `src/date.rs`

The `Date` type is the only date primitive used in this crate. It is a
`Copy`, three-field struct — a signed 32-bit year and two `u8` fields —
holding a proleptic Gregorian `(year, month, day)` triple. Five operations
on `Date` are load-bearing for the rest of the specification and are
specified individually below.

### Structure

```text
Length: 3 fields.

  ┌──────┬───────┬─────┐
  │ year │ month │ day │
  └──┬───┴───┬───┴──┬──┘
     │       │       └ 1..=days_in_month(year, month)
     │       └───────── 1..=12
     └───────────────── 1583..=9999  (Date::MIN_YEAR..=Date::MAX_YEAR)

Validation:  Date::ymd(year, month, day) -> Result<Date, ValidationError>
Trust-me:    Date::ymd_unchecked(year, month, day) -> Date  (const)
Accessors:   year() / month() / day()   (all const)
```

`Date::ymd` validates in three steps: year in `MIN_YEAR..=MAX_YEAR`, month
in `1..=12`, day in `1..=days_in_month(year, month)`. The unchecked form
exists for `const`-context construction and for reconstructing a `Date`
from fields validated earlier; any untrusted input must go through
`Date::ymd`.

`PartialOrd` and `Ord` follow the natural chronological order — equivalent
to lexicographic order on `(year, month, day)`, which is the same thing for
Gregorian dates.

### Epoch conversion — Hinnant

Every arithmetic operation on `Date` (`add_days`, `days_between`,
`day_of_week`) is grounded in **Howard Hinnant's `days_from_civil` /
`civil_from_days`** algorithm — a public-domain, branchless, integer-only
conversion between a `(year, month, day)` triple and a signed count of
days from the civil epoch 1970-01-01 — transcribed verbatim in `i64`. The
original derivation is at
<http://howardhinnant.github.io/date_algorithms.html>. The two conversion
helpers are `const fn` private to the module; every public arithmetic
operation factors through them.

### `is_leap_year(year)`

```text
is_leap_year(y) = (y % 4 == 0 AND y % 100 != 0) OR (y % 400 == 0)
```

So 1900 is not a leap year (divisible by 100 but not 400), 2000 is
(divisible by 400), and 2100 is not.

**Worked example.** `is_leap_year(2024) = true` (`2024 % 4 == 0` and
`2024 % 100 == 24 ≠ 0`). `is_leap_year(1900) = false` (`1900 % 4 == 0`
and `1900 % 100 == 0` and `1900 % 400 == 300 ≠ 0`). `is_leap_year(2025) =
false` (`2025 % 4 == 1`).

### `day_of_week`

```text
z   = to_civil_days(year, month, day)   // signed count from 1970-01-01
idx = (z + 3).rem_euclid(7)             // 1970-01-01 was a Thursday
return (Mon, Tue, Wed, Thu, Fri, Sat, Sun)[idx]
```

The `+ 3` offsets Thursday (the civil epoch's weekday) to index 3 in the
Monday-first variant table; `rem_euclid` lifts the signed civil-day count
into the non-negative weekday index without a manual shift.

**Worked example.** `Date::ymd(2026, 5, 23).unwrap().day_of_week() ==
Weekday::Sat`. Cross-verified against the Apollo 11 anchor: 1969-07-20
was a Sunday, and the crate's per-anchor test set pins both.

### `easter_sunday(year)`

```text
Anonymous Gregorian computus (Meeus / Butcher form). Variable names follow
Meeus, Astronomical Algorithms, 2nd ed., §8:

  a = y mod 19
  b = y / 100
  c = y mod 100
  d = b / 4
  e = b mod 4
  f = (b + 8) / 25
  g = (b - f + 1) / 3
  h = (19a + b - d - g + 15) mod 30
  i = c / 4
  k = c mod 4
  l = (32 + 2e + 2i - h - k) mod 7
  m = (a + 11h + 22l) / 451
  month = (h + l - 7m + 114) / 31      // 3 = March, 4 = April
  day   = ((h + l - 7m + 114) mod 31) + 1
```

The algorithm is integer-only and exact for every Gregorian year. It
underlies the TARGET2 holiday rule (Good Friday = Easter − 2, Easter
Monday = Easter + 1) and the Good Friday entry of the US and UK snapshot
calendars.

**Worked example.** `Date::easter_sunday(2026) == Date::ymd(2026, 4, 5)`.
Other anchors pinned by the unit tests: 2024-03-31, 2025-04-20,
2027-03-28, 2038-04-25.

### `add_months_eom_aware(months)`

```text
total     = year * 12 + (month - 1) + months
new_year  = total div 12
new_month = (total mod 12) + 1
new_day   = min(self.day, days_in_month(new_year, new_month))
return Date(new_year, new_month, new_day)
```

The clamp on `new_day` is the load-bearing rule: a date already at the end
of its month rolls forward to the end of the new month, not "the same
numeric day, even though that day does not exist". So `2026-01-31 + 1
month = 2026-02-28` (non-leap) and `2024-01-31 + 1 month = 2024-02-29`
(leap). The arithmetic is performed in `i64` to avoid overflow on the
extremes of the supported year range.

**Worked example.** `Date::ymd(2024, 1, 31).unwrap().add_months_eom_aware(1)
== Date::ymd(2024, 2, 29).unwrap()`. The same call on 2026 (non-leap) lands
on 2026-02-28, demonstrating the leap-year-aware clamp.

### Governing standards

ISO 8601, *Date and time — Representations for information interchange*,
§3.4.1 (calendar date) and §3.4.2 (proleptic Gregorian calendar). Howard
E. Hinnant, *chrono-Compatible Low-Level Date Algorithms*,
<http://howardhinnant.github.io/date_algorithms.html> (public domain).
Jean Meeus, *Astronomical Algorithms*, 2nd ed., Willmann-Bell, 1998, §8
"The date of Easter".

---

## Act/360 — ISDA 2006 §4.16(e) — `src/day_count/act_360.rs`

Act/360 is the money-market default: the year fraction between two dates
is the actual count of calendar days separating them, divided by the fixed
denominator 360. Every major short-term floating-rate index settled in
USD or EUR — SOFR, €STR, the legacy USD-LIBOR fixings, the Fed Funds
effective rate — accrues on Act/360, and so do the EUR and USD legs of the
overwhelming majority of interest-rate swaps. It is the simplest of the
eleven conventions in this crate: a signed day count over the period,
then one division.

### Structure

```text
fraction(start, end) = days_between(start, end) / 360.0

where days_between is signed, end-exclusive, start-inclusive.
```

The denominator is the literal constant `360`. The numerator is signed —
an inverted interval (`start > end`) yields a negative fraction.

### Worked example — 90-day interval

```text
start = 2026-01-01
end   = 2026-04-01
days  = 31 (Jan) + 28 (Feb, 2026 non-leap) + 31 (Mar) = 90
f     = 90 / 360 = 0.25
```

Asserted in `tests/integration.rs::golden::act_360_90_days` to within the
crate-wide `TOL = 1e-12`.

### Governing standard

ISDA 2006 Definitions §4.16(e), *Actual/360*.

---

## Act/365F — ISDA 2006 §4.16(d) — `src/day_count/act_365f.rs`

Act/365F (Actual/365 Fixed) is the sterling-bloc money-market default: the
year fraction between two dates is the actual count of calendar days
separating them, divided by the fixed denominator 365. GBP, JPY, AUD, CAD,
HKD and several other markets quote their short-term floating-rate indices
on this basis — SONIA, TONA, the legacy GBP- and JPY-LIBOR fixings, BBSW
and CDOR all accrue on Act/365F. The procedure is identical to Act/360
except for the denominator.

### Structure

```text
fraction(start, end) = days_between(start, end) / 365.0

where days_between is signed, end-exclusive, start-inclusive.
```

The defining property: a full one-year non-leap-year span (365 calendar
days) returns exactly `1.0`; a leap-year span (366 days) returns
`366/365 ≈ 1.0027`.

### Worked example — 90-day interval

```text
start = 2026-01-01
end   = 2026-04-01
days  = 31 (Jan) + 28 (Feb, 2026 non-leap) + 31 (Mar) = 90
f     = 90 / 365 = 0.246575342465753
```

Asserted in `tests/integration.rs::golden::act_365f_90_days` to within
`TOL = 1e-12`.

### Governing standard

ISDA 2006 Definitions §4.16(d), *Actual/365 (Fixed)*.

---

## ActAct ISDA — ISDA 2006 §4.16(b) — `src/day_count/act_act_isda.rs`

Actual/Actual (ISDA) is the leap-year-aware day-count fraction used by
every standard ISDA-documented interest-rate swap whose floating leg
references an "Actual/Actual" basis. Unlike Act/360 and Act/365F — both
of which use a single, fixed denominator — Act/Act ISDA splits the
calculation period at every January 1 it crosses and divides each
sub-period's actual day count by 366 if its containing calendar year is a
Gregorian leap year and 365 otherwise. The sub-fractions are summed. The
construction is the one specified by the standard verbatim and is the one
printed in the ISDA 2006 Definitions appendix.

### Structure

```text
if start.year == end.year:
    d     = days_between(start, end)
    denom = 366 if is_leap_year(start.year) else 365
    return d / denom

else (multi-year span):
    sum = 0.0
    for y in start.year ..= end.year:
        sub_start = max(start, Date(y,     1, 1))
        sub_end   = min(end,   Date(y + 1, 1, 1))
        d_y       = days_between(sub_start, sub_end)
        if d_y > 0:
            denom = 366 if is_leap_year(y) else 365
            sum  += d_y / denom
    return sum
```

The half-open clamp `[Date(y, 1, 1), Date(y + 1, 1, 1))` per year is the
shape the standard prescribes and matches the crate-wide end-exclusive
convention. The same-year case is fast-pathed.

### Worked example — ISDA 2006 §4.16(b) printed example

```text
start = 2007-12-28
end   = 2008-02-29

Days in 2007: 2007-12-28 → 2008-01-01 (excl.) = 4 days
              (28, 29, 30, 31 December). 2007 non-leap → /365.
Days in 2008: 2008-01-01 → 2008-02-29 (excl.) = 31 + 28 = 59 days.
              2008 leap → /366.

f = 4/365 + 59/366
  = 0.010958904109589 + 0.161202185792350
  = 0.172161089901939
```

Asserted in `tests/integration.rs::golden::isda_act_act_2007_to_2008` to
`(f - 0.172_161_089_901_939).abs() < 1e-12`, matching the printed ISDA
2006 Definitions worked example exactly. Cross-verified against QuantLib
v1.34 `test-suite/daycounters.cpp:145` (1999-02-01 → 1999-07-01 =
0.410958904110), `:157` (1999-07-01 → 2000-07-01 = 1.001377348600), and
four further QuantLib vectors covering the same-year-leap and
multi-year-span paths.

### Governing standard

ISDA 2006 Definitions §4.16(b), *Actual/Actual* / *Actual/Actual (ISDA)*
/ *Act/Act* / *Act/Act (ISDA)*.

---

## ActAct ICMA — ICMA Rule 251 — `src/day_count/act_act_icma.rs`

Actual/Actual (ICMA) is the bond-market day-count convention prescribed by
the International Capital Market Association in Rule 251 and used to
compute accrued interest on fixed-rate bonds the world over. Unlike the
money-market fractions, the denominator is not a constant: it is the
actual length, in calendar days, of the *reference* coupon period
multiplied by the coupon frequency. A semi-annual bond whose calculation
period coincides with one full coupon period therefore always returns
exactly `0.5`, regardless of whether that period happens to be 181 or 184
days long — the property the bond market wants from a "fair fraction of a
coupon".

The two-argument dispatcher `day_count::fraction` cannot compute this
variant because it lacks the reference period and frequency; it returns
`f64::NAN`. Callers route through `day_count::year_fraction_with_freq` (or
call `act_act_icma::fraction` directly) with the five-argument signature.

### Structure

```text
fraction(start, end, ref_start, ref_end, freq) =
    days_between(start, end) / (freq * days_between(ref_start, ref_end))
```

`freq` is the integer coupon frequency: 1 (annual), 2 (semi-annual), 4
(quarterly), 12 (monthly). The implementation covers the **regular-period**
case — the calculation period sits inside (or coincides with) one
reference coupon period. Irregular (long / short) first or last coupons
require the caller to pre-decompose into regular sub-periods and sum.

Defensive behaviour: `freq == 0` or `ref_start == ref_end` returns `0.0`
rather than NaN — both are caller-side precondition violations and the
chosen sentinel avoids propagating an undefined value through downstream
cashflow arithmetic.

### Worked example — full semi-annual period

```text
start     = 2026-03-01     end     = 2026-09-01
ref_start = 2026-03-01     ref_end = 2026-09-01
freq      = 2 (semi-annual)

days(calc) = 31 + 30 + 31 + 30 + 31 + 31 = 184
days(ref)  = 184
f          = 184 / (2 * 184) = 0.5
```

Asserted in `tests/integration.rs::cross_oracle::
quantlib_act_act_icma_full_semiannual_period_is_half` to `1e-12`. The same
test file also pins the annual full-period (= 1.0) and the 1999-02-01 →
1999-07-01 vector (= 0.410958904110) against the same QuantLib reference.

### Governing standard

International Capital Market Association, *ICMA Rule 251 — Accrued
Interest Calculation*.

---

## 30/360 BondBasis — ISDA 2006 §4.16(f) — `src/day_count/thirty_360_bond_basis.rs`

30/360 Bond Basis is the US bond-market default: every month is treated as
30 days long and every year as 360 days, with two specific day-of-month
adjustments applied to the endpoints before differencing. The convention
underpins coupon accrual on US Treasury notes and bonds, US agency debt,
and most US corporate fixed-rate bonds; it is also the historic basis for
the swap-leg "30/360" tag in pre-ISDA confirmations.

### Structure

With `(Y1, M1, D1)` from `start` and `(Y2, M2, D2)` from `end`:

```text
1. If D1 == 31, set D1 = 30.
2. If D2 == 31 AND D1 in {30, 31}, set D2 = 30.
   (After step 1 has run, this is equivalent to "D2 == 31 AND D1 == 30".)
3. numerator = 360 * (Y2 - Y1) + 30 * (M2 - M1) + (D2 - D1)
4. f         = numerator / 360.0
```

The conditionality of step 2 on `D1` is the single rule that distinguishes
30/360 Bond Basis from 30E/360 (§4.16(g)) — Bond Basis suppresses the `D2`
collapse when `D1 ∉ {30, 31}`; 30E/360 collapses unconditionally.

### Worked example — `D2 = 31` with `D1 = 15`, no adjustment

```text
start = 2026-01-15, end = 2026-07-31
D1 = 15 (no change). D2 = 31 but D1 = 15 ∉ {30, 31} → no adjust.
numerator = 360 * 0 + 30 * (7-1) + (31-15) = 180 + 16 = 196
f         = 196 / 360
```

Asserted in `tests/integration.rs::golden::thirty_e_360_vs_bond_basis_d2_31`
to `1e-12`. The same test pairs the BondBasis result against the 30E/360
value on the same input (195/360) so the 1/360 gap between the two
conventions is auditable in a single place. Cross-verified against
QuantLib v1.34 `test-suite/daycounters.cpp:514` (2006-08-31 → 2007-02-28 =
178/360), `:515` (2007-02-28 → 2007-08-31 = 183/360), `:534` (2008-02-29 →
2009-02-28 = 359/360), and three other QuantLib vectors.

### Governing standard

ISDA 2006 Definitions §4.16(f), *30/360* (also known as "Bond Basis").

---

## 30E/360 — ISDA 2006 §4.16(g) — `src/day_count/thirty_e_360.rs`

30E/360 — the "Eurobond Basis" — is the day-count fraction the European
corporate-bond market uses on its fixed legs. It is a thirty-day-month
convention: every month is treated as having exactly 30 days and every
year as having exactly 360, with two end-of-month adjustments applied to
the raw `(year, month, day)` triples before the arithmetic. The result is
a clean, semi-annual coupon period that always lands on `0.5` regardless
of the actual day count.

### Structure

```text
1. If D1 == 31, set D1 = 30.
2. If D2 == 31, set D2 = 30.        (UNCONDITIONAL on D1)
3. numerator = 360 * (Y2 - Y1) + 30 * (M2 - M1) + (D2 - D1)
4. f         = numerator / 360.0
```

Step 2 fires whenever `D2 == 31`, regardless of `D1` — the single
divergence from 30/360 Bond Basis. The two conventions agree on every
input whose `end.day` is not 31; they disagree by `1/360` on the
`start.day ∉ {30, 31}, end.day == 31` case.

### Worked example — `D1 = 15, D2 = 31` collapses

```text
start = 2026-01-15, end = 2026-07-31
D1 = 15 (no change). D2 = 31 → D2 = 30 unconditionally.
numerator = 360 * 0 + 30 * (7-1) + (30-15) = 180 + 15 = 195
f         = 195 / 360
```

Asserted in `tests/integration.rs::golden::thirty_e_360_vs_bond_basis_d2_31`.
Cross-verified against QuantLib v1.34 `test-suite/daycounters.cpp:557`
(2006-02-28 → 2006-08-31 = 182/360), `:561` (2008-02-29 → 2008-08-31 =
181/360, where the BondBasis answer would be 182/360), `:585` (2008-02-28
→ 2008-03-31 = 32/360), and three other QuantLib vectors.

### Governing standard

ISDA 2006 Definitions §4.16(g), *30E/360* (also marketed as the "Eurobond
Basis").

---

## 30E/360 ISDA — ISDA 2006 §4.16(h) — `src/day_count/thirty_e_360_isda.rs`

30E/360 ISDA is the maturity-day-aware sibling of 30E/360. The
30-day-month / 360-day-year arithmetic is identical to that of the plain
30E/360 fraction (§4.16(g)), but the day-of-month adjustments are governed
by a different rule: a day is collapsed to 30 when it is the **last
calendar day of its month** — not merely when it is the 31st. So a 28
February in a non-leap year, a 29 February in a leap year, and a 30 April
all qualify, where the plain 30E/360 fraction adjusts only the four
31-day-month endings.

On top of that, the convention carries one deliberate carve-out: when the
end date is the **maturity date** of the instrument *and* the end month
is February, the end-day adjustment is **suppressed** — `D2` is left at
the actual last day of February (28 or 29). This is the load-bearing case
of the convention; it is the entire reason §4.16(h) exists alongside
§4.16(g). Mishandling it silently over- or under-accrues the final coupon
of every February-maturing instrument that quotes on 30E/360 ISDA — a
1/360 to 2/360 absolute drift in the year fraction, well above any
oracle-matching tolerance.

The signature carries an extra `end_is_maturity` flag the two-argument
dispatcher cannot infer; `day_count::fraction` defaults it to `false`, so
the maturity case must call `thirty_e_360_isda::fraction` directly.

### Structure

```text
1. If D1 == days_in_month(Y1, M1), set D1 = 30.
2. If D2 == days_in_month(Y2, M2)
       AND NOT (M2 == 2 AND end_is_maturity),
   set D2 = 30.
3. numerator = 360 * (Y2 - Y1) + 30 * (M2 - M1) + (D2 - D1)
4. f         = numerator / 360.0
```

`days_in_month` returns 28/29/30/31 for any valid `(year, month)`, so the
"last day" test is exact (and leap-year aware in February).

### Worked example 1 — not at maturity, end-day collapses

```text
start = 2007-08-31, end = 2008-02-29, end_is_maturity = false
D1 = 31 = days_in_month(2007, 8) → D1 = 30
D2 = 29 = days_in_month(2008, 2), M2 = 2 but NOT maturity → D2 = 30
numerator = 360 + 30 * (2 - 8) + (30 - 30) = 360 - 180 = 180
f         = 180 / 360 = 0.5
```

Asserted in `tests/integration.rs::cross_oracle::
quantlib_eurobond_isda_2007_08_31_to_2008_02_29_not_maturity` to `1e-12`,
matching QuantLib v1.34 `test-suite/daycounters.cpp:622`.

### Worked example 2 — at maturity, end-day suppressed

```text
start = 2011-08-31, end = 2012-02-29, end_is_maturity = true
D1 = 31 = days_in_month(2011, 8) → D1 = 30
D2 = 29 = days_in_month(2012, 2), M2 = 2 AND maturity → SUPPRESS, D2 stays 29
numerator = 360 + 30 * (2 - 8) + (29 - 30) = 360 - 180 - 1 = 179
f         = 179 / 360
```

Asserted in `tests/integration.rs::cross_oracle::
quantlib_eurobond_isda_2011_08_31_to_2012_02_29_at_maturity`, matching
QuantLib v1.34 `test-suite/daycounters.cpp:630`. The 1/360 gap between the
two worked examples (180 vs 179) is exactly the swing the February
suppression introduces.

### Governing standard

ISDA 2006 Definitions §4.16(h), *30E/360 (ISDA)*.

---

## Act/365L — ICMA — `src/day_count/act_365l.rs`

Act/365L (Actual/365 Leap-year-sensitive) is the "leap-aware" cousin of
Act/365F: the numerator is the same signed count of actual calendar days
separating the two dates, but the denominator switches to 366 whenever a
29 February falls inside the period and stays at 365 otherwise. The
convention is used for sterling floating-rate notes whose coupon basis
must reflect the presence of a leap day inside the accrual window.

### Structure

```text
numerator   = days_between(start, end)                   // signed
denominator = 366 if any 29 February d satisfies start <= d < end
                  (or end <= d < start for inverted intervals)
              else 365
f           = numerator / denominator
```

The leap-day inclusion test is **start-inclusive, end-exclusive** —
`start <= ymd(y, 2, 29) < end` — exactly matching the half-open
convention `days_between` uses for the numerator. So a period anchored on
a leap-day boundary picks up that 29 February only when it sits on the
`start` edge, never when it sits on the `end` edge. For an inverted
interval the test is symmetric (`end <= ymd(y, 2, 29) < start`), keeping
`fraction(end, start) == -fraction(start, end)` exact.

### Worked example — straddling 2024-02-29

```text
start = 2023-12-01, end = 2024-06-01
days  = 31 (Dec) + 31 (Jan) + 29 (Feb 2024) + 31 (Mar) + 30 (Apr)
      + 31 (May) = 183
2024-02-29 ∈ [2023-12-01, 2024-06-01) → denominator = 366
f     = 183 / 366 = 0.5
```

Asserted in `tests/integration.rs::cross_oracle::
hand_act_365l_straddle_leap_day_2024`. The same module also pins
`hand_act_365l_full_non_leap_year` (= 1.0 for a 365-day non-leap year)
and `hand_act_365l_q3_2024_after_leap` (= 92/365 for an interval that
does *not* contain 2024-02-29, exercising the half-open inclusion).

### Governing standard

ICMA Rule 251 and the sterling money-market convention literature on the
*Actual/365L* leap-aware day count.

---

## NL/365 — conventional — `src/day_count/nl_365.rs`

NL/365 — "No-Leap / 365" — is a money-market variant of Act/365F that
excludes 29 February from the numerator: the fraction's numerator is the
actual count of calendar days between `start` and `end`, *minus* the
number of 29 February dates that fall in the half-open interval `[start,
end)`; the denominator is the fixed constant 365. The effect is that a
full leap-year span and a full non-leap-year span both return exactly
`1.0` — the convention is "leap-year-blind" by construction, in the same
way Act/360 is leap-year-blind by virtue of a constant 360 denominator,
but here every year contributes exactly `365/365` to the fraction.

The convention has no entry in the ISDA 2006 Definitions §4.16 catalogue;
it is a market-practice convention found principally in some legacy
money-market and fixed-income contracts.

### Structure

```text
numerator   = days_between(start, end) - leap_days_in_interval(start, end)
denominator = 365
f           = numerator / 365.0

leap_days_in_interval(lo, hi) = count of ymd(y, 2, 29) for leap year y
                                satisfying lo <= ymd(y, 2, 29) < hi
```

For an inverted interval (`start > end`) the leap-day count is taken over
the forward half-open interval and the sign is carried by the signed
`days_between`, preserving `fraction(end, start) == -fraction(start,
end)`.

### Worked example — full leap year is exactly 1.0

```text
start = 2024-01-01, end = 2025-01-01
days  = 366 (2024 is a leap year)
2024-02-29 ∈ [start, end) → subtract 1
num   = 365
f     = 365 / 365 = 1.0
```

Asserted in `tests/integration.rs::cross_oracle::
hand_nl_365_full_leap_year_is_one`. The same module also pins
`hand_nl_365_eight_year_span_two_leaps` (2020-01-01 → 2028-01-01 =
`2920/365 = 8.0` exactly, covering the 2020 and 2024 leap days) and
`hand_nl_365_q1_non_leap` (90/365 for a non-leap-year first quarter).

### Governing standard

Market-convention day-count catalogues that list NL/365 alongside the
ISDA / ICMA fractions — the convention is non-statutory; no single
primary reference applies.

---

## Bus/252 — Brazilian convention — `src/day_count/bus_252.rs`

Bus/252 is the day-count fraction used for almost every BRL-denominated
fixed-income instrument: federal CDI / Selic-indexed notes, ANBIMA
reference yields, B3-listed DI futures, and the corporate debentures
priced off them. The numerator is the count of **business days** in the
half-open interval `[start, end)` under the caller-supplied calendar; the
denominator is the fixed constant 252 — the conventional Brazilian
business-year length.

Bus/252 cannot share the two-argument signature of the other fractions:
by definition it needs to know which dates are business days, and that
knowledge lives in `crate::calendar`. The signature therefore takes a
caller-supplied `is_business_day: impl Fn(Date) -> bool` predicate; the
two-argument dispatcher `day_count::fraction` returns `f64::NAN` for the
`Bus252` variant. In practice the caller either passes the closure
`|d| calendar::is_business_day(d, cal)` for their chosen `cal`, or uses
the convenience helper `calendar::bus_252_for_calendar(start, end, cal)`
that builds the closure for them.

### Structure

```text
For start <= end:
    count = number of dates d with start <= d < end and is_business_day(d)
    return count as f64 / 252.0
For start > end:
    return -fraction(end, start, is_business_day)
```

The walk is one day at a time, bounded defensively at `MAX_DAYS = 73_000`
steps (~200 years) so a pathological predicate cannot loop indefinitely.

### Worked example — two weeks under TARGET2

```text
start = 2026-05-01 (Fri), end = 2026-05-15 (Fri), calendar = TARGET2
Business days in [2026-05-01, 2026-05-15):
    May  1 (Fri) — TARGET2 Labour Day, NOT a business day
    May  4 (Mon), May  5 (Tue), May  6 (Wed), May  7 (Thu), May  8 (Fri)
    May 11 (Mon), May 12 (Tue), May 13 (Wed), May 14 (Thu)
= 9 business days
f = 9 / 252
```

Asserted in `tests/integration.rs::golden::bus_252_target2_two_weeks`. The
same input is also pinned end-to-end in
`tests/integration.rs::cross_module::bus_252_for_calendar_matches_direct_bus_252_call`,
which checks that the convenience helper and the direct
`bus_252::fraction` call produce bit-exact equal `f64` values.

### Governing standards

ANBIMA, *Caderno de Fórmulas — Títulos Públicos Federais* (Bus/252
numerator definition for LTN, NTN-B, NTN-F). B3, *Manual de Apreçamento
— Derivativos de Renda Fixa* (Bus/252 denominator = 252 for DI futures).

---

## 1/1 — OIS shortcut — `src/day_count/one_one.rs`

1/1 is the degenerate convention: every interval, regardless of length,
direction, or endpoint placement, maps to the year-fraction `1.0`. The
function ignores its two arguments entirely — they are accepted only so
the signature lines up with every other fraction in the dispatcher.

The convention exists as a placeholder inside the schedule of an
overnight-indexed-swap (OIS) contract. In an OIS, the floating coupon is
the geometric compound of the overnight rate over the accrual period; the
daily compounding does *all* the time-weighting work internally. Once
that compounded rate has been computed, multiplying it by the period's
year-fraction would double-count the time dimension. Setting the
day-count to 1/1 keeps the uniform cashflow formula
`payment = notional * rate * fraction(start, end)` with `fraction ≡ 1`
on the OIS leg.

### Structure

```text
fraction(_, _) = 1.0
```

This is the one place in the crate where `fraction(d, d) != 0.0` and where
`fraction(end, start) != -fraction(start, end)`. Both are deliberate.

### Worked example

```text
start = 2026-01-01, end = 2026-04-01 → 1.0
start = 2026-05-23, end = 2026-05-23 → 1.0  (zero-length)
start = 2026-04-01, end = 2026-01-01 → 1.0  (inverted)
```

Asserted bit-exactly (no tolerance) in `src/day_count/one_one.rs`'s unit
tests and via `tests/integration.rs::properties::one_one_is_one_on_zero_length`.

### Governing standard

ISDA OIS terminology notes (the "1/1" / "Act/Act" placeholder on
compounded-rate legs).

---

## Unadjusted date roll — `src/roll.rs`

`Roll::Unadjusted` is the no-op roll: it returns the input date as given,
even when the date is a weekend or holiday. It exists because some term
sheets quote an unadjusted accrual date alongside an adjusted payment
date, and the schedule generator needs to distinguish the two.

### Structure

```text
apply(date, Unadjusted, _) = date
```

The business-day predicate is never consulted.

### Worked example

```text
apply(2026-05-23, Unadjusted, weekends_only) = 2026-05-23 (Sat)
```

Asserted in `src/roll.rs`'s `unadjusted_returns_input` unit test.

### Governing standard

ISDA 2006 Definitions §4.12 (*Business Day Convention*) — the "Unadjusted"
option of the Business Day Convention.

---

## Following date roll — `src/roll.rs`

`Roll::Following` rolls a non-business date forward to the next business
day. It is the default settlement-date convention in most floating-rate
contracts.

### Structure

```text
apply(date, Following, is_biz):
    d = date
    while not is_biz(d):
        d = d.add_days(1)
        (bounded by MAX_STEPS = 366 one-day steps)
    return d
```

A defensive cap of 366 steps in either direction protects against a
pathological predicate. If the cap is reached `apply` silently returns
the input unchanged — the safest fallback for a function without a
`Result` return.

### Worked example — Saturday under a weekends-only calendar

```text
2026-05-23 (Sat)
  -> not a business day → step to 2026-05-24 (Sun)
  -> not a business day → step to 2026-05-25 (Mon)
  -> business day → return 2026-05-25
```

Asserted in `src/roll.rs::following_from_saturday`. The same pattern is
exercised end-to-end under TARGET2 in
`tests/integration.rs::cross_module::adjust_then_fraction_under_target2`
(2026-05-23 (Sat) → 2026-05-25 (Mon) under `Calendar::Target2`).

### Governing standard

ISDA 2006 Definitions §4.12 (*Business Day Convention*) — the "Following
Business Day Convention".

---

## ModifiedFollowing date roll — `src/roll.rs`

`Roll::ModifiedFollowing` rolls forward like `Following`, but if the
forward roll would cross into the next calendar month it falls back to
`Preceding` from the original date. It is the standard convention for
fixed-leg coupon dates on interest-rate swaps and for almost every
month-end-anchored fixing schedule.

### Structure

```text
apply(date, ModifiedFollowing, is_biz):
    f = walk_forward(date, is_biz)
    if f.month() == date.month():
        return f
    else:
        return walk_backward(date, is_biz)
```

### Worked example — Sunday at month-end

```text
2026-05-31 (Sun) under a weekends-only calendar
  Following → 2026-06-01 (Mon)        // crosses into June
  fall back → Preceding from 2026-05-31
              → 2026-05-30 (Sat) → 2026-05-29 (Fri) → return 2026-05-29
```

Asserted in `src/roll.rs::modified_following_falls_back_when_month_changes`.

### Governing standard

ISDA 2006 Definitions §4.12 (*Business Day Convention*) — the "Modified
Following Business Day Convention".

---

## Preceding date roll — `src/roll.rs`

`Roll::Preceding` rolls a non-business date backward to the previous
business day. It is the mirror of `Following` and is occasionally used
for fixing dates that must precede the accrual start.

### Structure

```text
apply(date, Preceding, is_biz):
    d = date
    while not is_biz(d):
        d = d.add_days(-1)
        (bounded by MAX_STEPS = 366 one-day steps)
    return d
```

### Worked example — Saturday under a weekends-only calendar

```text
2026-05-23 (Sat)
  -> not a business day → step to 2026-05-22 (Fri)
  -> business day → return 2026-05-22
```

Asserted in `src/roll.rs::preceding_from_saturday`.

### Governing standard

ISDA 2006 Definitions §4.12 (*Business Day Convention*) — the "Preceding
Business Day Convention".

---

## ModifiedPreceding date roll — `src/roll.rs`

`Roll::ModifiedPreceding` rolls backward like `Preceding`, but if the
backward roll would cross into the previous calendar month it falls back
to `Following` from the original date. It is the symmetric counterpart
to `ModifiedFollowing` and is used in jurisdictions whose conventions
anchor on the *start* of the month.

### Structure

```text
apply(date, ModifiedPreceding, is_biz):
    p = walk_backward(date, is_biz)
    if p.month() == date.month():
        return p
    else:
        return walk_forward(date, is_biz)
```

### Worked example — Sunday at month-start

```text
2026-02-01 (Sun) under a weekends-only calendar
  Preceding → 2026-01-30 (Fri)        // crosses into January
  fall back → Following from 2026-02-01
              → 2026-02-02 (Mon) → return 2026-02-02
```

Asserted in `src/roll.rs::modified_preceding_falls_back_when_month_changes`.

### Governing standard

ISDA 2006 Definitions §4.12 (*Business Day Convention*) — the "Modified
Preceding Business Day Convention".

---

## Nearest date roll — `src/roll.rs`

`Roll::Nearest` rolls a non-business date to the closer of the next and
previous business days; ties go to the forward direction. It is the
market convention used in some short-dated money-market instruments.

### Structure

```text
apply(date, Nearest, is_biz):
    if is_biz(date): return date
    fwd     = walk_forward(date, is_biz)
    bwd     = walk_backward(date, is_biz)
    fwd_d   = |date.days_between(fwd)|
    bwd_d   = |date.days_between(bwd)|
    return fwd if fwd_d <= bwd_d else bwd
```

The tie predicate `fwd_d <= bwd_d` breaks forward, as documented.

### Worked example — Saturday goes to Friday

```text
2026-05-23 (Sat) under a weekends-only calendar
  Friday  2026-05-22 = 1 day back
  Monday  2026-05-25 = 2 days forward
  → return 2026-05-22 (Fri)
```

Asserted in `src/roll.rs::nearest_on_saturday_goes_friday`.

### Governing standard

Market convention — the "Nearest Business Day Convention" is not in
ISDA §4.12 but is widely listed in money-market practitioner references
alongside the four ISDA conventions.

---

## EndOfMonth date roll — `src/roll.rs`

`Roll::EndOfMonth` anchors every monthly roll-forward that started on the
last business day of a month to the last business day of the new month it
lands on. In ISDA wording: "if the date is the last business day of its
month, every rolled coupon date is also the last business day of its
month".

### Structure

```text
apply(date, EndOfMonth, is_biz):
    if is_last_business_day_of_month(date, is_biz):
        return last_business_day_of_month(date, is_biz)
    else:
        return ModifiedFollowing(date, is_biz)
```

`is_last_business_day_of_month` walks forward from `date` to the end of
its calendar month and reports `true` iff no later business day exists.
`last_business_day_of_month` walks backward from the calendar last-day to
find the latest business day inside the month. When `date` is not itself
the last business day of its month, the roll falls through to
`ModifiedFollowing` so the day-of-month behaviour is unsurprising.

The intended usage is "EOM-aware schedule generation": pair this roll
with `Date::add_months_eom_aware` to roll a coupon period that started at
month-end forward by a fixed number of months and land again at the
month-end.

### Worked example — last Friday of a month under TARGET2

```text
2026-05-29 (Fri) is the last business day of May 2026 under TARGET2.
apply(2026-05-29, EndOfMonth, is_business_day(_, Target2)) = 2026-05-29.

2026-05-31 (Sun) is NOT the last business day of May (the predicate
returns false on Sunday). EndOfMonth falls through to ModifiedFollowing,
which lands back on the last business day Fri 2026-05-29.
```

Both branches are asserted in `src/roll.rs::end_of_month_*` and in
`tests/integration.rs::cross_module` (indirectly, via the TARGET2
adjustment helpers).

### Governing standard

ISDA 2006 Definitions §4.12 (*Business Day Convention*) — the "EOM"
qualifier on a Business Day Convention, paired with the "End of Month"
schedule-rolling rule.

---

## TARGET2 calendar — ECB fixed rule — `src/calendar/target2.rs`

TARGET2 — Trans-European Automated Real-time Gross settlement Express
Transfer system — is the euro-area large-value payment system, and its
closing-days schedule defines the "TARGET2 business day" used by every
EUR-denominated cashflow this crate processes. Unlike the six dated-
snapshot calendars catalogued below, TARGET2 is **fully rule-based**: the
European Central Bank fixes exactly six closing dates per year, derivable
from the year alone, and that schedule has not changed since TARGET2 went
live in 2008. No dated table is stored — the rule is exact for every year
in the supported `Date` range.

### Structure

```text
easter        = Date::easter_sunday(date.year())   // Meeus / Computus
good_friday   = easter.add_days(-2)
easter_monday = easter.add_days(+1)

is_holiday(date) =
    date == ymd(year,  1,  1)   ||   // New Year's Day        (fixed)
    date == good_friday          ||   // Easter − 2            (computed)
    date == easter_monday        ||   // Easter + 1            (computed)
    date == ymd(year,  5,  1)   ||   // Labour Day             (fixed)
    date == ymd(year, 12, 25)   ||   // Christmas Day          (fixed)
    date == ymd(year, 12, 26)        // Christmas Holiday      (fixed)
```

Whit Monday (Easter + 50 days) was a TARGET closing day pre-2002 and was
**removed** when TARGET2 superseded TARGET; it is *not* a TARGET2
holiday. The regression test `whit_monday_is_not_a_holiday` pins this.
Weekend dates are not classified as holidays here; the `is_business_day`
dispatcher composes the weekend test with this holiday test.

### Worked example — 2024

```text
Date::easter_sunday(2024) = 2024-03-31
  → Good Friday    = 2024-03-29
  → Easter Monday  = 2024-04-01

holidays(2024) = { 2024-01-01,   // New Year's Day (Mon)
                   2024-03-29,   // Good Friday
                   2024-04-01,   // Easter Monday
                   2024-05-01,   // Labour Day (Wed)
                   2024-12-25,   // Christmas Day (Wed)
                   2024-12-26 }  // Christmas Holiday (Thu)
```

Asserted in `tests/integration.rs::golden::target2_known_holidays` (the
four non-fixed entries: Good Friday, Easter Monday, Christmas Day, Boxing
Day) and in `src/calendar/target2.rs::holidays_2024` (all six).

### Governing standard

European Central Bank, *Decision of the European Central Bank on the
TARGET2 closing days* — the six fixed closing dates, unchanged since
TARGET2 went live on 19 November 2007 and replaced TARGET on 19 May 2008.
Easter is computed by Jean Meeus, *Astronomical Algorithms*, 2nd ed.,
Willmann-Bell, 1998, §8.

---

## Luxembourg calendar — Loi du 21 juillet 1928 — `src/calendar/luxembourg.rs`

Luxembourg's bank-holiday calendar is the schedule under which Luxembourg
banks, the Banque centrale du Luxembourg, and the BCEE close, and which
every Luxembourg-domiciled UCITS or SIF fund uses (typically composited
with TARGET2 via `JointBusiness`) to determine NAV-publication dates.
Like TARGET2 it is **fully rule-based** — every date is either a fixed
civil date or an Easter-derived date that `Date::easter_sunday` already
gives us, so no snapshot table is stored and the calendar is exact for
every year in the supported `Date` range.

The list differs from TARGET2 in five entries: Luxembourg observes
Whit Monday (TARGET2 dropped it in 2002), Ascension Day, Europe Day (from
2019), Luxembourg National Day, the Assumption of Mary, and All Saints'
Day; it does NOT observe Good Friday (which is a religious observance but
not a gazetted Luxembourg bank holiday). The Luxembourg-specific dates
add five extra closures per year relative to TARGET2 — the gap a UCITS /
SIF NAV calendar fills via composition.

### Structure

```text
Eleven holidays per year:

  1.  1 January       (New Year's Day)              fixed
  2.  Easter Monday   = Easter Sunday + 1 day       Easter-derived
  3.  1 May           (Labour Day)                  fixed
  4.  9 May           (Europe Day)                  fixed (from 2019 onwards)
  5.  Ascension Day   = Easter Sunday + 39 days     Easter-derived
  6.  Whit Monday     = Easter Sunday + 50 days     Easter-derived
  7. 23 June          (National Day)                fixed
  8. 15 August        (Assumption of Mary)          fixed
  9.  1 November      (All Saints' Day)             fixed
 10. 25 December      (Christmas Day)               fixed
 11. 26 December      (Boxing Day / St. Stephen)    fixed
```

No weekend-observance shift — like TARGET2, the function reports the
**rule date** in every year. A holiday that falls on a Sunday is reported
on the Sunday; the weekend filter is handled separately by
`is_business_day`.

### Worked example — 2026

```text
Easter Sunday 2026 = 5 April  (computed by Date::easter_sunday)

  2026-01-01  Thu   New Year's Day
  2026-04-06  Mon   Easter Monday   (Easter + 1)
  2026-05-01  Fri   Labour Day
  2026-05-09  Sat   Europe Day
  2026-05-14  Thu   Ascension Day   (Easter + 39)
  2026-05-25  Mon   Whit Monday     (Easter + 50)
  2026-06-23  Tue   National Day
  2026-08-15  Sat   Assumption
  2026-11-01  Sun   All Saints'
  2026-12-25  Fri   Christmas Day
  2026-12-26  Sat   Boxing Day
```

Asserted in `src/calendar/luxembourg.rs::tests::holidays_2026`.

### Europe Day (the 2019 cutover)

Europe Day (the anniversary of the Schuman Declaration, 9 May 1950) was
added to the Luxembourg public-holiday list by the **Loi du 28 février
2019 fixant les jours fériés légaux**, with first observance in 2019.
The implementation guards this exactly:

```text
europe_day = (date == ymd(year, 5, 9)) && year >= 2019
```

so `is_holiday(2018-05-09)` returns `false` and `is_holiday(2019-05-09)`
returns `true`. The cutover is asserted in
`src/calendar/luxembourg.rs::tests::europe_day_added_in_2019`.

### Composition with TARGET2

The canonical Luxembourg-fund NAV calendar is

```rust
use regit_daycount::{Calendar, calendar::JointBusiness};

static LU_FUND: &[Calendar] = &[Calendar::Target2, Calendar::Luxembourg];
let nav = JointBusiness::new(LU_FUND);
```

which closes on any date either TARGET2 or Luxembourg considers
non-business — the broadest correct schedule for a SICAV.

### Governing standards

Loi du 21 juillet 1928 sur le droit du travail (Luxembourg), as amended;
Loi du 28 février 2019 fixant les jours fériés légaux (the law that added
Europe Day). Easter is the Anonymous Gregorian / Meeus Computus shared
with TARGET2.

---

## United States calendar — NYSE / Federal Reserve — `src/calendar/united_states.rs`

The US calendar is an embedded, dated snapshot of the holidays observed
by the New York Stock Exchange and the US Federal Reserve. The NYSE
schedule is a superset of the Federal Reserve operating-day list (the
NYSE adds Good Friday); this module models the NYSE list, which is the
conservative — and standard — choice for US bond and equity settlement
dates.

### Structure

```text
Snapshot date: SNAPSHOT_DATE = "2026-05-23"
Coverage:      COVERAGE      = (2020, 2040) inclusive
Entries:       208 observed holiday dates (10 holidays × 21 years − 2
               edge gaps for Juneteenth before 2022)
```

The ten observed holidays follow these rules (the *observed* date for a
fixed-date holiday falling on a weekend is shifted per the published
schedule; the snapshot records the observed date, not the rule date):

```text
New Year's Day        Jan 1   (observed Fri ← Sat, Mon → Sun)
MLK Day               3rd Monday of January
Presidents' Day       3rd Monday of February
Good Friday           Easter Sunday − 2 days
Memorial Day          last Monday of May
Juneteenth (from 2022) Jun 19 (observed Mon → Sun, Fri ← Sat); NYSE since 2022
Independence Day      Jul 4   (observed Fri ← Sat, Mon → Sun)
Labor Day             1st Monday of September
Thanksgiving          4th Thursday of November
Christmas Day         Dec 25  (observed Fri ← Sat, Mon → Sun)
```

The 208-entry table is a sorted `&'static [(year, month, day)]` array in
the source file; it is not reproduced inline here. Lookups outside
`COVERAGE` return `false` — never a panic, never an extrapolation.

### Worked example

```text
is_holiday(2024-07-04, UnitedStates) = true   // Independence Day (Thu)
is_holiday(2024-11-28, UnitedStates) = true   // Thanksgiving Day (4th Thu)
```

Asserted in `tests/integration.rs::golden::us_known_holidays`. The
out-of-coverage behaviour is asserted in
`tests/integration.rs::invalid::out_of_coverage_calendar_returns_false_not_panic`
(`is_holiday(2019-12-25, UnitedStates) = false`).

### Governing standards

NYSE Holiday Calendar, the published trading-floor schedule. Federal
Reserve Bank Services holiday schedule. SEC / NYSE notices for
discretionary closures (hurricanes, state funerals).

---

## United Kingdom calendar — Bank of England — `src/calendar/united_kingdom.rs`

The UK calendar is an embedded, dated snapshot of the Bank of England's
bank-holiday schedule for England & Wales — the schedule that governs
sterling money-market and gilt settlement dates.

### Structure

```text
Snapshot date: SNAPSHOT_DATE = "2026-05-23"
Coverage:      COVERAGE      = (2020, 2040) inclusive
Entries:       171 entries (8 per year × 21 years − 1 displaced Spring
               Bank Holiday in 2022 + 2 extra Royal-proclamation days)
```

The eight base holidays per year:

```text
New Year's Day      Jan 1   (observed Mon → Sun)
Good Friday         Easter Sunday − 2 days
Easter Monday       Easter Sunday + 1 day
Early May Bank Hol. 1st Monday of May
Spring Bank Hol.    Last Monday of May (DISPLACED in 2022)
Summer Bank Hol.    Last Monday of August (England & Wales)
Christmas Day       Dec 25  (observed Mon → Sun, Tue → Sat)
Boxing Day          Dec 26  (observed Tue → Sun, Mon → Sat)
```

The snapshot also records the Royal proclamation specials — the 2022
Platinum Jubilee (Spring Bank moved to Thu 2 June, plus an extra Friday
3 June), the 2023 Coronation, and the 2022 funeral of Queen Elizabeth II
— which are statutory dated additions, not rule-based.

The 171-entry table lives in the source file; it is not reproduced inline.

### Worked example

```text
is_holiday(2022-06-02, UnitedKingdom) = true  // Platinum Jubilee Spring Bank
is_holiday(2022-06-03, UnitedKingdom) = true  // Platinum Jubilee extra
```

Asserted in `tests/integration.rs::golden::uk_platinum_jubilee_2022`. This
test pins the dated specials that no closed-form rule would generate.

### Governing standards

Bank of England, *UK bank holidays*. HM Government, *UK bank holidays*
(gov.uk). Banking and Financial Dealings Act 1971 (c. 80), Schedule 1.

---

## Japan calendar — JPX — `src/calendar/japan.rs`

The Japan calendar is an embedded, dated snapshot of the Japan Exchange
Group trading calendar for the Tokyo Stock Exchange — which composes the
16 statutory national holidays under the Act on National Holidays
(国民の祝日に関する法律, Law No. 178 of 1948) with the year-end / new-year
exchange closure (typically 2 January and 31 December).

### Structure

```text
Snapshot date: SNAPSHOT_DATE = "2026-05-23"
Coverage:      COVERAGE      = (2020, 2040) inclusive
```

Several Japanese holidays are date-shifted by the "happy Monday" law, and
the vernal and autumnal equinox holidays depend on the equinox tables
published by the National Astronomical Observatory; both make the
snapshot the only reliable form. Substitute holidays (振替休日) for
holidays that fall on a Sunday are also encoded.

The table lives in the source file; it is not reproduced inline.

### Worked example

```text
is_holiday(2024-01-01, Japan) = true   // Ganjitsu (New Year's Day)
```

Asserted in `src/calendar/mod.rs::dispatches_to_every_named_calendar` and
in the `src/calendar/japan.rs` per-year unit tests.

### Governing standards

*Act on National Holidays* (国民の祝日に関する法律), Law No. 178 of 1948.
Cabinet Office (Government of Japan), annual *koyomi yōkō*. Japan
Exchange Group, *Trading Calendar of the Tokyo Stock Exchange*.

---

## Switzerland calendar — SIX — `src/calendar/switzerland.rs`

The Switzerland calendar is an embedded, dated snapshot of the SIX Swiss
Exchange trading calendar — combining the Swiss federal holidays
(Neujahrstag, Bundesfeier on 1 August) with the cantonal holidays the
exchange observes (Berchtoldstag on 2 January, Auffahrt = Ascension Day
on Easter + 39, Pfingstmontag = Whit Monday on Easter + 50).

### Structure

```text
Snapshot date: SNAPSHOT_DATE = "2026-05-23"
Coverage:      COVERAGE      = (2020, 2040) inclusive
Entries:       210 (10 holidays × 21 years; no extraordinary closures)
```

The ten observed holidays per year are: Neujahrstag (Jan 1), Berchtoldstag
(Jan 2), Karfreitag (Easter − 2), Ostermontag (Easter + 1), Tag der Arbeit
(May 1), Auffahrt (Easter + 39), Pfingstmontag (Easter + 50), Bundesfeier
(Aug 1), Weihnachten (Dec 25), Stephanstag (Dec 26).

The table lives in the source file; it is not reproduced inline.

### Worked example

```text
is_holiday(2024-08-01, Switzerland) = true   // Bundesfeier
```

Asserted in `src/calendar/mod.rs::dispatches_to_every_named_calendar`.

### Governing standards

SIX Swiss Exchange, *Trading calendar*. Federal Constitution of the
Swiss Confederation, Art. 110 (1 August National Day).

---

## Hong Kong calendar — HKEX — `src/calendar/hong_kong.rs`

The Hong Kong calendar is an embedded, dated snapshot of the Hong Kong
Exchanges and Clearing trading calendar. The HKEX schedule combines the
Western fixed-date holidays (Western New Year, Christmas, Boxing Day)
with several **lunar / Chinese-calendar holidays** — Lunar New Year (3
days), Ching Ming Festival, Buddha's Birthday, Tuen Ng (Dragon Boat),
Mid-Autumn Festival, Chung Yeung Festival — none of which have a closed-
form Gregorian rule and which therefore force the snapshot form.

### Structure

```text
Snapshot date: SNAPSHOT_DATE = "2026-05-23"
Coverage:      COVERAGE      = (2020, 2040) inclusive
```

The table lives in the source file; it is not reproduced inline.

### Worked example

```text
is_holiday(2024-12-25, HongKong) = true   // Christmas (Western fixed)
```

Asserted in `src/calendar/mod.rs::dispatches_to_every_named_calendar`.

### Governing standards

Hong Kong Exchanges and Clearing Limited, *HKEX Trading Calendar*. Hong
Kong Government Gazette, *General Holidays Ordinance* (Cap. 149).

---

## Singapore calendar — SGX — `src/calendar/singapore.rs`

The Singapore calendar is an embedded, dated snapshot of the Singapore
Exchange trading calendar. The SGX schedule combines the Western fixed-
date holidays with the multi-faith Singaporean public holidays — Chinese
New Year (lunar), Vesak Day (Buddhist), Hari Raya Puasa and Hari Raya
Haji (Islamic), Deepavali (Hindu / lunar) — and applies the §4 Holidays
Act 1998 Monday-in-lieu rule for fixed-date holidays falling on a Sunday.

### Structure

```text
Snapshot date: SNAPSHOT_DATE = "2026-05-23"
Coverage:      COVERAGE      = (2020, 2040) inclusive
```

The table lives in the source file; it is not reproduced inline.

### Worked example

```text
is_holiday(2024-08-09, Singapore) = true   // National Day
```

Asserted in `src/calendar/mod.rs::dispatches_to_every_named_calendar`.

### Governing standards

Singapore Ministry of Manpower, *Public Holidays in Singapore*. Singapore
Exchange (SGX), *SGX Securities and Derivatives Trading Calendar*.
Holidays Act 1998 (Singapore), §4 (Monday-in-lieu rule).

---

## Calendar dispatchers and business-day primitives — `src/calendar/mod.rs`

The `calendar` module exposes a small, uniform set of free functions
that fan out over the named [`Calendar`] enum. Every function is `Copy`-
by-value over `Date` + `Calendar`, allocation-free, and `no_std`-clean.

### Classification

```text
is_weekend(date)             -> bool   // true iff Sat / Sun
is_holiday(date, cal)        -> bool   // true iff date is a holiday under cal
                                       //   (does NOT include weekends)
is_business_day(date, cal)   -> bool   // !is_weekend(date) && !is_holiday(date, cal)
```

### Rolling and walking

```text
adjust(date, conv, cal)           -> Date  // roll::apply(date, conv,
                                           //             |d| is_business_day(d, cal))
next_business_day(date, cal)      -> Date  // adjust(date, Roll::Following, cal)
previous_business_day(date, cal)  -> Date  // adjust(date, Roll::Preceding, cal)
add_business_days(date, n, cal)   -> Date  // T+N settlement primitive
business_days_between(start, end, cal) -> u32   // half-open [start, end)
bus_252_for_calendar(start, end, cal)  -> f64   // Bus/252 closed over cal
```

### `add_business_days(date, n, cal)`

The settlement-date primitive. Walks one calendar day at a time,
counting only business days (under `cal`) against `n`. Weekends and
holidays are skipped without consuming a count.

- `n > 0`  — walk forward; T+N settlement on T = `date`.
- `n < 0`  — walk backward; the "n business days before" form.
- `n == 0` — return `date` unchanged if it is itself a business day;
  otherwise walk forward to the next business day (the market convention
  for T+0 on a non-business start).

```text
add_business_days(Mon 2026-05-25, 2, Target2)   = Wed 2026-05-27
add_business_days(Thu 2026-05-28, 2, Target2)   = Mon 2026-06-01   // across weekend
add_business_days(Thu 2026-05-28, -2, Target2)  = Tue 2026-05-26
```

The walk is bounded at roughly 200 years of calendar walking — a
defensive cap that no real settlement convention approaches. If the
cap is reached, the partial result is returned.

### `business_days_between(start, end, cal)`

Counts business days in `[start, end)` (start-inclusive, end-exclusive
— the same half-open convention used everywhere in this crate). For
`start >= end` the count is `0`; the function does not return signed
counts. Walk is one calendar day at a time, defensively bounded at
~200 years.

```text
business_days_between(2026-05-01, 2026-05-15, Target2) = 9
  // May 1 is Labour Day; business days are 4, 5, 6, 7, 8, 11, 12, 13, 14
```

### Worked example — TARGET2 fortnight

Asserted in `src/calendar/mod.rs::tests::business_days_between_two_weeks_with_target2_labour_day`
and the matching `bus_252_for_calendar_matches_manual_count` test.

### Governing standards

ISDA 2006 Definitions §4.12 (business-day conventions) for the roll
fall-through to [`crate::roll::apply`]. The T+N settlement form is
market convention; the half-open `[start, end)` interval is consistent
with the day-count fractions in §3.

---

## Composite / JointBusiness combinators — `src/calendar/composite.rs`

A real-world cashflow often touches more than one calendar at once. A
cross-currency swap settles a leg in each currency on the other leg's
holiday: the joint settlement date is the next day on which both legs'
calendars are open. A Luxembourg-domiciled UCITS fund computes NAV on a
day that is both a TARGET2 business day and a Luxembourg bank-holiday
business day. The two combinators in this module name those operations.

- **`Composite`** takes the **union of holidays**: a date is a holiday on
  the composite if it is a holiday on *any* underlying calendar.
  Equivalent to "broadest possible non-business set across the group".
- **`JointBusiness`** takes the **intersection of business days**: a
  date is a business day iff *every* underlying calendar treats it as
  one. Equivalent to "narrowest possible business set across the group".

The two operations are semantic duals (`Composite::is_holiday` is the
union of holidays; `JointBusiness::is_business_day` is the intersection of
business days, which is the same set once weekends are accounted for);
the distinction is one of ergonomic framing, not of semantics. Both
structs hold a `&'static [Calendar]`, are `Copy`, allocation-free, and
`no_std`-clean.

### Structure

```text
Composite { kinds: &'static [Calendar] }
    is_holiday(d)      = any(is_holiday(d, k) for k in kinds)
    is_business_day(d) = all(is_business_day(d, k) for k in kinds)

JointBusiness { kinds: &'static [Calendar] }
    is_business_day(d) = all(is_business_day(d, k) for k in kinds)
    is_holiday(d)      = !is_business_day(d) && !is_weekend(d)
```

`is_business_day` on both types is the intersection — it is `false` if
*any* underlying calendar would close on `d`, whether through a weekend or
a named holiday.

### Worked example — EUR/USD joint business calendar, 4 July 2024

```text
JointBusiness { kinds: &[Target2, UnitedStates] }

2024-07-04 (Thu)
  is_holiday(_, Target2)       = false    // not a TARGET2 holiday
  is_business_day(_, Target2)  = true     // Thursday, not a holiday → open
  is_holiday(_, UnitedStates)  = true     // Independence Day
  is_business_day(_, UnitedStates) = false

is_business_day(2024-07-04) = all([true, false]) = false
is_holiday(2024-07-04)      = true                    // composite holiday
```

Asserted in `tests/integration.rs::golden::joint_us_eur_4_july_not_business`
and in `tests/integration.rs::cross_module::composite_then_business_days_between`
(which walks the full week 2024-07-01 → 2024-07-08 under
`Composite([Target2, UnitedStates])` and counts 4 business days,
correctly dropping Thu 4 July as the only US-only holiday in that
window).

### Governing standards

The combinators have no single governing standard; the union-of-holidays
and intersection-of-business-days framings are the multi-calendar
practitioner conventions that real-world cross-currency, multi-leg and
joint-jurisdiction settlement schedules use.

---

## Standards index

| Standard / source | Section | File |
|---|---|---|
| ISO 8601 (calendar date, proleptic Gregorian) | §2 | `src/date.rs` |
| Howard Hinnant, *chrono-Compatible Low-Level Date Algorithms* | §2 | `src/date.rs` |
| Jean Meeus, *Astronomical Algorithms*, 2nd ed., §8 (Computus) | §2, §21, §25 | `src/date.rs`, `src/calendar/target2.rs`, `src/calendar/switzerland.rs` |
| ISDA 2006 Definitions §4.16(e), *Actual/360* | §3 | `src/day_count/act_360.rs` |
| ISDA 2006 Definitions §4.16(d), *Actual/365 (Fixed)* | §4 | `src/day_count/act_365f.rs` |
| ISDA 2006 Definitions §4.16(b), *Actual/Actual (ISDA)* | §5 | `src/day_count/act_act_isda.rs` |
| ICMA Rule 251, *Accrued Interest Calculation* | §6, §10 | `src/day_count/act_act_icma.rs`, `src/day_count/act_365l.rs` |
| ISDA 2006 Definitions §4.16(f), *30/360* (Bond Basis) | §7 | `src/day_count/thirty_360_bond_basis.rs` |
| ISDA 2006 Definitions §4.16(g), *30E/360* (Eurobond Basis) | §8 | `src/day_count/thirty_e_360.rs` |
| ISDA 2006 Definitions §4.16(h), *30E/360 (ISDA)* | §9 | `src/day_count/thirty_e_360_isda.rs` |
| ANBIMA, *Caderno de Fórmulas — Títulos Públicos Federais* | §12 | `src/day_count/bus_252.rs` |
| B3, *Manual de Apreçamento — Derivativos de Renda Fixa* | §12 | `src/day_count/bus_252.rs` |
| ISDA OIS terminology notes (1/1 placeholder) | §13 | `src/day_count/one_one.rs` |
| ISDA 2006 Definitions §4.12, *Business Day Convention* | §14–§20 | `src/roll.rs` |
| ECB *Decision on the TARGET2 closing days* | §21 | `src/calendar/target2.rs` |
| NYSE Holiday Calendar | §22 | `src/calendar/united_states.rs` |
| Federal Reserve Bank Services holiday schedule | §22 | `src/calendar/united_states.rs` |
| Bank of England, *UK bank holidays* | §23 | `src/calendar/united_kingdom.rs` |
| Banking and Financial Dealings Act 1971 (c. 80), Schedule 1 | §23 | `src/calendar/united_kingdom.rs` |
| *Act on National Holidays* (Japan), Law No. 178 of 1948 | §24 | `src/calendar/japan.rs` |
| Japan Exchange Group, *Trading Calendar of the Tokyo Stock Exchange* | §24 | `src/calendar/japan.rs` |
| SIX Swiss Exchange, *Trading calendar* | §25 | `src/calendar/switzerland.rs` |
| Hong Kong Exchanges and Clearing, *HKEX Trading Calendar* | §26 | `src/calendar/hong_kong.rs` |
| Hong Kong *General Holidays Ordinance* (Cap. 149) | §26 | `src/calendar/hong_kong.rs` |
| Singapore Exchange (SGX), *Trading Calendar* | §27 | `src/calendar/singapore.rs` |
| Singapore Ministry of Manpower, *Public Holidays in Singapore* | §27 | `src/calendar/singapore.rs` |
| Singapore *Holidays Act 1998*, §4 (Monday-in-lieu rule) | §27 | `src/calendar/singapore.rs` |
| QuantLib v1.34, `test-suite/daycounters.cpp` (cross-oracle reference) | §5, §6, §7, §8, §9 | `tests/integration.rs` |

---

*Part of [Regit OS](https://www.regit.io) — the operating system for
investment products. From Luxembourg.*