Skip to content

Commit c791536

Browse files
fix: ignore normal preferred day assignment for month-end rolling when series starts on Feb 28th or 29th
1 parent 16ddeaf commit c791536

File tree

2 files changed

+82
-1
lines changed

2 files changed

+82
-1
lines changed

lib/src/series.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,13 @@ sealed class Series {
173173
};
174174

175175
if (monthlyOrHigher && hasMonthEndDay(normalized)) {
176-
preferredDay = 31; // rollMonth will cap to last day of target month
176+
// Treat last day in Feb as normal month day
177+
final isFeb = normalized.month == DateTime.february;
178+
final daysInFeb = (isFeb && isLeapYear(normalized.year)) ? 29 : 28;
179+
180+
// rollMonth will cap to last day of target month, or
181+
// 28th/29th in the case of Feb start date
182+
preferredDay = isFeb ? daysInFeb : 31;
177183
}
178184

179185
final List<DateTime> dates = [normalized];

test/calculator_test.dart

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,79 @@ post_date label amount capital intere
148148
() => schedule.prettyPrint(convention: convention), prints(expected));
149149
});
150150
});
151+
152+
group('Calculator ad-hoc calculations', () {
153+
test('solve payment using a different interest frequency, leap day start', () async {
154+
// This test is to check correct handling of month rolling
155+
// where a series starts on the last day of Feb - see series.dart
156+
// lines 175-183
157+
final calculator = Calculator()
158+
..add(SeriesAdvance(
159+
numberOf: 1,
160+
label: 'Loan',
161+
amount: 10000,
162+
postDateFrom: DateTime.utc(2024, 1, 31),
163+
))
164+
..add(SeriesPayment(
165+
numberOf: 36,
166+
label: 'Instalment',
167+
amount: null,
168+
isInterestCapitalised: false,
169+
postDateFrom: DateTime.utc(2024, 2, 29)//leap year
170+
))
171+
..add(SeriesPayment(
172+
numberOf: 12,
173+
label: 'Interest',
174+
amount: 0.0,
175+
isInterestCapitalised: true,
176+
frequency: Frequency.quarterly,
177+
postDateFrom: DateTime.utc(2024, 4, 29)
178+
));
179+
final convention = US30U360();
180+
final startDate = DateTime.utc(2026, 1, 31);
181+
182+
final pmt = await calculator.solveValue(
183+
convention: convention, interestRate: 0.1, startDate: startDate);
184+
final irr = await calculator.solveRate(
185+
convention: convention, startDate: startDate);
186+
expect(pmt, closeTo(322.27, 0.01));
187+
expect(irr, closeTo(0.09999676, 1e-8));
188+
});
189+
test('solve payment using a different interest frequency, non-leap day start', () async {
190+
// This test is to check correct handling of month rolling
191+
// where a series starts on the last day of Feb - see series.dart
192+
// lines 175-183
193+
final calculator = Calculator()
194+
..add(SeriesAdvance(
195+
numberOf: 1,
196+
label: 'Loan',
197+
amount: 10000,
198+
postDateFrom: DateTime.utc(2026, 1, 31),
199+
))
200+
..add(SeriesPayment(
201+
numberOf: 36,
202+
label: 'Instalment',
203+
amount: null,
204+
isInterestCapitalised: false,
205+
postDateFrom: DateTime.utc(2026, 2, 28)
206+
))
207+
..add(SeriesPayment(
208+
numberOf: 12,
209+
label: 'Interest',
210+
amount: 0.0,
211+
isInterestCapitalised: true,
212+
frequency: Frequency.quarterly,
213+
postDateFrom: DateTime.utc(2026, 4, 28)
214+
));
215+
final convention = US30U360();
216+
final startDate = DateTime.utc(2026, 1, 31);
217+
218+
final pmt = await calculator.solveValue(
219+
convention: convention, interestRate: 0.1, startDate: startDate);
220+
final irr = await calculator.solveRate(
221+
convention: convention, startDate: startDate);
222+
expect(pmt, closeTo(322.27, 0.01));
223+
expect(irr, closeTo(0.09999676, 1e-8));
224+
});
225+
});
151226
}

0 commit comments

Comments
 (0)