sof-cycle 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98a3b894aa388fbe5e272203915060486d219d54872ee3f54bbbc9aed3c343d7
4
- data.tar.gz: 4023edaf4b2c668680d4fb7147f920b7dff727ade6bc02a64c5d928dabc5bbd5
3
+ metadata.gz: 0eb9458907426d68d8fc05c159a9bb782819b2ec821baf2ec24109b45c6794ff
4
+ data.tar.gz: e4c0b55074d59aad3f4e562e579759d8d85413b1fd93c93c873b79bdbff1c629
5
5
  SHA512:
6
- metadata.gz: 32ed188de45051c75b5e9fc41f26f59ccba11e2432eb7714601eed2baa581a70b00136cd7b7e8c5bac50d5966dca221540b92e570c6ac06aa7bbfe9a1be72123
7
- data.tar.gz: e2b243c6387a5a79b7c1dfda6c7a2efb31f692f5196924abf7351bcfbf9ba87e7416e5528533f9a7ab29c7200c709e9b2d426a00db8222c570621bd16b040210
6
+ metadata.gz: c799aed72bf8973a8b6438cab7d2635f2563575fcf3d16eac4b4bf38df409953000d78afc8b691ed4b5e4eb40b39a7969858e79d9ee844697ce113e1f0170ae5
7
+ data.tar.gz: c99b76559985729d0ea59ee3ff5be523500965578c450c85a90c24fd2d958ebd6eb4ff4ad550d101e8178423f2f39d02ffe79a47a28f716e34865c4028ce83a5
data/CHANGELOG.md CHANGED
@@ -5,10 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.1.11] - 2025-09-05
8
+ ## [0.1.13] - 2026-03-20
9
+
10
+ ### Added
11
+
12
+ - Interval cycle kind (`I` notation) — repeating windows anchored to a from_date that re-anchor from completion date (e.g., `V1I24MF2026-03-31`)
9
13
 
10
14
  ### Changed
11
15
 
12
- - Use `time_span.notation` instead of `TimeSpan.notation` in `Cycle#notation`.
16
+ - Dormant capability is now declared on each cycle class (`def self.dormant_capable? = true`) instead of only in `Parser.dormant_capable_kinds`
17
+
18
+ ### Fixed
19
+
20
+ - EndOf cycle `final_date` was off by one period — `V1E12M` now correctly expires at the end of the 12th month, not the 11th
21
+
22
+ ## [0.1.12] - 2025-09-05
23
+
24
+ ### Added
13
25
 
14
- ## [0.1.10] - 2025-09-04
26
+ - Missing code coverage and updated EndOf to properly handle dormant cycles.
data/README.md CHANGED
@@ -25,7 +25,9 @@ cycle.to_s # => "3x in the prior 3 days"
25
25
 
26
26
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
27
27
 
28
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+ To install this gem onto your local machine, run `bundle exec rake install`.
29
+
30
+ This project is managed with [Reissue](https://github.com/SOFware/reissue). Releases are automated via the [shared release workflow](https://github.com/SOFware/reissue/blob/main/.github/workflows/SHARED_WORKFLOW_README.md). Trigger a release by running the "Release gem to RubyGems.org" workflow from the Actions tab.
29
31
 
30
32
  ## Contributing
31
33
 
data/Rakefile CHANGED
@@ -14,4 +14,5 @@ require "reissue/gem"
14
14
 
15
15
  Reissue::Task.create do |task|
16
16
  task.version_file = "lib/sof/cycle/version.rb"
17
+ task.push_finalize = :branch
17
18
  end
@@ -0,0 +1 @@
1
+ 3ce0dfe7d95a5cde507d7563ca9aa834457913dff9861d36547cff571fa9371be9fdeb60ec6892edb23b14945ab7a25f569d86cb023b67adfd3028a4570ac381
@@ -0,0 +1 @@
1
+ e7091d8a602dd216e947b2c0207ad66383b0322646c1b3ab4eacd670cd885035d3bf6e1e5ba3c848466aa9989f298ef8993fd1a260559953bb47a866cc613a52
@@ -2,6 +2,6 @@
2
2
 
3
3
  module SOF
4
4
  class Cycle
5
- VERSION = "0.1.11"
5
+ VERSION = "0.1.13"
6
6
  end
7
7
  end
data/lib/sof/cycle.rb CHANGED
@@ -130,6 +130,8 @@ module SOF
130
130
  attr_reader :notation_id, :kind, :valid_periods
131
131
  def volume_only? = @volume_only
132
132
 
133
+ def dormant_capable? = false
134
+
133
135
  def recurring? = raise "#{name} must implement #{__method__}"
134
136
 
135
137
  # Raises an error if the given period isn't in the list of valid periods.
@@ -215,12 +217,13 @@ module SOF
215
217
 
216
218
  attr_reader :parser
217
219
 
218
- delegate [:activated_notation, :volume, :from, :from_date, :time_span, :period,
220
+ delegate [:activated_notation, :volume, :from,
221
+ :from_date, :time_span, :period,
219
222
  :humanized_period, :period_key, :active?] => :@parser
220
223
  delegate [:kind, :recurring?, :volume_only?, :valid_periods] => "self.class"
221
224
  delegate [:period_count, :duration] => :time_span
222
- delegate [:calendar?, :dormant?, :end_of?, :lookback?, :volume_only?,
223
- :within?] => :kind_inquiry
225
+ delegate [:calendar?, :dormant?, :end_of?, :interval?, :lookback?,
226
+ :volume_only?, :within?] => :kind_inquiry
224
227
 
225
228
  def kind_inquiry = ActiveSupport::StringInquirer.new(kind.to_s)
226
229
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Captures the logic for enforcing the EndOf cycle variant
4
4
  # 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.
6
6
  # Complete 1 by that date to reset the cycle.
7
7
  #
8
8
  # Some of the calculations are quite different from other cycles.
@@ -18,6 +18,8 @@ module SOF
18
18
 
19
19
  def self.recurring? = true
20
20
 
21
+ def self.dormant_capable? = true
22
+
21
23
  def self.description
22
24
  "End of - occurrences by the end of a time period"
23
25
  end
@@ -27,7 +29,7 @@ module SOF
27
29
  end
28
30
 
29
31
  def to_s
30
- return dormant_to_s if dormant?
32
+ return dormant_to_s if parser.dormant? || from_date.nil?
31
33
 
32
34
  "#{volume}x by #{final_date.to_fs(:american)}"
33
35
  end
@@ -45,39 +47,45 @@ module SOF
45
47
  # Cycle.for("V1E18MF2020-01-09")
46
48
  # .expiration_of(anchor: "2020-06-04".to_date)
47
49
  # # => #<Date: 2021-06-30>
48
- def expiration_of(_ = nil, anchor: nil) = final_date
50
+ def expiration_of(_ = nil, anchor: nil)
51
+ return nil if parser.dormant? || from_date.nil?
52
+ final_date
53
+ end
49
54
 
50
55
  # Is the supplied anchor date prior to the final date?
51
56
  #
52
57
  # @return [Boolean] true if the cycle is satisfied, false otherwise
53
- def satisfied_by?(_ = nil, anchor: Date.current) = anchor <= final_date
58
+ def satisfied_by?(_ = nil, anchor: Date.current)
59
+ return false if parser.dormant? || from_date.nil?
60
+ anchor <= final_date
61
+ end
54
62
 
55
63
  # Calculates the final date of the cycle
56
64
  #
57
65
  # @param [nil] _ Unused parameter, maintained for compatibility
58
66
  # @return [Date] The final date of the cycle calculated as the end of the
59
- # nth subsequent period after the FROM date, where n = (period count - 1)
67
+ # nth period after the FROM date
60
68
  #
61
69
  # @example
62
70
  # Cycle.for("V1E18MF2020-01-09").final_date
63
- # # => #<Date: 2021-06-30>
64
- def final_date(_ = nil) = time_span
65
- .end_date(start_date - 1.send(period))
66
- .end_of_month
71
+ # # => #<Date: 2021-07-31>
72
+ def final_date(_ = nil)
73
+ return nil if parser.dormant? || from_date.nil?
74
+ time_span
75
+ .end_date(start_date)
76
+ .end_of_month
77
+ end
67
78
 
68
- def start_date(_ = nil) = from_date.to_date
79
+ def start_date(_ = nil) = from_date&.to_date
69
80
 
70
81
  private
71
82
 
72
83
  def dormant_to_s
73
- <<~DESC.squish
74
- #{volume}x by the last day of the #{subsequent_ordinal}
75
- subsequent #{period}
76
- DESC
84
+ "#{volume}x by the last day of the #{ordinalized_period_count} #{period}"
77
85
  end
78
86
 
79
- def subsequent_ordinal
80
- ActiveSupport::Inflector.ordinalize(period_count - 1)
87
+ def ordinalized_period_count
88
+ ActiveSupport::Inflector.ordinalize(period_count)
81
89
  end
82
90
  end
83
91
  end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Captures the logic for enforcing the Interval cycle variant
4
+ # E.g. "V1I24MF2026-03-31" means:
5
+ # Complete 1 every 24 months, current window from 2026-03-31.
6
+ # After completion, the consuming app re-anchors from the completion date.
7
+ #
8
+ # Unlike EndOf, there is no end-of-month rounding.
9
+ # Unlike Lookback, the window is anchored to a from_date, not sliding from today.
10
+ # Unlike Within, the window is repeating — it re-anchors from the completion date.
11
+ module SOF
12
+ module Cycles
13
+ class Interval < Cycle
14
+ @volume_only = false
15
+ @notation_id = "I"
16
+ @kind = :interval
17
+ @valid_periods = %w[D W M Y]
18
+
19
+ def self.recurring? = true
20
+
21
+ def self.dormant_capable? = true
22
+
23
+ def self.description
24
+ "Interval - occurrences within a repeating window that re-anchors from completion date"
25
+ end
26
+
27
+ def self.examples
28
+ ["V1I24MF2026-03-31 - once every 24 months from March 31, 2026 (re-anchors after completion)"]
29
+ end
30
+
31
+ def to_s
32
+ return dormant_to_s unless active?
33
+
34
+ "#{volume}x every #{humanized_span} from #{start_date.to_fs(:american)}"
35
+ end
36
+
37
+ # Returns the expiration date for the current window
38
+ #
39
+ # @return [Date, nil] The final date of the current window
40
+ def expiration_of(_ = nil, anchor: nil)
41
+ final_date
42
+ end
43
+
44
+ # Is the supplied anchor date within the current window?
45
+ #
46
+ # @return [Boolean] true if the anchor is before or on the final date
47
+ def satisfied_by?(_ = nil, anchor: Date.current)
48
+ anchor <= final_date
49
+ end
50
+
51
+ # Returns the from_date as the last completed date
52
+ def last_completed(_ = nil) = from_date&.to_date
53
+
54
+ # Calculates the final date of the current window
55
+ #
56
+ # @return [Date] from_date + period (no end-of-month rounding)
57
+ #
58
+ # @example
59
+ # Cycle.for("V1I24MF2026-03-31").final_date
60
+ # # => #<Date: 2028-03-31>
61
+ def final_date(_ = nil)
62
+ return nil if start_date.nil?
63
+ time_span.end_date(start_date)
64
+ end
65
+
66
+ def start_date(_ = nil) = from_date&.to_date
67
+
68
+ private
69
+
70
+ def dormant_to_s
71
+ "#{volume}x every #{humanized_span}"
72
+ end
73
+ end
74
+ end
75
+ end
@@ -12,7 +12,7 @@ module SOF
12
12
  def handles?(sym) = sym.nil? || sym.to_s == "volume_only"
13
13
 
14
14
  def validate_period(period)
15
- raise InvalidPeriod, <<~ERR.squish unless period.nil?
15
+ raise Cycle::InvalidPeriod, <<~ERR.squish unless period.nil?
16
16
  Invalid period value of '#{period}' provided. Valid periods are:
17
17
  #{valid_periods.join(", ")}
18
18
  ERR
@@ -10,6 +10,8 @@ module SOF
10
10
 
11
11
  def self.recurring? = false
12
12
 
13
+ def self.dormant_capable? = true
14
+
13
15
  def self.description
14
16
  "Within - occurrences within a time period from a specific date"
15
17
  end
data/lib/sof/parser.rb CHANGED
@@ -14,13 +14,15 @@ module SOF
14
14
  extend Forwardable
15
15
  PARTS_REGEX = /
16
16
  ^(?<vol>V(?<volume>\d*))? # optional volume
17
- (?<set>(?<kind>L|C|W|E) # kind
17
+ (?<set>(?<kind>L|C|W|E|I) # kind
18
18
  (?<period_count>\d+) # period count
19
19
  (?<period_key>D|W|M|Q|Y)?)? # period_key
20
20
  (?<from>F(?<from_date>\d{4}-\d{2}-\d{2}))?$ # optional from
21
21
  /ix
22
22
 
23
- def self.dormant_capable_kinds = %w[E W]
23
+ def self.dormant_capable_kinds
24
+ Cycle.cycle_handlers.select(&:dormant_capable?).map(&:notation_id).compact
25
+ end
24
26
 
25
27
  def self.for(notation_or_parser)
26
28
  return notation_or_parser if notation_or_parser.is_a? self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sof-cycle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ version: 0.1.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay
@@ -54,6 +54,8 @@ files:
54
54
  - checksums/sof-cycle-0.1.0.gem.sha512
55
55
  - checksums/sof-cycle-0.1.1.gem.sha512
56
56
  - checksums/sof-cycle-0.1.10.gem.sha512
57
+ - checksums/sof-cycle-0.1.11.gem.sha512
58
+ - checksums/sof-cycle-0.1.12.gem.sha512
57
59
  - checksums/sof-cycle-0.1.2.gem.sha512
58
60
  - checksums/sof-cycle-0.1.6.gem.sha512
59
61
  - checksums/sof-cycle-0.1.7.gem.sha512
@@ -65,6 +67,7 @@ files:
65
67
  - lib/sof/cycles/calendar.rb
66
68
  - lib/sof/cycles/dormant.rb
67
69
  - lib/sof/cycles/end_of.rb
70
+ - lib/sof/cycles/interval.rb
68
71
  - lib/sof/cycles/lookback.rb
69
72
  - lib/sof/cycles/volume_only.rb
70
73
  - lib/sof/cycles/within.rb