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
// Copyright 2026 Regit.io — Nicolas Koenig
// SPDX-License-Identifier: Apache-2.0
//! Act/Act (ICMA) — ICMA Rule 251.
//!
//! Act/Act (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 (Act/360, Act/365F) 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".
//!
//! This module implements the **regular-period** case only: the calculation
//! period sits inside (or coincides with) one reference coupon period. The
//! standard further prescribes a split-into-regular-sub-periods procedure
//! for irregular (long / short) first or last coupons; that variant is
//! deliberately out of scope here and will be added in a future revision as
//! a separate `act_act_icma_irregular` function. Callers handling an
//! irregular calculation period must either pre-decompose or wait for that
//! addition.
//!
//! # Algorithm
//!
//! ```text
//! fraction(start, end, ref_start, ref_end, freq)
//! = days_between(start, end) / (freq * days_between(ref_start, ref_end))
//! ```
//!
//! Where `days_between` is the signed count of days from the first argument
//! to the second, end-exclusive, start-inclusive (the convention shared by
//! every fraction in this crate). `freq` is the integer coupon frequency:
//! 1 (annual), 2 (semi-annual), 4 (quarterly), 12 (monthly).
//!
//! # Defensive behaviour
//!
//! If the caller passes `freq == 0` or a degenerate reference period for
//! which `days_between(ref_start, ref_end) == 0`, the function returns
//! `0.0`. Both are precondition violations by the caller — a coupon
//! frequency of zero is not a thing, and a zero-length reference period
//! cannot be a coupon period — but returning `0.0` is preferable to
//! emitting a silent `NaN` that would propagate through downstream cashflow
//! arithmetic undetected.
//!
//! # Worked example — full semi-annual period
//!
//! ```text
//! start = 2026-03-01
//! end = 2026-09-01
//! ref_start = 2026-03-01 (calculation period coincides with the
//! ref_end = 2026-09-01 reference coupon period)
//! freq = 2 (semi-annual)
//! days(calc) = 31 + 30 + 31 + 30 + 31 + 31 = 184
//! days(ref) = 184 (same interval)
//! f = 184 / (2 * 184) = 0.500000000000000
//! ```
//!
//! # References
//!
//! - International Capital Market Association, *ICMA Rule 251 — Accrued
//! Interest Calculation*.
use crateDate;
/// Computes the Act/Act (ICMA) year fraction for a regular calculation
/// period.
///
/// The formula is `days_between(start, end) / (freq *
/// days_between(ref_start, ref_end))`, where the reference period is the
/// one coupon period that contains (or coincides with) the calculation
/// period. See the module-level docstring for the full algorithm, the
/// regular-period precondition, and the defensive behaviour for
/// degenerate inputs.
///
/// # Arguments
///
/// - `start`, `end`: the calculation period.
/// - `ref_start`, `ref_end`: the reference coupon period that contains
/// `[start, end)`.
/// - `freq`: coupon frequency — `1` (annual), `2` (semi-annual), `4`
/// (quarterly), `12` (monthly).
///
/// Returns `0.0` defensively if `freq == 0` or if `ref_start == ref_end`
/// (a degenerate reference period). Both are caller-side precondition
/// violations; the alternative is a silent `NaN` propagating through
/// downstream arithmetic.
///
/// # Examples
///
/// ```
/// use regit_daycount::Date;
/// use regit_daycount::day_count::act_act_icma;
///
/// // Semi-annual bond, calculation period coincides with the reference
/// // coupon period: f = 184 / (2 * 184) = 0.5 exactly.
/// let start = Date::ymd(2026, 3, 1).unwrap();
/// let end = Date::ymd(2026, 9, 1).unwrap();
/// let ref_start = start;
/// let ref_end = end;
/// let f = act_act_icma::fraction(start, end, ref_start, ref_end, 2);
/// assert!((f - 0.5).abs() < 1e-12);
/// ```