From ecc0e0fd654c806613b6fdef36768b0aede05a1f Mon Sep 17 00:00:00 2001 From: John Dowd Date: Sun, 1 Sep 2024 07:23:28 -0400 Subject: [PATCH 1/3] Fix incorrect calculation V1E18MF{some_date} means "add 17 months and go to the last day of that month" We had been calculating "add 18 months and go to the last day of that month" --- lib/sof/cycles/end_of.rb | 2 +- spec/sof/cycles/end_of_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/sof/cycles/end_of.rb b/lib/sof/cycles/end_of.rb index fbb03ef..3a80777 100644 --- a/lib/sof/cycles/end_of.rb +++ b/lib/sof/cycles/end_of.rb @@ -17,7 +17,7 @@ def to_s end 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 diff --git a/spec/sof/cycles/end_of_spec.rb b/spec/sof/cycles/end_of_spec.rb index 3840a7f..11e289a 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.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 @@ -38,7 +38,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)}" @@ -52,7 +52,7 @@ 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 it "given an anchor date, returns dates that fall within it's window" do From a09df124bdbccbdab3ae24ed001e5482c3aadcd2 Mon Sep 17 00:00:00 2001 From: John Dowd Date: Sun, 1 Sep 2024 06:52:41 -0400 Subject: [PATCH 2/3] Fix behavior of Cycles::EndOf EndOf#expiration_of(...) always returns #final_date --- lib/sof/cycles/end_of.rb | 34 +++++++++++++ spec/sof/cycles/end_of_spec.rb | 87 +++++++++++++++++----------------- 2 files changed, 78 insertions(+), 43 deletions(-) diff --git a/lib/sof/cycles/end_of.rb b/lib/sof/cycles/end_of.rb index 3a80777..8a7b263 100644 --- a/lib/sof/cycles/end_of.rb +++ b/lib/sof/cycles/end_of.rb @@ -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 @@ -16,6 +24,32 @@ 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) + # # => # + 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 + # # => # def final_date(_ = nil) = time_span .end_date(start_date - 1.send(period)) .end_of_month diff --git a/spec/sof/cycles/end_of_spec.rb b/spec/sof/cycles/end_of_spec.rb index 11e289a..24bcbda 100644 --- a/spec/sof/cycles/end_of_spec.rb +++ b/spec/sof/cycles/end_of_spec.rb @@ -13,21 +13,7 @@ module SOF 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] @@ -55,6 +41,23 @@ module SOF 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, @@ -65,56 +68,54 @@ module SOF end end - describe "#satisfied_by?(completed_dates, anchor:)" do - context "when the completions--judged from the --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 From f5e1fb6881c48d8ab349e26ccaa0a6bb5985a2c5 Mon Sep 17 00:00:00 2001 From: John Dowd Date: Sun, 1 Sep 2024 09:13:40 -0400 Subject: [PATCH 3/3] Update Changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 885bfce..291f318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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