Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 7 additions & 10 deletions lib/sof/cycles/end_of.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
# # => #<Date: 2021-06-30>
# # => #<Date: 2021-07-31>
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

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion spec/sof/cycles/dormant_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 14 additions & 14 deletions spec/sof/cycles/end_of_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) { [] }
Expand All @@ -24,21 +24,21 @@ 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)}"

context "when the cycle is dormant" do
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
Expand Down Expand Up @@ -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 }
Expand All @@ -85,23 +85,23 @@ 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:)
end
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:)
end
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:)
Expand All @@ -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
Expand Down
Loading