sof-cycle 0.1.12 → 0.1.14
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 +4 -4
- data/.tool-versions +1 -1
- data/CHANGELOG.md +9 -5
- data/README.md +3 -1
- data/Rakefile +1 -0
- data/checksums/sof-cycle-0.1.12.gem.sha512 +1 -0
- data/lib/sof/cycle/version.rb +1 -1
- data/lib/sof/cycle.rb +7 -3
- data/lib/sof/cycles/end_of.rb +9 -10
- data/lib/sof/cycles/interval.rb +75 -0
- data/lib/sof/cycles/lookback_end_of.rb +43 -0
- data/lib/sof/cycles/within.rb +2 -0
- data/lib/sof/parser.rb +5 -2
- data/lib/sof/time_span.rb +2 -0
- metadata +5 -3
- data/.claude/settings.local.json +0 -17
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4bf300b22d76c7736f524022360c052b1546fa573ca39e851d4ce2a2ec2de4b7
|
|
4
|
+
data.tar.gz: 30f5a7972655d52acd3a547f59482c32d6c8fbdf22711f9f23eca27e695053cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63208d5574eaf86293c0a39214155f7a645b74d51b2dd23bfd03ae83b9dcbd032d145ffd909a8b3a675caef7485f6abe14d5b4bbfcbaafac3c383e29193fa8a7
|
|
7
|
+
data.tar.gz: e4caecf602ea58c7b0cec5a27b1efe36e23b65d0cf191b9c71eb7415e75a01c05079d4783c75d69e02d1ed31da76ef2765a15bc93ee84d43b39c8f03d615a17d
|
data/.tool-versions
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
ruby
|
|
1
|
+
ruby 4.0.2
|
data/CHANGELOG.md
CHANGED
|
@@ -5,14 +5,18 @@ 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.
|
|
8
|
+
## [0.1.14] - 2026-04-27
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## [0.1.13] - 2026-03-20
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
### Added
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
- Interval cycle kind (`I` notation) — repeating windows anchored to a from_date that re-anchor from completion date (e.g., `V1I24MF2026-03-31`)
|
|
15
15
|
|
|
16
16
|
### Changed
|
|
17
17
|
|
|
18
|
-
-
|
|
18
|
+
- Dormant capability is now declared on each cycle class (`def self.dormant_capable? = true`) instead of only in `Parser.dormant_capable_kinds`
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- EndOf cycle `final_date` was off by one period — `V1E12M` now correctly expires at the end of the 12th month, not the 11th
|
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`.
|
|
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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
e7091d8a602dd216e947b2c0207ad66383b0322646c1b3ab4eacd670cd885035d3bf6e1e5ba3c848466aa9989f298ef8993fd1a260559953bb47a866cc613a52
|
data/lib/sof/cycle/version.rb
CHANGED
data/lib/sof/cycle.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "parser"
|
|
|
6
6
|
module SOF
|
|
7
7
|
class Cycle
|
|
8
8
|
extend ::Forwardable
|
|
9
|
+
|
|
9
10
|
class InvalidInput < StandardError; end
|
|
10
11
|
|
|
11
12
|
class InvalidPeriod < InvalidInput; end
|
|
@@ -130,6 +131,8 @@ module SOF
|
|
|
130
131
|
attr_reader :notation_id, :kind, :valid_periods
|
|
131
132
|
def volume_only? = @volume_only
|
|
132
133
|
|
|
134
|
+
def dormant_capable? = false
|
|
135
|
+
|
|
133
136
|
def recurring? = raise "#{name} must implement #{__method__}"
|
|
134
137
|
|
|
135
138
|
# Raises an error if the given period isn't in the list of valid periods.
|
|
@@ -215,12 +218,13 @@ module SOF
|
|
|
215
218
|
|
|
216
219
|
attr_reader :parser
|
|
217
220
|
|
|
218
|
-
delegate [:activated_notation, :volume, :from,
|
|
221
|
+
delegate [:activated_notation, :volume, :from,
|
|
222
|
+
:from_date, :time_span, :period,
|
|
219
223
|
:humanized_period, :period_key, :active?] => :@parser
|
|
220
224
|
delegate [:kind, :recurring?, :volume_only?, :valid_periods] => "self.class"
|
|
221
225
|
delegate [:period_count, :duration] => :time_span
|
|
222
|
-
delegate [:calendar?, :dormant?, :end_of?, :
|
|
223
|
-
:within?] => :kind_inquiry
|
|
226
|
+
delegate [:calendar?, :dormant?, :end_of?, :interval?, :lookback?,
|
|
227
|
+
:lookback_end_of?, :volume_only?, :within?] => :kind_inquiry
|
|
224
228
|
|
|
225
229
|
def kind_inquiry = ActiveSupport::StringInquirer.new(kind.to_s)
|
|
226
230
|
|
data/lib/sof/cycles/end_of.rb
CHANGED
|
@@ -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
|
|
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
|
|
67
|
+
# nth period after the FROM date
|
|
66
68
|
#
|
|
67
69
|
# @example
|
|
68
70
|
# Cycle.for("V1E18MF2020-01-09").final_date
|
|
69
|
-
# # => #<Date: 2021-
|
|
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
|
|
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
|
-
|
|
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
|
|
89
|
-
ActiveSupport::Inflector.ordinalize(period_count
|
|
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
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SOF
|
|
4
|
+
module Cycles
|
|
5
|
+
class LookbackEndOf < Cycle
|
|
6
|
+
@volume_only = false
|
|
7
|
+
@notation_id = "LE"
|
|
8
|
+
@kind = :lookback_end_of
|
|
9
|
+
@valid_periods = %w[D W M Q Y]
|
|
10
|
+
|
|
11
|
+
def self.recurring? = true
|
|
12
|
+
|
|
13
|
+
def self.description
|
|
14
|
+
"Lookback End of Period - occurrences within a prior time period, expiring at the end of the calendar period"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.examples
|
|
18
|
+
["V1LE24M - once in the prior 24 months (expires end of month)", "V2LE3W - twice in the prior 3 weeks (expires end of week)"]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_s = "#{volume}x in the prior #{period_count} #{humanized_period} (end of period)"
|
|
22
|
+
|
|
23
|
+
def expiration_of(completion_dates, anchor: Date.current)
|
|
24
|
+
oldest = completion_dates.max_by(volume) { it }.min
|
|
25
|
+
return unless satisfied_by?(completion_dates, anchor:)
|
|
26
|
+
|
|
27
|
+
final_date(oldest)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def final_date(anchor)
|
|
31
|
+
return if anchor.nil?
|
|
32
|
+
|
|
33
|
+
time_span.end_date_of_period(time_span.end_date(anchor.to_date))
|
|
34
|
+
end
|
|
35
|
+
alias_method :window_end, :final_date
|
|
36
|
+
|
|
37
|
+
def start_date(anchor)
|
|
38
|
+
time_span.begin_date_of_period(time_span.begin_date(anchor.to_date))
|
|
39
|
+
end
|
|
40
|
+
alias_method :window_start, :start_date
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/sof/cycles/within.rb
CHANGED
data/lib/sof/parser.rb
CHANGED
|
@@ -12,15 +12,18 @@ module SOF
|
|
|
12
12
|
# This is an internal implementation of Cycle behavior.
|
|
13
13
|
class Parser
|
|
14
14
|
extend Forwardable
|
|
15
|
+
|
|
15
16
|
PARTS_REGEX = /
|
|
16
17
|
^(?<vol>V(?<volume>\d*))? # optional volume
|
|
17
|
-
(?<set>(?<kind>L|C|W|E) # kind
|
|
18
|
+
(?<set>(?<kind>LE|L|C|W|E|I) # kind
|
|
18
19
|
(?<period_count>\d+) # period count
|
|
19
20
|
(?<period_key>D|W|M|Q|Y)?)? # period_key
|
|
20
21
|
(?<from>F(?<from_date>\d{4}-\d{2}-\d{2}))?$ # optional from
|
|
21
22
|
/ix
|
|
22
23
|
|
|
23
|
-
def self.dormant_capable_kinds
|
|
24
|
+
def self.dormant_capable_kinds
|
|
25
|
+
Cycle.cycle_handlers.select(&:dormant_capable?).map(&:notation_id).compact
|
|
26
|
+
end
|
|
24
27
|
|
|
25
28
|
def self.for(notation_or_parser)
|
|
26
29
|
return notation_or_parser if notation_or_parser.is_a? self
|
data/lib/sof/time_span.rb
CHANGED
|
@@ -5,6 +5,7 @@ module SOF
|
|
|
5
5
|
# This is an internal implementation of Cycle behavior.
|
|
6
6
|
class TimeSpan
|
|
7
7
|
extend Forwardable
|
|
8
|
+
|
|
8
9
|
# TimeSpan objects map Cycle notations to behaviors for their periods
|
|
9
10
|
#
|
|
10
11
|
# For example:
|
|
@@ -58,6 +59,7 @@ module SOF
|
|
|
58
59
|
# quarter, etc.
|
|
59
60
|
class DatePeriod
|
|
60
61
|
extend Forwardable
|
|
62
|
+
|
|
61
63
|
class << self
|
|
62
64
|
def for(count, period_notation)
|
|
63
65
|
@cached_periods ||= {}
|
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.
|
|
4
|
+
version: 0.1.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jim Gay
|
|
@@ -43,7 +43,6 @@ executables: []
|
|
|
43
43
|
extensions: []
|
|
44
44
|
extra_rdoc_files: []
|
|
45
45
|
files:
|
|
46
|
-
- ".claude/settings.local.json"
|
|
47
46
|
- ".rspec"
|
|
48
47
|
- ".simplecov"
|
|
49
48
|
- ".tool-versions"
|
|
@@ -55,6 +54,7 @@ files:
|
|
|
55
54
|
- checksums/sof-cycle-0.1.1.gem.sha512
|
|
56
55
|
- checksums/sof-cycle-0.1.10.gem.sha512
|
|
57
56
|
- checksums/sof-cycle-0.1.11.gem.sha512
|
|
57
|
+
- checksums/sof-cycle-0.1.12.gem.sha512
|
|
58
58
|
- checksums/sof-cycle-0.1.2.gem.sha512
|
|
59
59
|
- checksums/sof-cycle-0.1.6.gem.sha512
|
|
60
60
|
- checksums/sof-cycle-0.1.7.gem.sha512
|
|
@@ -66,7 +66,9 @@ files:
|
|
|
66
66
|
- lib/sof/cycles/calendar.rb
|
|
67
67
|
- lib/sof/cycles/dormant.rb
|
|
68
68
|
- lib/sof/cycles/end_of.rb
|
|
69
|
+
- lib/sof/cycles/interval.rb
|
|
69
70
|
- lib/sof/cycles/lookback.rb
|
|
71
|
+
- lib/sof/cycles/lookback_end_of.rb
|
|
70
72
|
- lib/sof/cycles/volume_only.rb
|
|
71
73
|
- lib/sof/cycles/within.rb
|
|
72
74
|
- lib/sof/parser.rb
|
|
@@ -90,7 +92,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
90
92
|
- !ruby/object:Gem::Version
|
|
91
93
|
version: '0'
|
|
92
94
|
requirements: []
|
|
93
|
-
rubygems_version:
|
|
95
|
+
rubygems_version: 4.0.6
|
|
94
96
|
specification_version: 4
|
|
95
97
|
summary: Parse and interact with SOF cycle notation.
|
|
96
98
|
test_files: []
|
data/.claude/settings.local.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(find:*)",
|
|
5
|
-
"Bash(ls:*)",
|
|
6
|
-
"Bash(mkdir:*)",
|
|
7
|
-
"Bash(touch:*)",
|
|
8
|
-
"Bash(rm:*)",
|
|
9
|
-
"Bash(bundle exec rspec:*)",
|
|
10
|
-
"Bash(bundle exec rake:*)",
|
|
11
|
-
"Bash(bundle exec standardrb:*)",
|
|
12
|
-
"Bash(git checkout:*)",
|
|
13
|
-
"Bash(bundle exec ruby:*)"
|
|
14
|
-
],
|
|
15
|
-
"deny": []
|
|
16
|
-
}
|
|
17
|
-
}
|