Skip to content

Commit 0bc3a40

Browse files
committed
Fix EndOf cycle final_date off-by-one
EndOf#final_date subtracted 1 period before adding period_count, causing V1E12M to expire at end of month 11 instead of month 12. Remove the offset so the notation matches user expectations: V1E12M from Dec 1 now correctly expires Dec 31 (not Nov 30). Fixed: EndOf cycle expiration was 1 month early (QUAL-6189)
1 parent 38917eb commit 0bc3a40

4 files changed

Lines changed: 24 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [0.1.13] - Unreleased
99

10+
### Fixed
11+
12+
- EndOf cycle `final_date` was off by one period — `V1E12M` now correctly expires at the end of the 12th month, not the 11th
13+
1014
## [0.1.12] - 2025-09-05
1115

1216
### Added

lib/sof/cycles/end_of.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Captures the logic for enforcing the EndOf cycle variant
44
# E.g. "V1E18MF2020-01-05" means:
5-
# You're good until the end of the 17th subsequent month from 2020-01-05.
5+
# You're good until the end of the 18th month from 2020-01-05.
66
# Complete 1 by that date to reset the cycle.
77
#
88
# Some of the calculations are quite different from other cycles.
@@ -62,15 +62,15 @@ def satisfied_by?(_ = nil, anchor: Date.current)
6262
#
6363
# @param [nil] _ Unused parameter, maintained for compatibility
6464
# @return [Date] The final date of the cycle calculated as the end of the
65-
# nth subsequent period after the FROM date, where n = (period count - 1)
65+
# nth period after the FROM date
6666
#
6767
# @example
6868
# Cycle.for("V1E18MF2020-01-09").final_date
69-
# # => #<Date: 2021-06-30>
69+
# # => #<Date: 2021-07-31>
7070
def final_date(_ = nil)
7171
return nil if parser.dormant? || from_date.nil?
7272
time_span
73-
.end_date(start_date - 1.send(period))
73+
.end_date(start_date)
7474
.end_of_month
7575
end
7676

@@ -86,7 +86,7 @@ def dormant_to_s
8686
end
8787

8888
def subsequent_ordinal
89-
ActiveSupport::Inflector.ordinalize(period_count - 1)
89+
ActiveSupport::Inflector.ordinalize(period_count)
9090
end
9191
end
9292
end

spec/sof/cycles/dormant_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ module SOF
5858

5959
context "with a dormant EndOf cycle" do
6060
it "returns the cycle string representation with (dormant) suffix" do
61-
expect(end_of_cycle.to_s).to eq "2x by the last day of the 17th subsequent month (dormant)"
61+
expect(end_of_cycle.to_s).to eq "2x by the last day of the 18th subsequent month (dormant)"
6262
end
6363
end
6464
end

spec/sof/cycles/end_of_spec.rb

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module SOF
1010
let(:notation) { "V2E18MF#{from_date}" }
1111
let(:anchor) { nil }
1212

13-
let(:end_date) { (from_date + 17.months).end_of_month }
13+
let(:end_date) { (from_date + 18.months).end_of_month }
1414
let(:from_date) { "2020-01-01".to_date }
1515

1616
let(:completed_dates) { [] }
@@ -24,21 +24,21 @@ module SOF
2424
end
2525
end
2626

27-
@end_date = ("2020-01-01".to_date + 17.months).end_of_month
27+
@end_date = ("2020-01-01".to_date + 18.months).end_of_month
2828
it_behaves_like "#to_s returns",
2929
"2x by #{@end_date.to_fs(:american)}"
3030

3131
context "when the cycle is dormant" do
3232
before { allow(cycle.parser).to receive(:dormant?).and_return(true) }
3333

3434
it_behaves_like "#to_s returns",
35-
"2x by the last day of the 17th subsequent month"
35+
"2x by the last day of the 18th subsequent month"
3636
end
3737
it_behaves_like "#volume returns the volume"
3838
it_behaves_like "#notation returns the notation"
3939
it_behaves_like "#as_json returns the notation"
4040
it_behaves_like "it computes #final_date(given)",
41-
given: nil, returns: ("2020-01-01".to_date + 17.months).end_of_month
41+
given: nil, returns: ("2020-01-01".to_date + 18.months).end_of_month
4242
it_behaves_like "it cannot be extended"
4343

4444
describe "#last_completed" do
@@ -66,7 +66,7 @@ module SOF
6666
too_late_date
6767
]
6868
end
69-
let(:recent_date) { from_date + 17.months }
69+
let(:recent_date) { from_date + 18.months }
7070
let(:middle_date) { from_date + 2.months }
7171
let(:early_date) { from_date + 1.month }
7272
let(:too_early_date) { from_date - 1.day }
@@ -85,23 +85,23 @@ module SOF
8585

8686
describe "#satisfied_by?(anchor:)" do
8787
context "when the anchor date is < the final date" do
88-
let(:anchor) { "2021-06-29".to_date }
88+
let(:anchor) { "2021-07-30".to_date }
8989

9090
it "returns true" do
9191
expect(cycle).to be_satisfied_by(anchor:)
9292
end
9393
end
9494

9595
context "when the anchor date is = the final date" do
96-
let(:anchor) { "2021-06-30".to_date }
96+
let(:anchor) { "2021-07-31".to_date }
9797

9898
it "returns true" do
9999
expect(cycle).to be_satisfied_by(anchor:)
100100
end
101101
end
102102

103103
context "when the anchor date is > the final date" do
104-
let(:anchor) { "2021-07-01".to_date }
104+
let(:anchor) { "2021-08-01".to_date }
105105

106106
it "returns false" do
107107
expect(cycle).not_to be_satisfied_by(completed_dates, anchor:)
@@ -111,26 +111,26 @@ module SOF
111111

112112
describe "#expiration_of(completion_dates)" do
113113
context "when the anchor date is < the final date" do
114-
let(:anchor) { "2021-06-29".to_date }
114+
let(:anchor) { "2021-07-30".to_date }
115115

116116
it "returns the final date" do
117-
expect(cycle.expiration_of(anchor:)).to eq "2021-06-30".to_date
117+
expect(cycle.expiration_of(anchor:)).to eq "2021-07-31".to_date
118118
end
119119
end
120120

121121
context "when the anchor date = the final date" do
122-
let(:anchor) { "2021-06-30".to_date }
122+
let(:anchor) { "2021-07-31".to_date }
123123

124124
it "returns the final date" do
125-
expect(cycle.expiration_of(anchor:)).to eq "2021-06-30".to_date
125+
expect(cycle.expiration_of(anchor:)).to eq "2021-07-31".to_date
126126
end
127127
end
128128

129129
context "when the anchor date > the final date" do
130-
let(:anchor) { "2021-07-31".to_date }
130+
let(:anchor) { "2021-08-31".to_date }
131131

132132
it "returns the final date" do
133-
expect(cycle.expiration_of(anchor:)).to eq "2021-06-30".to_date
133+
expect(cycle.expiration_of(anchor:)).to eq "2021-07-31".to_date
134134
end
135135
end
136136
end

0 commit comments

Comments
 (0)