sof-cycle 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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.