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
// Copyright 2026 Regit.io — Nicolas Koenig
// SPDX-License-Identifier: Apache-2.0
//! Bus/252 — Brazilian business-day convention (ANBIMA / B3).
//!
//! 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 (roughly 365 calendar days minus 104 weekend days
//! minus the federal / state holidays B3 observes).
//!
//! # API shape — closure predicate
//!
//! Every other fraction in this crate has the signature `fn fraction(start:
//! Date, end: Date) -> f64`. Bus/252 cannot — by definition it needs to know
//! which dates are business days, and that knowledge lives in
//! `crate::calendar`. Importing `calendar` here would close the cycle
//! `day_count::bus_252 → calendar → day_count` (calendars compose with
//! year-fraction queries through the dispatcher), so the signature instead
//! takes a caller-supplied `is_business_day: impl Fn(Date) -> bool`
//! predicate. In practice the caller passes `|d| calendar::is_business_day
//! (d, cal)` for their chosen `cal`; `bus_252` itself just counts.
//!
//! This is the same pattern [`crate::roll::apply`] uses for the same
//! reason — see `roll.rs` for the canonical statement.
//!
//! # Algorithm
//!
//! ```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` steps
//! — roughly two centuries — to keep a pathological interval from looping
//! indefinitely; no real fixed-income instrument crosses that horizon.
//!
//! # Worked example
//!
//! ```text
//! start = 2026-05-01 (Fri), end = 2026-05-15 (Fri), weekends-only calendar
//! business days in [2026-05-01, 2026-05-15):
//! May 1 Fri, 4 Mon, 5 Tue, 6 Wed, 7 Thu, 8 Fri,
//! 11 Mon, 12 Tue, 13 Wed, 14 Thu = 10 days
//! f = 10 / 252 = 0.039682539682540
//! ```
//!
//! # References
//!
//! - 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).
use crateDate;
/// Maximum number of one-day steps the counter walks before bailing out.
///
/// Sized for ~200 years (`365 * 200 = 73_000`), which covers every term
/// structure of any real BRL instrument by orders of magnitude. If the cap
/// is ever reached — a pathological interval far outside any practical use
/// — the running count at that point is returned, divided by 252 as usual;
/// the function never panics and never loops indefinitely.
const MAX_DAYS: u32 = 73_000;
/// Computes the Bus/252 year fraction between two dates.
///
/// The numerator is the count of `d` with `start <= d < end` (or
/// `end <= d < start` for an inverted interval, with the sign flipped)
/// satisfying `is_business_day(d)`; the denominator is the constant 252.
/// See the module-level docstring for the full algorithm, the rationale for
/// the closure-predicate signature, and the worked example.
///
/// # Examples
///
/// ```
/// use regit_daycount::{Date, Weekday};
/// use regit_daycount::day_count::bus_252;
///
/// // Weekends-only calendar: Sat and Sun are not business days.
/// let is_biz = |d: Date| {
/// let wd = d.day_of_week();
/// wd != Weekday::Sat && wd != Weekday::Sun
/// };
///
/// // The worked example: 2026-05-01 (Fri) → 2026-05-15 (Fri).
/// // Business days in [2026-05-01, 2026-05-15) are May 1, 4, 5, 6, 7, 8,
/// // 11, 12, 13, 14 — ten in total.
/// let start = Date::ymd(2026, 5, 1).unwrap();
/// let end = Date::ymd(2026, 5, 15).unwrap();
/// let f = bus_252::fraction(start, end, is_biz);
/// assert!((f - 10.0 / 252.0).abs() < 1e-12);
/// ```