sof-cycle 0.1.12 → 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: 24c5b52ab1efb27e840af6448d16f2d74074310909a00d6d8a9bd10d42466b83
4
- data.tar.gz: a7507d727a8c7d87b34a6cabeccd49abaabb9ae29374bed6c9eff70a800d9c9b
3
+ metadata.gz: 0eb9458907426d68d8fc05c159a9bb782819b2ec821baf2ec24109b45c6794ff
4
+ data.tar.gz: e4c0b55074d59aad3f4e562e579759d8d85413b1fd93c93c873b79bdbff1c629
5
5
  SHA512:
6
- metadata.gz: deb2c0b296fef6fdc65fb0db0e7d92934de2c8a402f96757ac357446e87c2dc4d5cd880e6a8a210aed4bc794ff1977bba1397fb2de96aadd909879566851e783
7
- data.tar.gz: e238e4937718f34f644acf024ea4aae39742eee04a9b6d0b75a10df2306219ab8c3ac13abf6d7760e401d9554882f467518348ba0744e1eb51cf0fd2741452b9
6
+ metadata.gz: c799aed72bf8973a8b6438cab7d2635f2563575fcf3d16eac4b4bf38df409953000d78afc8b691ed4b5e4eb40b39a7969858e79d9ee844697ce113e1f0170ae5
7
+ data.tar.gz: c99b76559985729d0ea59ee3ff5be523500965578c450c85a90c24fd2d958ebd6eb4ff4ad550d101e8178423f2f39d02ffe79a47a28f716e34865c4028ce83a5
data/CHANGELOG.md CHANGED
@@ -5,14 +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.12] - 2025-09-05
8
+ ## [0.1.13] - 2026-03-20
9
9
 
10
10
  ### Added
11
11
 
12
- - Missing code coverage and updated EndOf to properly handle dormant cycles.
13
-
14
- ## [0.1.11] - 2025-09-05
12
+ - Interval cycle kind (`I` notation) repeating windows anchored to a from_date that re-anchor from completion date (e.g., `V1I24MF2026-03-31`)
15
13
 
16
14
  ### Changed
17
15
 
18
- - 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
25
+
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
+ e7091d8a602dd216e947b2c0207ad66383b0322646c1b3ab4eacd670cd885035d3bf6e1e5ba3c848466aa9989f298ef8993fd1a260559953bb47a866cc613a52
@@ -2,6 +2,6 @@
2
2
 
3
3
  module SOF
4
4
  class Cycle
5
- VERSION = "0.1.12"
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
@@ -62,15 +64,15 @@ module SOF
62
64
  #
63
65
  # @param [nil] _ Unused parameter, maintained for compatibility
64
66
  # @return [Date] The final date of the cycle calculated as the end of the
65
- # nth subsequent period after the FROM date, where n = (period count - 1)
67
+ # nth period after the FROM date
66
68
  #
67
69
  # @example
68
70
  # Cycle.for("V1E18MF2020-01-09").final_date
69
- # # => #<Date: 2021-06-30>
71
+ # # => #<Date: 2021-07-31>
70
72
  def final_date(_ = nil)
71
73
  return nil if parser.dormant? || from_date.nil?
72
74
  time_span
73
- .end_date(start_date - 1.send(period))
75
+ .end_date(start_date)
74
76
  .end_of_month
75
77
  end
76
78
 
@@ -79,14 +81,11 @@ module SOF
79
81
  private
80
82
 
81
83
  def dormant_to_s
82
- <<~DESC.squish
83
- #{volume}x by the last day of the #{subsequent_ordinal}
84
- subsequent #{period}
85
- DESC
84
+ "#{volume}x by the last day of the #{ordinalized_period_count} #{period}"
86
85
  end
87
86
 
88
- def subsequent_ordinal
89
- ActiveSupport::Inflector.ordinalize(period_count - 1)
87
+ def ordinalized_period_count
88
+ ActiveSupport::Inflector.ordinalize(period_count)
90
89
  end
91
90
  end
92
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
@@ -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.12
4
+ version: 0.1.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay
@@ -55,6 +55,7 @@ files:
55
55
  - checksums/sof-cycle-0.1.1.gem.sha512
56
56
  - checksums/sof-cycle-0.1.10.gem.sha512
57
57
  - checksums/sof-cycle-0.1.11.gem.sha512
58
+ - checksums/sof-cycle-0.1.12.gem.sha512
58
59
  - checksums/sof-cycle-0.1.2.gem.sha512
59
60
  - checksums/sof-cycle-0.1.6.gem.sha512
60
61
  - checksums/sof-cycle-0.1.7.gem.sha512
@@ -66,6 +67,7 @@ files:
66
67
  - lib/sof/cycles/calendar.rb
67
68
  - lib/sof/cycles/dormant.rb
68
69
  - lib/sof/cycles/end_of.rb
70
+ - lib/sof/cycles/interval.rb
69
71
  - lib/sof/cycles/lookback.rb
70
72
  - lib/sof/cycles/volume_only.rb
71
73
  - lib/sof/cycles/within.rb