sof-cycle 0.1.0 → 0.1.2

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: 416c2fd3c9e369ece57bafe7cbf5a58f07b9e30177c04b7253059cb87d6e32dc
4
- data.tar.gz: 00fd30f51d4efb2eeabbe7eef1b8fdd58a6290a08579ae661984b2de359585e4
3
+ metadata.gz: ea1c2c9a48bba72114b7681278cd467c57ee147f6f8b1ff176b4508de355920b
4
+ data.tar.gz: 93a7560799df4730a5eaf2d14708b9a24199c0a95d2b01f1487ddc587b0846da
5
5
  SHA512:
6
- metadata.gz: 582f1597715e073ea8d517b8236ad3daf81fcdf9ce023c860a77ed2ddb4847e4df027b606764915d816d7d5ddd06ec383bcae58fb0174e8e1e6f02d97265ed71
7
- data.tar.gz: 8d9e747a1ac2d0a649f911f6c7de8e81dc7a8139294c41c37eb20b8d9cc67e52e5242f61df1315b74aab24127f5c0c7ea7738a9d9fc7e1130ddc94dec9de6445
6
+ metadata.gz: baf569f5fb7e0500b8a28915c8f19b86dc97ebee380301787182216d162fd02e659645345d369440959ebde737b59bb8ddd4372dae0bf014a9e3b448e8455e03
7
+ data.tar.gz: 84756e0428c56a7f6dd72e5dc7aa545a587bbffd49479a436e4f8bee2d60ec3496e128f713a36b0169799ff908e0690e4518b147e750eaa0fb662681f0e036a0
data/CHANGELOG.md CHANGED
@@ -5,8 +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.0] - 2024-07-09
8
+ ## [0.1.2] - 2024-08-09
9
9
 
10
10
  ### Added
11
11
 
12
- - Initial extraction
12
+ - `Cycle#recurring?` to reveal if a given Cycle is one-and-done or must be repeated.
13
+
14
+ ## [0.1.1] - 2024-08-09
15
+
16
+ ### Added
17
+
18
+ - Basic example in README.md
19
+ - Add `Cycles::EndOf` to handle cycles that cover through the end of the nth
20
+ subsequent period
21
+ - Add predicate methods for each `Cycle` subclass. E.g. `#dormant?`, `#within?`, etc
22
+ - Refactor into namespaces
data/README.md CHANGED
@@ -14,7 +14,12 @@ If bundler is not being used to manage dependencies, install the gem by executin
14
14
 
15
15
  ## Usage
16
16
 
17
- TODO: Write usage instructions here
17
+ ```ruby
18
+ cycle = SOF::Cycle.load({ volume: 3, kind: :lookback, period: :day, period_count: 3 })
19
+ cycle.to_h # => { volume: 3, kind: :lookback, period: :day, period_count: 3 }
20
+ cycle.notation # => "V3L3D"
21
+ cycle.to_s # => "3x in the prior 3 days"
22
+ ```
18
23
 
19
24
  ## Development
20
25
 
data/Rakefile CHANGED
@@ -14,4 +14,4 @@ require "reissue/gem"
14
14
 
15
15
  Reissue::Task.create do |task|
16
16
  task.version_file = "lib/sof/cycle/version.rb"
17
- end
17
+ end
@@ -0,0 +1 @@
1
+ 9a87a33268363c7f7afbb084677ff7e37c9b1102992b8082cac7529cfb3871fc92e8218d14de218a04b26afb415505286a76f948094b3cd5032b52dcf81645c6
@@ -0,0 +1 @@
1
+ 53766201b732ae80e67079d1efeda25c86f144cc59c1aed2a210a8d068a689409bfd1fc0e6be7b769ae15500c3224d2c5c7854385ac84b60e44a54f94bb22c65
@@ -0,0 +1 @@
1
+ 78a488a4cea134800aeca3e3e06461a413c564b111d96b25a74c4ae44b5afa66a04d4c59be33cc455bab0c9b1d80cfdd3af1b980859ae3955cb1fd7350fe1459
@@ -2,6 +2,6 @@
2
2
 
3
3
  module SOF
4
4
  class Cycle
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.2"
6
6
  end
7
7
  end
data/lib/sof/cycle.rb CHANGED
@@ -1,11 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "cycle/version"
4
- require "forwardable"
5
- require_relative "cycle/parser"
6
- require_relative "cycle/time_span"
7
- require "active_support/core_ext/date/conversions"
8
- require "active_support/core_ext/string/filters"
3
+ require_relative "parser"
9
4
 
10
5
  module SOF
11
6
  class Cycle
@@ -30,8 +25,10 @@ module SOF
30
25
 
31
26
  delegate [:activated_notation, :volume, :from, :from_date, :time_span, :period,
32
27
  :humanized_period, :period_key, :active?] => :@parser
33
- delegate [:kind, :volume_only?, :valid_periods] => "self.class"
28
+ delegate [:kind, :recurring?, :volume_only?, :valid_periods] => "self.class"
34
29
  delegate [:period_count, :duration] => :time_span
30
+ delegate [:calendar?, :dormant?, :end_of?, :lookback?, :volume_only?,
31
+ :within?] => :kind_inquiry
35
32
 
36
33
  # Turn a cycle or notation string into a hash
37
34
  def self.dump(cycle_or_string)
@@ -83,7 +80,7 @@ module SOF
83
80
  # @return [Cycle] a Cycle object representing the provide string notation
84
81
  def self.for(notation)
85
82
  return notation if notation.is_a? Cycle
86
- return notation if notation.is_a? Cycle::Dormant
83
+ return notation if notation.is_a? Cycles::Dormant
87
84
  parser = Parser.new(notation)
88
85
  unless parser.valid?
89
86
  raise InvalidInput, "'#{notation}' is not a valid input"
@@ -94,7 +91,7 @@ module SOF
94
91
  end.new(notation, parser:)
95
92
  return cycle if parser.active?
96
93
 
97
- Cycle::Dormant.new(cycle, parser:)
94
+ Cycles::Dormant.new(cycle, parser:)
98
95
  end
99
96
 
100
97
  # Return the appropriate class for the give notation id
@@ -133,10 +130,11 @@ module SOF
133
130
  @kind = nil
134
131
  @valid_periods = []
135
132
 
136
- def self.volume_only? = @volume_only
137
-
138
133
  class << self
139
134
  attr_reader :notation_id, :kind, :valid_periods
135
+ def volume_only? = @volume_only
136
+
137
+ def recurring? = raise "#{name} must implement #{__method__}"
140
138
  end
141
139
 
142
140
  # Raises an error if the given period isn't in the list of valid periods.
@@ -150,6 +148,8 @@ module SOF
150
148
  ERR
151
149
  end
152
150
 
151
+ def kind_inquiry = ActiveSupport::StringInquirer.new(kind.to_s)
152
+
153
153
  def validate_period
154
154
  return if valid_periods.empty?
155
155
 
@@ -208,146 +208,5 @@ module SOF
208
208
  end
209
209
 
210
210
  def as_json(...) = notation
211
-
212
- class Dormant
213
- def initialize(cycle, parser:)
214
- @cycle = cycle
215
- @parser = parser
216
- end
217
-
218
- attr_reader :cycle, :parser
219
-
220
- def to_s
221
- cycle.to_s + " (dormant)"
222
- end
223
-
224
- def covered_dates(...) = []
225
-
226
- def expiration_of(...) = nil
227
-
228
- def satisfied_by?(...) = false
229
-
230
- def cover?(...) = false
231
-
232
- def method_missing(method, ...) = cycle.send(method, ...)
233
-
234
- def respond_to_missing?(method, include_private = false)
235
- cycle.respond_to?(method, include_private)
236
- end
237
- end
238
-
239
- class Within < self
240
- @volume_only = false
241
- @notation_id = "W"
242
- @kind = :within
243
- @valid_periods = %w[D W M Y]
244
-
245
- def to_s = "#{volume}x within #{date_range}"
246
-
247
- def date_range
248
- return humanized_span unless active?
249
-
250
- [start_date, final_date].map { _1.to_fs(:american) }.join(" - ")
251
- end
252
-
253
- def final_date(_ = nil) = time_span.end_date(start_date)
254
-
255
- def start_date(_ = nil) = from_date.to_date
256
- end
257
-
258
- class VolumeOnly < self
259
- @volume_only = true
260
- @notation_id = nil
261
- @kind = :volume_only
262
- @valid_periods = []
263
-
264
- class << self
265
- def handles?(sym) = sym.nil? || super
266
-
267
- def validate_period(period)
268
- raise InvalidPeriod, <<~ERR.squish unless period.nil?
269
- Invalid period value of '#{period}' provided. Valid periods are:
270
- #{valid_periods.join(", ")}
271
- ERR
272
- end
273
- end
274
-
275
- def to_s = "#{volume}x total"
276
-
277
- def covered_dates(dates, ...) = dates
278
-
279
- def cover?(...) = true
280
- end
281
-
282
- class Lookback < self
283
- @volume_only = false
284
- @notation_id = "L"
285
- @kind = :lookback
286
- @valid_periods = %w[D W M Y]
287
-
288
- def to_s = "#{volume}x in the prior #{period_count} #{humanized_period}"
289
-
290
- def volume_to_delay_expiration(completion_dates, anchor:)
291
- oldest_relevant_completion = completion_dates.min
292
- [completion_dates.count(oldest_relevant_completion), volume].min
293
- end
294
-
295
- # "Absent further completions, you go red on this date"
296
- # @return [Date, nil] the date on which the cycle will expire given the
297
- # provided completion dates. Returns nil if the cycle is already unsatisfied.
298
- def expiration_of(completion_dates)
299
- anchor = completion_dates.max_by(volume) { _1 }.min
300
- return unless satisfied_by?(completion_dates, anchor:)
301
-
302
- window_end anchor
303
- end
304
-
305
- def final_date(anchor)
306
- return if anchor.nil?
307
-
308
- time_span.end_date(anchor.to_date)
309
- end
310
- alias_method :window_end, :final_date
311
-
312
- def start_date(anchor)
313
- time_span.begin_date(anchor.to_date)
314
- end
315
- alias_method :window_start, :start_date
316
- end
317
-
318
- class Calendar < self
319
- @volume_only = false
320
- @notation_id = "C"
321
- @kind = :calendar
322
- @valid_periods = %w[M Q Y]
323
-
324
- class << self
325
- def frame_of_reference = "total"
326
- end
327
-
328
- def to_s
329
- "#{volume}x every #{period_count} calendar #{humanized_period}"
330
- end
331
-
332
- # "Absent further completions, you go red on this date"
333
- # @return [Date, nil] the date on which the cycle will expire given the
334
- # provided completion dates. Returns nil if the cycle is already unsatisfied.
335
- def expiration_of(completion_dates)
336
- anchor = completion_dates.max_by(volume) { _1 }.min
337
- return unless satisfied_by?(completion_dates, anchor:)
338
-
339
- window_end(anchor) + duration
340
- end
341
-
342
- def final_date(anchor)
343
- return if anchor.nil?
344
- time_span.end_date_of_period(anchor.to_date)
345
- end
346
- alias_method :window_end, :final_date
347
-
348
- def start_date(anchor)
349
- time_span.begin_date_of_period(anchor.to_date)
350
- end
351
- end
352
211
  end
353
212
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SOF
4
+ module Cycles
5
+ class Calendar < Cycle
6
+ @volume_only = false
7
+ @notation_id = "C"
8
+ @kind = :calendar
9
+ @valid_periods = %w[M Q Y]
10
+
11
+ class << self
12
+ def frame_of_reference = "total"
13
+ end
14
+
15
+ def self.recurring? = true
16
+
17
+ def to_s
18
+ "#{volume}x every #{period_count} calendar #{humanized_period}"
19
+ end
20
+
21
+ # "Absent further completions, you go red on this date"
22
+ # @return [Date, nil] the date on which the cycle will expire given the
23
+ # provided completion dates. Returns nil if the cycle is already unsatisfied.
24
+ def expiration_of(completion_dates)
25
+ anchor = completion_dates.max_by(volume) { _1 }.min
26
+ return unless satisfied_by?(completion_dates, anchor:)
27
+
28
+ window_end(anchor) + duration
29
+ end
30
+
31
+ def final_date(anchor)
32
+ return if anchor.nil?
33
+ time_span.end_date_of_period(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(anchor.to_date)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SOF
4
+ module Cycles
5
+ class Dormant
6
+ def initialize(cycle, parser:)
7
+ @cycle = cycle
8
+ @parser = parser
9
+ end
10
+
11
+ attr_reader :cycle, :parser
12
+
13
+ def self.recurring? = false
14
+
15
+ def kind = :dormant
16
+
17
+ def dormant? = true
18
+
19
+ def to_s
20
+ cycle.to_s + " (dormant)"
21
+ end
22
+
23
+ def covered_dates(...) = []
24
+
25
+ def expiration_of(...) = nil
26
+
27
+ def satisfied_by?(...) = false
28
+
29
+ def cover?(...) = false
30
+
31
+ def method_missing(method, ...) = cycle.send(method, ...)
32
+
33
+ def respond_to_missing?(method, include_private = false)
34
+ cycle.respond_to?(method, include_private)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SOF
4
+ module Cycles
5
+ class EndOf < Cycle
6
+ @volume_only = false
7
+ @notation_id = "E"
8
+ @kind = :end_of
9
+ @valid_periods = %w[W M Q Y]
10
+
11
+ def self.recurring? = true
12
+
13
+ def to_s
14
+ return dormant_to_s if dormant?
15
+
16
+ "#{volume}x by #{final_date.to_fs(:american)}"
17
+ end
18
+
19
+ def final_date(_ = nil) = time_span
20
+ .end_date(start_date)
21
+ .end_of_month
22
+
23
+ def start_date(_ = nil) = from_date.to_date
24
+
25
+ private
26
+
27
+ def dormant_to_s
28
+ <<~DESC.squish
29
+ #{volume}x by the last day of the #{subsequent_ordinal}
30
+ subsequent #{period}
31
+ DESC
32
+ end
33
+
34
+ def subsequent_ordinal
35
+ ActiveSupport::Inflector.ordinalize(period_count - 1)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SOF
4
+ module Cycles
5
+ class Lookback < Cycle
6
+ @volume_only = false
7
+ @notation_id = "L"
8
+ @kind = :lookback
9
+ @valid_periods = %w[D W M Y]
10
+
11
+ def self.recurring? = true
12
+
13
+ def to_s = "#{volume}x in the prior #{period_count} #{humanized_period}"
14
+
15
+ def volume_to_delay_expiration(completion_dates, anchor:)
16
+ oldest_relevant_completion = completion_dates.min
17
+ [completion_dates.count(oldest_relevant_completion), volume].min
18
+ end
19
+
20
+ # "Absent further completions, you go red on this date"
21
+ # @return [Date, nil] the date on which the cycle will expire given the
22
+ # provided completion dates. Returns nil if the cycle is already unsatisfied.
23
+ def expiration_of(completion_dates)
24
+ anchor = completion_dates.max_by(volume) { _1 }.min
25
+ return unless satisfied_by?(completion_dates, anchor:)
26
+
27
+ window_end anchor
28
+ end
29
+
30
+ def final_date(anchor)
31
+ return if anchor.nil?
32
+
33
+ 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(anchor.to_date)
39
+ end
40
+ alias_method :window_start, :start_date
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SOF
4
+ module Cycles
5
+ class VolumeOnly < Cycle
6
+ @volume_only = true
7
+ @notation_id = nil
8
+ @kind = :volume_only
9
+ @valid_periods = []
10
+
11
+ class << self
12
+ def handles?(sym) = sym.nil? || super
13
+
14
+ def validate_period(period)
15
+ raise InvalidPeriod, <<~ERR.squish unless period.nil?
16
+ Invalid period value of '#{period}' provided. Valid periods are:
17
+ #{valid_periods.join(", ")}
18
+ ERR
19
+ end
20
+ end
21
+
22
+ def self.recurring? = false
23
+
24
+ def to_s = "#{volume}x total"
25
+
26
+ def covered_dates(dates, ...) = dates
27
+
28
+ def cover?(...) = true
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SOF
4
+ module Cycles
5
+ class Within < Cycle
6
+ @volume_only = false
7
+ @notation_id = "W"
8
+ @kind = :within
9
+ @valid_periods = %w[D W M Y]
10
+
11
+ def self.recurring? = false
12
+
13
+ def to_s = "#{volume}x within #{date_range}"
14
+
15
+ def date_range
16
+ return humanized_span unless active?
17
+
18
+ [start_date, final_date].map { _1.to_fs(:american) }.join(" - ")
19
+ end
20
+
21
+ def final_date(_ = nil) = time_span.end_date(start_date)
22
+
23
+ def start_date(_ = nil) = from_date.to_date
24
+ end
25
+ end
26
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../cycle"
3
+ require_relative "cycle"
4
4
  require "active_support/core_ext/hash/keys"
5
5
  require "active_support/core_ext/object/blank"
6
6
  require "active_support/core_ext/object/inclusion"
@@ -10,22 +10,22 @@ require "active_support/isolated_execution_state"
10
10
  module SOF
11
11
  # This class is not intended to be referenced directly.
12
12
  # This is an internal implementation of Cycle behavior.
13
- class Cycle::Parser
13
+ class Parser
14
14
  extend Forwardable
15
15
  PARTS_REGEX = /
16
16
  ^(?<vol>V(?<volume>\d*))? # optional volume
17
- (?<set>(?<kind>L|C|W) # kind
17
+ (?<set>(?<kind>L|C|W|E) # 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[W]
23
+ def self.dormant_capable_kinds = %w[E W]
24
24
 
25
- def self.for(str_or_notation)
26
- return str_or_notation if str_or_notation.is_a? self
25
+ def self.for(notation_or_parser)
26
+ return notation_or_parser if notation_or_parser.is_a? self
27
27
 
28
- new(str_or_notation)
28
+ new(notation_or_parser)
29
29
  end
30
30
 
31
31
  def self.load(hash)
@@ -50,7 +50,7 @@ module SOF
50
50
 
51
51
  # Return a TimeSpan object for the period and period_count
52
52
  def time_span
53
- @time_span ||= Cycle::TimeSpan.for(period_count, period_key)
53
+ @time_span ||= TimeSpan.for(period_count, period_key)
54
54
  end
55
55
 
56
56
  def valid? = match.present?
@@ -1,15 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../cycle"
4
- require "active_support/deprecator"
5
- require "active_support/deprecation"
6
- require "active_support/core_ext/numeric/time"
7
- require "active_support/core_ext/integer/time"
8
- require "active_support/core_ext/string/conversions"
9
3
  module SOF
10
4
  # This class is not intended to be referenced directly.
11
5
  # This is an internal implementation of Cycle behavior.
12
- class Cycle::TimeSpan
6
+ class TimeSpan
13
7
  extend Forwardable
14
8
  # TimeSpan objects map Cycle notations to behaviors for their periods
15
9
  #
data/lib/sof-cycle.rb CHANGED
@@ -1 +1,17 @@
1
+ require "forwardable"
2
+ require "active_support/deprecator"
3
+ require "active_support/deprecation"
4
+ require "active_support/core_ext/numeric/time"
5
+ require "active_support/core_ext/integer/time"
6
+ require "active_support/core_ext/string/conversions"
7
+ require "active_support/core_ext/date/conversions"
8
+ require "active_support/core_ext/string/filters"
9
+ require "active_support/inflector"
10
+ require "active_support/string_inquirer"
11
+
12
+ require_relative "sof/cycle/version"
1
13
  require_relative "sof/cycle"
14
+
15
+ Dir[File.join(__dir__, "sof", "cycles", "*.rb")].each { |file| require file }
16
+
17
+ require_relative "sof/time_span"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sof-cycle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Gay
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-09 00:00:00.000000000 Z
11
+ date: 2024-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: forwardable
@@ -50,11 +50,20 @@ files:
50
50
  - CHANGELOG.md
51
51
  - README.md
52
52
  - Rakefile
53
+ - checksums/sof-cycle-0.1.0.gem.sha512
54
+ - checksums/sof-cycle-0.1.1.gem.sha512
55
+ - checksums/sof-cycle-0.1.2.gem.sha512
53
56
  - lib/sof-cycle.rb
54
57
  - lib/sof/cycle.rb
55
- - lib/sof/cycle/parser.rb
56
- - lib/sof/cycle/time_span.rb
57
58
  - lib/sof/cycle/version.rb
59
+ - lib/sof/cycles/calendar.rb
60
+ - lib/sof/cycles/dormant.rb
61
+ - lib/sof/cycles/end_of.rb
62
+ - lib/sof/cycles/lookback.rb
63
+ - lib/sof/cycles/volume_only.rb
64
+ - lib/sof/cycles/within.rb
65
+ - lib/sof/parser.rb
66
+ - lib/sof/time_span.rb
58
67
  homepage: https://github.com/SOFware/sof-cycle
59
68
  licenses: []
60
69
  metadata:
@@ -75,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
84
  - !ruby/object:Gem::Version
76
85
  version: '0'
77
86
  requirements: []
78
- rubygems_version: 3.5.9
87
+ rubygems_version: 3.4.13
79
88
  signing_key:
80
89
  specification_version: 4
81
90
  summary: Parse and interact with SOF cycle notation.