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
// Copyright 2026 Regit.io — Nicolas Koenig
// SPDX-License-Identifier: Apache-2.0
//! 30E/360 ISDA — ISDA 2006 §4.16(h).
//!
//! 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.
//!
//! # Signature
//!
//! Unlike every other fraction in this catalogue, the entry point takes
//! an additional flag, `end_is_maturity`, because the convention is not
//! a pure function of `(start, end)`. The flag is the caller's
//! responsibility — it is `true` if and only if `end` is the maturity
//! date of the instrument the fraction is being computed for.
//!
//! # Algorithm
//!
//! With `(Y1, M1, D1)` from `start` and `(Y2, M2, D2)` from `end`:
//!
//! ```text
//! 1. If D1 is the last day of month M1 in year Y1, set D1 = 30.
//! (i.e. D1 == days_in_month(Y1, M1))
//! 2. If D2 is the last day of month M2 in year Y2,
//! 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
//! ```
//!
//! All arithmetic is performed in `i32` and converted to `f64` for the
//! final division; the intermediate values are bounded by the supported
//! year range and cannot overflow.
//!
//! # Worked example — maturity-day suppression
//!
//! ```text
//! start = 2024-01-31, end = 2025-02-28, end_is_maturity = true
//!
//! D1 = 31 = days_in_month(2024, 1) → D1 = 30
//! D2 = 28 = days_in_month(2025, 2)
//! AND M2 == 2 AND end_is_maturity → SUPPRESS, D2 stays 28
//!
//! Numerator = 360 * (2025 - 2024) + 30 * (2 - 1) + (28 - 30)
//! = 360 + 30 - 2
//! = 388
//! f = 388 / 360 = 1.077777777777778
//! ```
//!
//! Re-computing the same dates with `end_is_maturity = false` instead
//! takes `D2` to 30 and yields `390 / 360 = 1.083333333333333` — the
//! 2/360 swing the maturity-day suppression is designed to introduce.
//!
//! # Inverted intervals
//!
//! `fraction(end, start, _)` for `start < end` produces a negative
//! result by the natural arithmetic of the formula above — the
//! `(Y2 - Y1)`, `(M2 - M1)`, and `(D2 - D1)` differences all flip sign
//! together. No special case is needed in the body; the crate as a whole
//! does not reject inverted intervals (some callers compute
//! reverse-period accruals) and this module mirrors that behaviour.
//!
//! # References
//!
//! - ISDA 2006 Definitions §4.16(h), *30E/360 (ISDA)*.
use crateDate;
/// Computes the 30E/360 ISDA year fraction between two dates.
///
/// `end_is_maturity` is `true` if and only if `end` is the maturity
/// date of the instrument the fraction is being computed for; it
/// governs the February-suppression carve-out described in the
/// module-level docstring.
///
/// See the module-level docstring for the full algorithm and worked
/// examples.
///
/// # Examples
///
/// The same `(start, end)` pair yields two different fractions
/// depending on the maturity flag — the entire reason this convention
/// is distinct from 30E/360:
///
/// ```
/// use regit_daycount::Date;
/// use regit_daycount::day_count::thirty_e_360_isda;
///
/// let start = Date::ymd(2024, 1, 31).unwrap();
/// let end = Date::ymd(2025, 2, 28).unwrap();
///
/// // At maturity: D2 == 28 is the last day of February AND end is the
/// // maturity date AND M2 == 2, so the end-day adjustment is
/// // suppressed; D2 stays 28. Numerator = 360 + 30 - 2 = 388.
/// let at_maturity = thirty_e_360_isda::fraction(start, end, true);
/// assert!((at_maturity - 388.0_f64 / 360.0).abs() < 1e-12);
///
/// // Not at maturity: D2 == 28 is still the last day of February but
/// // the suppression carve-out does not apply, so D2 is taken to 30.
/// // Numerator = 360 + 30 + 0 = 390.
/// let mid_period = thirty_e_360_isda::fraction(start, end, false);
/// assert!((mid_period - 390.0_f64 / 360.0).abs() < 1e-12);
///
/// // The two differ by exactly 2/360 — the load-bearing swing.
/// assert!((mid_period - at_maturity - 2.0_f64 / 360.0).abs() < 1e-12);
/// ```