diff --git a/CHANGELOG.md b/CHANGELOG.md index 035b942..b13e77d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dormant capability is now declared on each cycle class (`def self.dormant_capable? = true`) instead of only in `Parser.dormant_capable_kinds` +### Fixed + +- EndOf cycle `final_date` was off by one period — `V1E12M` now correctly expires at the end of the 12th month, not the 11th + ## [0.1.12] - 2025-09-05 ### Added diff --git a/lib/sof/cycles/end_of.rb b/lib/sof/cycles/end_of.rb index 310f257..8cd64fb 100644 --- a/lib/sof/cycles/end_of.rb +++ b/lib/sof/cycles/end_of.rb @@ -2,7 +2,7 @@ # Captures the logic for enforcing the EndOf cycle variant # E.g. "V1E18MF2020-01-05" means: -# You're good until the end of the 17th subsequent month from 2020-01-05. +# You're good until the end of the 18th month from 2020-01-05. # Complete 1 by that date to reset the cycle. # # Some of the calculations are quite different from other cycles. @@ -64,15 +64,15 @@ def satisfied_by?(_ = nil, anchor: Date.current) # # @param [nil] _ Unused parameter, maintained for compatibility # @return [Date] The final date of the cycle calculated as the end of the - # nth subsequent period after the FROM date, where n = (period count - 1) + # nth period after the FROM date # # @example # Cycle.for("V1E18MF2020-01-09").final_date - # # => # + # # => # def final_date(_ = nil) return nil if parser.dormant? || from_date.nil? time_span - .end_date(start_date - 1.send(period)) + .end_date(start_date) .end_of_month end @@ -81,14 +81,11 @@ def start_date(_ = nil) = from_date&.to_date private def dormant_to_s - <<~DESC.squish - #{volume}x by the last day of the #{subsequent_ordinal} - subsequent #{period} - DESC + "#{volume}x by the last day of the #{ordinalized_period_count} #{period}" end - def subsequent_ordinal - ActiveSupport::Inflector.ordinalize(period_count - 1) + def ordinalized_period_count + ActiveSupport::Inflector.ordinalize(period_count) end end end diff --git a/spec/sof/cycles/dormant_spec.rb b/spec/sof/cycles/dormant_spec.rb index 180d09a..e3ecbb9 100644 --- a/spec/sof/cycles/dormant_spec.rb +++ b/spec/sof/cycles/dormant_spec.rb @@ -58,7 +58,7 @@ module SOF context "with a dormant EndOf cycle" do it "returns the cycle string representation with (dormant) suffix" do - expect(end_of_cycle.to_s).to eq "2x by the last day of the 17th subsequent month (dormant)" + expect(end_of_cycle.to_s).to eq "2x by the last day of the 18th month (dormant)" end end end diff --git a/spec/sof/cycles/end_of_spec.rb b/spec/sof/cycles/end_of_spec.rb index 7007fe6..5e65843 100644 --- a/spec/sof/cycles/end_of_spec.rb +++ b/spec/sof/cycles/end_of_spec.rb @@ -10,7 +10,7 @@ module SOF let(:notation) { "V2E18MF#{from_date}" } let(:anchor) { nil } - let(:end_date) { (from_date + 17.months).end_of_month } + let(:end_date) { (from_date + 18.months).end_of_month } let(:from_date) { "2020-01-01".to_date } let(:completed_dates) { [] } @@ -24,7 +24,7 @@ module SOF end end - @end_date = ("2020-01-01".to_date + 17.months).end_of_month + @end_date = ("2020-01-01".to_date + 18.months).end_of_month it_behaves_like "#to_s returns", "2x by #{@end_date.to_fs(:american)}" @@ -32,13 +32,13 @@ module SOF before { allow(cycle.parser).to receive(:dormant?).and_return(true) } it_behaves_like "#to_s returns", - "2x by the last day of the 17th subsequent month" + "2x by the last day of the 18th month" end it_behaves_like "#volume returns the volume" it_behaves_like "#notation returns the notation" it_behaves_like "#as_json returns the notation" it_behaves_like "it computes #final_date(given)", - given: nil, returns: ("2020-01-01".to_date + 17.months).end_of_month + given: nil, returns: ("2020-01-01".to_date + 18.months).end_of_month it_behaves_like "it cannot be extended" describe "#last_completed" do @@ -66,7 +66,7 @@ module SOF too_late_date ] end - let(:recent_date) { from_date + 17.months } + let(:recent_date) { from_date + 18.months } let(:middle_date) { from_date + 2.months } let(:early_date) { from_date + 1.month } let(:too_early_date) { from_date - 1.day } @@ -85,7 +85,7 @@ module SOF describe "#satisfied_by?(anchor:)" do context "when the anchor date is < the final date" do - let(:anchor) { "2021-06-29".to_date } + let(:anchor) { "2021-07-30".to_date } it "returns true" do expect(cycle).to be_satisfied_by(anchor:) @@ -93,7 +93,7 @@ module SOF end context "when the anchor date is = the final date" do - let(:anchor) { "2021-06-30".to_date } + let(:anchor) { "2021-07-31".to_date } it "returns true" do expect(cycle).to be_satisfied_by(anchor:) @@ -101,7 +101,7 @@ module SOF end context "when the anchor date is > the final date" do - let(:anchor) { "2021-07-01".to_date } + let(:anchor) { "2021-08-01".to_date } it "returns false" do expect(cycle).not_to be_satisfied_by(completed_dates, anchor:) @@ -111,26 +111,26 @@ module SOF describe "#expiration_of(completion_dates)" do context "when the anchor date is < the final date" do - let(:anchor) { "2021-06-29".to_date } + let(:anchor) { "2021-07-30".to_date } it "returns the final date" do - expect(cycle.expiration_of(anchor:)).to eq "2021-06-30".to_date + expect(cycle.expiration_of(anchor:)).to eq "2021-07-31".to_date end end context "when the anchor date = the final date" do - let(:anchor) { "2021-06-30".to_date } + let(:anchor) { "2021-07-31".to_date } it "returns the final date" do - expect(cycle.expiration_of(anchor:)).to eq "2021-06-30".to_date + expect(cycle.expiration_of(anchor:)).to eq "2021-07-31".to_date end end context "when the anchor date > the final date" do - let(:anchor) { "2021-07-31".to_date } + let(:anchor) { "2021-08-31".to_date } it "returns the final date" do - expect(cycle.expiration_of(anchor:)).to eq "2021-06-30".to_date + expect(cycle.expiration_of(anchor:)).to eq "2021-07-31".to_date end end end