Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Cycles::EndOf #15

Merged
merged 3 commits into from
Sep 1, 2024
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 @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.1.3] - Unreleased

### Fixed

- `Cycles::EndOf` to have the correct behavior

## [0.1.2] - 2024-08-09

### Added
Expand Down
36 changes: 35 additions & 1 deletion lib/sof/cycles/end_of.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# frozen_string_literal: true

# 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.
# Complete 1 by that date to reset the cycle.
#
# Some of the calculations are quite different from other cycles.
# Whereas other cycles look at completion dates to determine if the cycle is
# satisfied, this cycle checks whether the anchor date is prior to the final date.
module SOF
module Cycles
class EndOf < Cycle
Expand All @@ -16,8 +24,34 @@ def to_s
"#{volume}x by #{final_date.to_fs(:american)}"
end

# Returns the expiration date for the cycle
#
# @param [nil] _ Unused parameter, maintained for compatibility
# @param anchor [nil] _ Unused parameter, maintained for compatibility
# @return [Date] The final date of the cycle
#
# @example
# Cycle.for("V1E18MF2020-01-09")
# .expiration_of(anchor: "2020-06-04".to_date)
# # => #<Date: 2021-06-30>
def expiration_of(_ = nil, anchor: nil) = final_date

# Is the supplied anchor date prior to the final date?
#
# @return [Boolean] true if the cycle is satisfied, false otherwise
def satisfied_by?(_ = nil, anchor: Date.current) = anchor <= final_date

# Calculates the final date of the cycle
#
# @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)
#
# @example
# Cycle.for("V1E18MF2020-01-09").final_date
# # => #<Date: 2021-06-30>
def final_date(_ = nil) = time_span
.end_date(start_date)
.end_date(start_date - 1.send(period))
.end_of_month

def start_date(_ = nil) = from_date.to_date
Expand Down
93 changes: 47 additions & 46 deletions spec/sof/cycles/end_of_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,10 @@ module SOF
let(:notation) { "V2E18MF#{from_date}" }
let(:anchor) { nil }

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

let(:completed_dates) do
[
recent_date,
middle_date,
early_date,
early_date,
too_early_date,
too_late_date
]
end
let(:recent_date) { from_date.to_date + 17.months }
let(:middle_date) { from_date.to_date + 2.months }
let(:early_date) { from_date.to_date + 1.month }
let(:too_early_date) { from_date.to_date - 1.day }
let(:too_late_date) { end_date + 1.day }
let(:completed_dates) { [] }

it_behaves_like "#kind returns", :end_of
it_behaves_like "#valid_periods are", %w[W M Q Y]
Expand All @@ -38,7 +24,7 @@ module SOF
end
end

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

Expand All @@ -52,9 +38,26 @@ module SOF
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 + 18.months).end_of_month
given: nil, returns: ("2020-01-01".to_date + 17.months).end_of_month

describe "#covered_dates" do
let(:completed_dates) do
[
recent_date,
middle_date,
early_date,
early_date,
too_early_date,
too_late_date
]
end
let(:recent_date) { from_date.to_date + 17.months }
let(:middle_date) { from_date.to_date + 2.months }
let(:early_date) { from_date.to_date + 1.month }
let(:too_early_date) { from_date.to_date - 1.day }
let(:too_late_date) { end_date + 1.day }

let(:anchor) { "2021-06-29".to_date }
it "given an anchor date, returns dates that fall within it's window" do
expect(cycle.covered_dates(completed_dates, anchor:)).to eq([
recent_date,
Expand All @@ -65,56 +68,54 @@ module SOF
end
end

describe "#satisfied_by?(completed_dates, anchor:)" do
context "when the completions--judged from the <from_date>--satisfy the cycle" do
describe "#satisfied_by?(anchor:)" do
context "when the anchor date is < the final date" do
let(:anchor) { "2021-06-29".to_date }

it "returns true" do
expect(cycle).to be_satisfied_by(completed_dates, anchor:)
expect(cycle).to be_satisfied_by(anchor:)
end
end

context "when the completions are irrelevant to the given from_date" do
let(:completed_dates) do
[
10.years.ago,
Date.current,
10.years.from_now
]
end
context "when the anchor date is = the final date" do
let(:anchor) { "2021-06-30".to_date }

it "returns false" do
expect(cycle).not_to be_satisfied_by(completed_dates, anchor:)
it "returns true" do
expect(cycle).to be_satisfied_by(anchor:)
end
end

context "when the completions currently do not satisfy the cycle" do
let(:notation) { "V5E18M" }
context "when the anchor date is > the final date" do
let(:anchor) { "2021-07-01".to_date }

it "returns false" do
expect(cycle).not_to be_satisfied_by(completed_dates, anchor:)
end
end
end

context "when there are no completions" do
let(:completed_dates) { [] }
describe "#expiration_of(completion_dates)" do
context "when the anchor date is < the final date" do
let(:anchor) { "2021-06-29".to_date }

it "returns false" do
expect(cycle).not_to be_satisfied_by(completed_dates, anchor:)
it "returns the final date" do
expect(cycle.expiration_of(anchor:)).to eq "2021-06-30".to_date
end
end
end

describe "#expiration_of(completion_dates)" do
context "when the completions currently satisfy the cycle" do
it "returns the date on which the completions will no longer satisfy the cycle" do
expect(cycle.expiration_of(completed_dates)).to be nil
context "when the anchor date = the final date" do
let(:anchor) { "2021-06-30".to_date }

it "returns the final date" do
expect(cycle.expiration_of(anchor:)).to eq "2021-06-30".to_date
end
end

context "when the completions currently do not satisfy the cycle" do
let(:notation) { "V5E18M" }
context "when the anchor date > the final date" do
let(:anchor) { "2021-07-31".to_date }

it "returns nil" do
expect(cycle.expiration_of(completed_dates)).to be_nil
it "returns the final date" do
expect(cycle.expiration_of(anchor:)).to eq "2021-06-30".to_date
end
end
end
Expand Down