sof-cycle 0.1.0 → 0.1.1

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: a371875b67c2b91bcda00cd042bfa8f4daed5c0ecdbdff66b5680f546b039435
4
+ data.tar.gz: 720805643f533158704ad8316a95f0cf1d31b02681a4a7654a4e859951216242
5
5
  SHA512:
6
- metadata.gz: 582f1597715e073ea8d517b8236ad3daf81fcdf9ce023c860a77ed2ddb4847e4df027b606764915d816d7d5ddd06ec383bcae58fb0174e8e1e6f02d97265ed71
7
- data.tar.gz: 8d9e747a1ac2d0a649f911f6c7de8e81dc7a8139294c41c37eb20b8d9cc67e52e5242f61df1315b74aab24127f5c0c7ea7738a9d9fc7e1130ddc94dec9de6445
6
+ metadata.gz: 48aa749e3b0c8a8c810d8e863025af1dadc964348f4f98670f03fade2da9aded8be063c2fe7893ee50b3f9836de3658dc93137a8c41fa9e7afd33e8a51e8c3b3
7
+ data.tar.gz: 18148c4e037cd7edd5749ac2c216d13e63ac581065a2a6d45101947dfa5b9d041e611bbc64d36d20336c846cc0272c7eb3eeb077831372661de1512d2f0e3f2c
data/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ 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.1] - 2024-08-09
9
+
10
+ ### Added
11
+
12
+ - Basic example in README.md
13
+ - Add `Cycles::EndOf` to handle cycles that cover through the end of the nth
14
+ subsequent period
15
+ - Add predicate methods for each `Cycle` subclass. E.g. `#dormant?`, `#within?`, etc
16
+ - Refactor into namespaces
17
+
8
18
  ## [0.1.0] - 2024-07-09
9
19
 
10
20
  ### Added
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module SOF
4
4
  class Cycle
5
- VERSION = "0.1.0"
5
+ VERSION = "0.1.1"
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
@@ -32,6 +27,8 @@ module SOF
32
27
  :humanized_period, :period_key, :active?] => :@parser
33
28
  delegate [:kind, :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
@@ -150,6 +147,8 @@ module SOF
150
147
  ERR
151
148
  end
152
149
 
150
+ def kind_inquiry = ActiveSupport::StringInquirer.new(kind.to_s)
151
+
153
152
  def validate_period
154
153
  return if valid_periods.empty?
155
154
 
@@ -208,146 +207,5 @@ module SOF
208
207
  end
209
208
 
210
209
  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
210
  end
353
211
  end
@@ -0,0 +1,40 @@
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 to_s
16
+ "#{volume}x every #{period_count} calendar #{humanized_period}"
17
+ end
18
+
19
+ # "Absent further completions, you go red on this date"
20
+ # @return [Date, nil] the date on which the cycle will expire given the
21
+ # provided completion dates. Returns nil if the cycle is already unsatisfied.
22
+ def expiration_of(completion_dates)
23
+ anchor = completion_dates.max_by(volume) { _1 }.min
24
+ return unless satisfied_by?(completion_dates, anchor:)
25
+
26
+ window_end(anchor) + duration
27
+ end
28
+
29
+ def final_date(anchor)
30
+ return if anchor.nil?
31
+ time_span.end_date_of_period(anchor.to_date)
32
+ end
33
+ alias_method :window_end, :final_date
34
+
35
+ def start_date(anchor)
36
+ time_span.begin_date_of_period(anchor.to_date)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,36 @@
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 kind = :dormant
14
+
15
+ def dormant? = true
16
+
17
+ def to_s
18
+ cycle.to_s + " (dormant)"
19
+ end
20
+
21
+ def covered_dates(...) = []
22
+
23
+ def expiration_of(...) = nil
24
+
25
+ def satisfied_by?(...) = false
26
+
27
+ def cover?(...) = false
28
+
29
+ def method_missing(method, ...) = cycle.send(method, ...)
30
+
31
+ def respond_to_missing?(method, include_private = false)
32
+ cycle.respond_to?(method, include_private)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,37 @@
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 to_s
12
+ return dormant_to_s if dormant?
13
+
14
+ "#{volume}x by #{final_date.to_fs(:american)}"
15
+ end
16
+
17
+ def final_date(_ = nil) = time_span
18
+ .end_date(start_date)
19
+ .end_of_month
20
+
21
+ def start_date(_ = nil) = from_date.to_date
22
+
23
+ private
24
+
25
+ def dormant_to_s
26
+ <<~DESC.squish
27
+ #{volume}x by the last day of the #{subsequent_ordinal}
28
+ subsequent #{period}
29
+ DESC
30
+ end
31
+
32
+ def subsequent_ordinal
33
+ ActiveSupport::Inflector.ordinalize(period_count - 1)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,41 @@
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 to_s = "#{volume}x in the prior #{period_count} #{humanized_period}"
12
+
13
+ def volume_to_delay_expiration(completion_dates, anchor:)
14
+ oldest_relevant_completion = completion_dates.min
15
+ [completion_dates.count(oldest_relevant_completion), volume].min
16
+ end
17
+
18
+ # "Absent further completions, you go red on this date"
19
+ # @return [Date, nil] the date on which the cycle will expire given the
20
+ # provided completion dates. Returns nil if the cycle is already unsatisfied.
21
+ def expiration_of(completion_dates)
22
+ anchor = completion_dates.max_by(volume) { _1 }.min
23
+ return unless satisfied_by?(completion_dates, anchor:)
24
+
25
+ window_end anchor
26
+ end
27
+
28
+ def final_date(anchor)
29
+ return if anchor.nil?
30
+
31
+ time_span.end_date(anchor.to_date)
32
+ end
33
+ alias_method :window_end, :final_date
34
+
35
+ def start_date(anchor)
36
+ time_span.begin_date(anchor.to_date)
37
+ end
38
+ alias_method :window_start, :start_date
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
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 to_s = "#{volume}x total"
23
+
24
+ def covered_dates(dates, ...) = dates
25
+
26
+ def cover?(...) = true
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,24 @@
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 to_s = "#{volume}x within #{date_range}"
12
+
13
+ def date_range
14
+ return humanized_span unless active?
15
+
16
+ [start_date, final_date].map { _1.to_fs(:american) }.join(" - ")
17
+ end
18
+
19
+ def final_date(_ = nil) = time_span.end_date(start_date)
20
+
21
+ def start_date(_ = nil) = from_date.to_date
22
+ end
23
+ end
24
+ 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.1
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,19 @@ 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
53
55
  - lib/sof-cycle.rb
54
56
  - lib/sof/cycle.rb
55
- - lib/sof/cycle/parser.rb
56
- - lib/sof/cycle/time_span.rb
57
57
  - lib/sof/cycle/version.rb
58
+ - lib/sof/cycles/calendar.rb
59
+ - lib/sof/cycles/dormant.rb
60
+ - lib/sof/cycles/end_of.rb
61
+ - lib/sof/cycles/lookback.rb
62
+ - lib/sof/cycles/volume_only.rb
63
+ - lib/sof/cycles/within.rb
64
+ - lib/sof/parser.rb
65
+ - lib/sof/time_span.rb
58
66
  homepage: https://github.com/SOFware/sof-cycle
59
67
  licenses: []
60
68
  metadata:
@@ -75,7 +83,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
83
  - !ruby/object:Gem::Version
76
84
  version: '0'
77
85
  requirements: []
78
- rubygems_version: 3.5.9
86
+ rubygems_version: 3.4.13
79
87
  signing_key:
80
88
  specification_version: 4
81
89
  summary: Parse and interact with SOF cycle notation.