sof-cycle 0.1.13 → 0.1.15
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 +2 -18
- data/checksums/sof-cycle-0.1.13.gem.sha512 +1 -0
- data/checksums/sof-cycle-0.1.14.gem.sha512 +1 -0
- data/checksums/sof-cycle-0.1.3.gem.sha512 +1 -0
- data/checksums/sof-cycle-0.1.4.gem.sha512 +1 -0
- data/checksums/sof-cycle-0.1.5.gem.sha512 +1 -0
- data/lib/sof/cycle/parser.rb +127 -0
- data/lib/sof/cycle/registry.rb +86 -0
- data/lib/sof/cycle/time_span.rb +317 -0
- data/lib/sof/cycle/version.rb +1 -1
- data/lib/sof/cycle.rb +116 -31
- data/lib/sof/cycles/calendar.rb +1 -4
- data/lib/sof/cycles/dormant.rb +4 -0
- data/lib/sof/cycles/end_of.rb +1 -4
- data/lib/sof/cycles/interval.rb +1 -4
- data/lib/sof/cycles/lookback.rb +1 -4
- data/lib/sof/cycles/lookback_end_of.rb +40 -0
- data/lib/sof/cycles/volume_only.rb +1 -4
- data/lib/sof/cycles/within.rb +2 -5
- data/lib/sof-cycle.rb +1 -1
- metadata +11 -5
- data/.claude/settings.local.json +0 -17
- data/lib/sof/parser.rb +0 -109
- data/lib/sof/time_span.rb +0 -234
data/lib/sof/cycle.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "forwardable"
|
|
4
|
-
require_relative "
|
|
4
|
+
require_relative "cycle/registry"
|
|
5
|
+
require_relative "cycle/parser"
|
|
5
6
|
|
|
6
7
|
module SOF
|
|
7
8
|
class Cycle
|
|
8
9
|
extend ::Forwardable
|
|
10
|
+
|
|
9
11
|
class InvalidInput < StandardError; end
|
|
10
12
|
|
|
11
13
|
class InvalidPeriod < InvalidInput; end
|
|
@@ -69,9 +71,7 @@ module SOF
|
|
|
69
71
|
raise InvalidInput, "'#{notation}' is not a valid input"
|
|
70
72
|
end
|
|
71
73
|
|
|
72
|
-
cycle =
|
|
73
|
-
parser.parses?(klass.notation_id)
|
|
74
|
-
end.new(notation, parser:)
|
|
74
|
+
cycle = registry.for_notation_id(parser.kind).new(notation, parser:)
|
|
75
75
|
return cycle if parser.active?
|
|
76
76
|
|
|
77
77
|
Cycles::Dormant.new(cycle, parser:)
|
|
@@ -83,22 +83,14 @@ module SOF
|
|
|
83
83
|
# @example
|
|
84
84
|
# class_for_notation_id('L')
|
|
85
85
|
#
|
|
86
|
-
def class_for_notation_id(notation_id)
|
|
87
|
-
Cycle.cycle_handlers.find do |klass|
|
|
88
|
-
klass.notation_id == notation_id
|
|
89
|
-
end || raise(InvalidKind, "'#{notation_id}' is not a valid kind of #{name}")
|
|
90
|
-
end
|
|
86
|
+
def class_for_notation_id(notation_id) = registry.for_notation_id(notation_id)
|
|
91
87
|
|
|
92
88
|
# Return the class handling the kind
|
|
93
89
|
#
|
|
94
90
|
# @param sym [Symbol] symbol matching the kind of Cycle class
|
|
95
91
|
# @example
|
|
96
92
|
# class_for_kind(:lookback)
|
|
97
|
-
def class_for_kind(sym)
|
|
98
|
-
Cycle.cycle_handlers.find do |klass|
|
|
99
|
-
klass.handles?(sym)
|
|
100
|
-
end || raise(InvalidKind, "':#{sym}' is not a valid kind of Cycle")
|
|
101
|
-
end
|
|
93
|
+
def class_for_kind(sym) = registry.handling(sym)
|
|
102
94
|
|
|
103
95
|
# Return a legend explaining all notation components
|
|
104
96
|
#
|
|
@@ -122,6 +114,8 @@ module SOF
|
|
|
122
114
|
}
|
|
123
115
|
end
|
|
124
116
|
|
|
117
|
+
# Defaults for the base class itself, which handles no kind and so is
|
|
118
|
+
# never registered.
|
|
125
119
|
@volume_only = false
|
|
126
120
|
@notation_id = nil
|
|
127
121
|
@kind = nil
|
|
@@ -149,23 +143,77 @@ module SOF
|
|
|
149
143
|
kind.to_s == sym.to_s
|
|
150
144
|
end
|
|
151
145
|
|
|
152
|
-
|
|
153
|
-
|
|
146
|
+
# Declare what kind of cycle this class handles, and register it.
|
|
147
|
+
#
|
|
148
|
+
# Registration is a consequence of declaring, so the two can never
|
|
149
|
+
# disagree, and subclassing Cycle for any other reason — an abstract
|
|
150
|
+
# intermediate, a test double — registers nothing.
|
|
151
|
+
#
|
|
152
|
+
# An application can declare its own kind this way and the parser will
|
|
153
|
+
# recognise its notation, because the pattern is built from what is
|
|
154
|
+
# registered.
|
|
155
|
+
#
|
|
156
|
+
# @param kind [Symbol] the kind this class handles, e.g. :lookback
|
|
157
|
+
# @param notation [String, nil] the notation id opening the cycle, e.g.
|
|
158
|
+
# "L". Omit for a cycle with no notation of its own, such as
|
|
159
|
+
# volume-only.
|
|
160
|
+
# @param periods [Array<String>] period keys this kind accepts. Empty
|
|
161
|
+
# means the kind takes no period.
|
|
162
|
+
# @param volume_only [Boolean] whether this kind carries volume alone.
|
|
163
|
+
#
|
|
164
|
+
# @example
|
|
165
|
+
# class Fortnightly < SOF::Cycle
|
|
166
|
+
# handles :fortnightly, notation: "X", periods: %w[D W]
|
|
167
|
+
# def self.recurring? = true
|
|
168
|
+
# end
|
|
169
|
+
def handles(kind, notation: nil, periods: [], volume_only: false)
|
|
170
|
+
@kind = kind
|
|
171
|
+
@notation_id = notation
|
|
172
|
+
@valid_periods = periods
|
|
173
|
+
@volume_only = volume_only
|
|
174
|
+
register(self)
|
|
154
175
|
end
|
|
155
176
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
177
|
+
# Register a handler that does not inherit from Cycle. Inheriting and
|
|
178
|
+
# declaring with `handles` is the easier path, because it supplies the
|
|
179
|
+
# behaviour Cycle.for goes on to use; this is for a class that would
|
|
180
|
+
# rather implement that itself.
|
|
181
|
+
#
|
|
182
|
+
# The class must answer, at the class level:
|
|
183
|
+
#
|
|
184
|
+
# kind the kind it handles, e.g. :lookback
|
|
185
|
+
# notation_id the notation opening it, e.g. "L", or nil
|
|
186
|
+
# valid_periods period keys it accepts, e.g. %w[D W M Y]
|
|
187
|
+
# volume_only? whether it carries volume alone
|
|
188
|
+
# handles?(kind) whether it answers to a kind
|
|
189
|
+
# recurring? whether the window repeats
|
|
190
|
+
# dormant_capable? whether it can be written without a from date
|
|
191
|
+
# validate_period(key)
|
|
192
|
+
# description, examples (for the legend)
|
|
193
|
+
#
|
|
194
|
+
# and instances are built as `new(notation, parser:)`.
|
|
195
|
+
#
|
|
196
|
+
# Registering displaces any handler already answering to the same kind
|
|
197
|
+
# or notation id, so this replaces a built-in as readily as it adds one.
|
|
198
|
+
#
|
|
199
|
+
# @param cycle_class [Class] the handler to register
|
|
200
|
+
# @return [Class] the class registered
|
|
201
|
+
def register(cycle_class) = registry.register(cycle_class)
|
|
202
|
+
|
|
203
|
+
# Drop a handler, undoing `register` or a `handles` declaration.
|
|
204
|
+
def unregister(cycle_class) = registry.unregister(cycle_class)
|
|
205
|
+
|
|
206
|
+
def registry = Registry.instance
|
|
159
207
|
|
|
160
208
|
private
|
|
161
209
|
|
|
162
210
|
def build_kind_legend
|
|
163
211
|
legend = {}
|
|
164
|
-
|
|
212
|
+
registry.cycle_classes.each do |handler|
|
|
165
213
|
# Skip volume_only since it doesn't have a notation_id
|
|
166
|
-
next if handler.
|
|
214
|
+
next if handler.volume_only?
|
|
167
215
|
|
|
168
|
-
notation_id = handler.
|
|
216
|
+
notation_id = handler.notation_id
|
|
169
217
|
next unless notation_id
|
|
170
218
|
|
|
171
219
|
legend[notation_id] = {
|
|
@@ -178,14 +226,10 @@ module SOF
|
|
|
178
226
|
|
|
179
227
|
def build_period_legend
|
|
180
228
|
legend = {}
|
|
181
|
-
#
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
"M" => "month",
|
|
186
|
-
"Q" => "quarter",
|
|
187
|
-
"Y" => "year"
|
|
188
|
-
}
|
|
229
|
+
# Built from the registered periods, so one an application adds shows
|
|
230
|
+
# up here too.
|
|
231
|
+
period_mappings = TimeSpan.period_registry.period_classes
|
|
232
|
+
.to_h { |klass| [klass.code, klass.period.to_s] }
|
|
189
233
|
|
|
190
234
|
period_mappings.each do |code, period_name|
|
|
191
235
|
legend[code] = {
|
|
@@ -223,7 +267,7 @@ module SOF
|
|
|
223
267
|
delegate [:kind, :recurring?, :volume_only?, :valid_periods] => "self.class"
|
|
224
268
|
delegate [:period_count, :duration] => :time_span
|
|
225
269
|
delegate [:calendar?, :dormant?, :end_of?, :interval?, :lookback?,
|
|
226
|
-
:volume_only?, :within?] => :kind_inquiry
|
|
270
|
+
:lookback_end_of?, :volume_only?, :within?] => :kind_inquiry
|
|
227
271
|
|
|
228
272
|
def kind_inquiry = ActiveSupport::StringInquirer.new(kind.to_s)
|
|
229
273
|
|
|
@@ -252,6 +296,39 @@ module SOF
|
|
|
252
296
|
|
|
253
297
|
def extend_period(_ = nil) = self
|
|
254
298
|
|
|
299
|
+
# Whether this kind of cycle can be written without a from date, and so
|
|
300
|
+
# can be anchored later. Answers on the instance so a dormant cycle can
|
|
301
|
+
# be asked too — `cycle.class` is Cycles::Dormant there, which knows
|
|
302
|
+
# nothing of the kind it wraps.
|
|
303
|
+
def dormant_capable? = self.class.dormant_capable?
|
|
304
|
+
|
|
305
|
+
# The cycle restarted from the latest of `dates` past its current anchor.
|
|
306
|
+
#
|
|
307
|
+
# A recurring, dormant-capable window (E and I) is reset by the act that
|
|
308
|
+
# satisfies it — "complete 1 by that date to reset the cycle". Nothing in
|
|
309
|
+
# the notation does that on its own, so this used to fall to each
|
|
310
|
+
# consuming app.
|
|
311
|
+
#
|
|
312
|
+
# Dates on or before the current anchor are ignored: a back-dated act
|
|
313
|
+
# must not drag a forward-running window backwards. Every other kind
|
|
314
|
+
# returns itself — a lookback window already slides, a calendar window is
|
|
315
|
+
# pinned to the calendar, a one-shot window does not repeat, and an
|
|
316
|
+
# un-anchored one has no window to move until it is activated.
|
|
317
|
+
#
|
|
318
|
+
# @param dates [Array<Date, Time, nil>] the acts that satisfy this cycle
|
|
319
|
+
# @return [Cycle] the reset cycle, or self when nothing resets it
|
|
320
|
+
#
|
|
321
|
+
# @example
|
|
322
|
+
# Cycle.for("V1E18MF2026-02-01").reset_by([Date.new(2026, 9, 15)])
|
|
323
|
+
# # => Cycle.for("V1E18MF2026-09-15")
|
|
324
|
+
def reset_by(dates)
|
|
325
|
+
return self unless recurring? && dormant_capable?
|
|
326
|
+
return self if from_date.nil?
|
|
327
|
+
|
|
328
|
+
reset = reset_date(dates)
|
|
329
|
+
reset ? Cycle.for(activated_notation(reset)) : self
|
|
330
|
+
end
|
|
331
|
+
|
|
255
332
|
# From the supplied anchor date, are there enough in-window completions to
|
|
256
333
|
# satisfy the cycle?
|
|
257
334
|
#
|
|
@@ -302,5 +379,13 @@ module SOF
|
|
|
302
379
|
end
|
|
303
380
|
|
|
304
381
|
def as_json(...) = notation
|
|
382
|
+
|
|
383
|
+
private
|
|
384
|
+
|
|
385
|
+
# The latest date that moves this cycle's window forward, or nil.
|
|
386
|
+
def reset_date(dates)
|
|
387
|
+
anchored_on = from_date.to_date
|
|
388
|
+
Array(dates).filter_map { it&.to_date }.select { it > anchored_on }.max
|
|
389
|
+
end
|
|
305
390
|
end
|
|
306
391
|
end
|
data/lib/sof/cycles/calendar.rb
CHANGED
data/lib/sof/cycles/dormant.rb
CHANGED
|
@@ -22,6 +22,10 @@ module SOF
|
|
|
22
22
|
|
|
23
23
|
def covered_dates(...) = []
|
|
24
24
|
|
|
25
|
+
# No anchor yet, so nothing to move: a dormant cycle is reset by being
|
|
26
|
+
# activated, not by the acts that satisfy it.
|
|
27
|
+
def reset_by(...) = self
|
|
28
|
+
|
|
25
29
|
def expiration_of(...) = nil
|
|
26
30
|
|
|
27
31
|
def satisfied_by?(...) = false
|
data/lib/sof/cycles/end_of.rb
CHANGED
data/lib/sof/cycles/interval.rb
CHANGED
data/lib/sof/cycles/lookback.rb
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SOF
|
|
4
|
+
module Cycles
|
|
5
|
+
class LookbackEndOf < Cycle
|
|
6
|
+
handles :lookback_end_of, notation: "LE", periods: %w[D W M Q Y]
|
|
7
|
+
|
|
8
|
+
def self.recurring? = true
|
|
9
|
+
|
|
10
|
+
def self.description
|
|
11
|
+
"Lookback End of Period - occurrences within a prior time period, expiring at the end of the calendar period"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.examples
|
|
15
|
+
["V1LE24M - once in the prior 24 months (expires end of month)", "V2LE3W - twice in the prior 3 weeks (expires end of week)"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_s = "#{volume}x in the prior #{period_count} #{humanized_period} (end of period)"
|
|
19
|
+
|
|
20
|
+
def expiration_of(completion_dates, anchor: Date.current)
|
|
21
|
+
oldest = completion_dates.max_by(volume) { it }.min
|
|
22
|
+
return unless satisfied_by?(completion_dates, anchor:)
|
|
23
|
+
|
|
24
|
+
final_date(oldest)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def final_date(anchor)
|
|
28
|
+
return if anchor.nil?
|
|
29
|
+
|
|
30
|
+
time_span.end_date_of_period(time_span.end_date(anchor.to_date))
|
|
31
|
+
end
|
|
32
|
+
alias_method :window_end, :final_date
|
|
33
|
+
|
|
34
|
+
def start_date(anchor)
|
|
35
|
+
time_span.begin_date_of_period(time_span.begin_date(anchor.to_date))
|
|
36
|
+
end
|
|
37
|
+
alias_method :window_start, :start_date
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -3,10 +3,7 @@
|
|
|
3
3
|
module SOF
|
|
4
4
|
module Cycles
|
|
5
5
|
class VolumeOnly < Cycle
|
|
6
|
-
|
|
7
|
-
@notation_id = nil
|
|
8
|
-
@kind = :volume_only
|
|
9
|
-
@valid_periods = []
|
|
6
|
+
handles :volume_only, volume_only: true
|
|
10
7
|
|
|
11
8
|
class << self
|
|
12
9
|
def handles?(sym) = sym.nil? || sym.to_s == "volume_only"
|
data/lib/sof/cycles/within.rb
CHANGED
|
@@ -3,10 +3,7 @@
|
|
|
3
3
|
module SOF
|
|
4
4
|
module Cycles
|
|
5
5
|
class Within < Cycle
|
|
6
|
-
|
|
7
|
-
@notation_id = "W"
|
|
8
|
-
@kind = :within
|
|
9
|
-
@valid_periods = %w[D W M Y]
|
|
6
|
+
handles :within, notation: "W", periods: %w[D W M Y]
|
|
10
7
|
|
|
11
8
|
def self.recurring? = false
|
|
12
9
|
|
|
@@ -24,7 +21,7 @@ module SOF
|
|
|
24
21
|
|
|
25
22
|
def extend_period(count)
|
|
26
23
|
Cycle.for(
|
|
27
|
-
Parser.load(
|
|
24
|
+
Cycle::Parser.load(
|
|
28
25
|
parser.to_h.merge(period_count: period_count + count)
|
|
29
26
|
).to_s
|
|
30
27
|
)
|
data/lib/sof-cycle.rb
CHANGED
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.15
|
|
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"
|
|
@@ -56,23 +55,30 @@ files:
|
|
|
56
55
|
- checksums/sof-cycle-0.1.10.gem.sha512
|
|
57
56
|
- checksums/sof-cycle-0.1.11.gem.sha512
|
|
58
57
|
- checksums/sof-cycle-0.1.12.gem.sha512
|
|
58
|
+
- checksums/sof-cycle-0.1.13.gem.sha512
|
|
59
|
+
- checksums/sof-cycle-0.1.14.gem.sha512
|
|
59
60
|
- checksums/sof-cycle-0.1.2.gem.sha512
|
|
61
|
+
- checksums/sof-cycle-0.1.3.gem.sha512
|
|
62
|
+
- checksums/sof-cycle-0.1.4.gem.sha512
|
|
63
|
+
- checksums/sof-cycle-0.1.5.gem.sha512
|
|
60
64
|
- checksums/sof-cycle-0.1.6.gem.sha512
|
|
61
65
|
- checksums/sof-cycle-0.1.7.gem.sha512
|
|
62
66
|
- checksums/sof-cycle-0.1.8.gem.sha512
|
|
63
67
|
- checksums/sof-cycle-0.1.9.gem.sha512
|
|
64
68
|
- lib/sof-cycle.rb
|
|
65
69
|
- lib/sof/cycle.rb
|
|
70
|
+
- lib/sof/cycle/parser.rb
|
|
71
|
+
- lib/sof/cycle/registry.rb
|
|
72
|
+
- lib/sof/cycle/time_span.rb
|
|
66
73
|
- lib/sof/cycle/version.rb
|
|
67
74
|
- lib/sof/cycles/calendar.rb
|
|
68
75
|
- lib/sof/cycles/dormant.rb
|
|
69
76
|
- lib/sof/cycles/end_of.rb
|
|
70
77
|
- lib/sof/cycles/interval.rb
|
|
71
78
|
- lib/sof/cycles/lookback.rb
|
|
79
|
+
- lib/sof/cycles/lookback_end_of.rb
|
|
72
80
|
- lib/sof/cycles/volume_only.rb
|
|
73
81
|
- lib/sof/cycles/within.rb
|
|
74
|
-
- lib/sof/parser.rb
|
|
75
|
-
- lib/sof/time_span.rb
|
|
76
82
|
homepage: https://github.com/SOFware/sof-cycle
|
|
77
83
|
licenses: []
|
|
78
84
|
metadata:
|
|
@@ -92,7 +98,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
92
98
|
- !ruby/object:Gem::Version
|
|
93
99
|
version: '0'
|
|
94
100
|
requirements: []
|
|
95
|
-
rubygems_version:
|
|
101
|
+
rubygems_version: 4.0.6
|
|
96
102
|
specification_version: 4
|
|
97
103
|
summary: Parse and interact with SOF cycle notation.
|
|
98
104
|
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
|
-
}
|
data/lib/sof/parser.rb
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "cycle"
|
|
4
|
-
require "active_support/core_ext/hash/keys"
|
|
5
|
-
require "active_support/core_ext/object/blank"
|
|
6
|
-
require "active_support/core_ext/object/inclusion"
|
|
7
|
-
require "active_support/core_ext/hash/reverse_merge"
|
|
8
|
-
require "active_support/isolated_execution_state"
|
|
9
|
-
|
|
10
|
-
module SOF
|
|
11
|
-
# This class is not intended to be referenced directly.
|
|
12
|
-
# This is an internal implementation of Cycle behavior.
|
|
13
|
-
class Parser
|
|
14
|
-
extend Forwardable
|
|
15
|
-
PARTS_REGEX = /
|
|
16
|
-
^(?<vol>V(?<volume>\d*))? # optional volume
|
|
17
|
-
(?<set>(?<kind>L|C|W|E|I) # kind
|
|
18
|
-
(?<period_count>\d+) # period count
|
|
19
|
-
(?<period_key>D|W|M|Q|Y)?)? # period_key
|
|
20
|
-
(?<from>F(?<from_date>\d{4}-\d{2}-\d{2}))?$ # optional from
|
|
21
|
-
/ix
|
|
22
|
-
|
|
23
|
-
def self.dormant_capable_kinds
|
|
24
|
-
Cycle.cycle_handlers.select(&:dormant_capable?).map(&:notation_id).compact
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def self.for(notation_or_parser)
|
|
28
|
-
return notation_or_parser if notation_or_parser.is_a? self
|
|
29
|
-
|
|
30
|
-
new(notation_or_parser)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def self.load(hash)
|
|
34
|
-
hash.symbolize_keys!
|
|
35
|
-
hash.reverse_merge!(volume: 1)
|
|
36
|
-
keys = %i[volume kind period_count period_key]
|
|
37
|
-
str = "V#{hash.values_at(*keys).join}"
|
|
38
|
-
return new(str) unless hash[:from_date]
|
|
39
|
-
|
|
40
|
-
new([str, "F#{hash[:from_date]}"].join)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def initialize(notation)
|
|
44
|
-
@notation = notation&.upcase
|
|
45
|
-
@match = @notation&.match(PARTS_REGEX)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
attr_reader :match, :notation
|
|
49
|
-
|
|
50
|
-
delegate [:dormant_capable_kinds] => "self.class"
|
|
51
|
-
delegate [:period, :humanized_period] => :time_span
|
|
52
|
-
|
|
53
|
-
# Return a TimeSpan object for the period and period_count
|
|
54
|
-
def time_span
|
|
55
|
-
@time_span ||= TimeSpan.for(period_count, period_key)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def valid? = match.present?
|
|
59
|
-
|
|
60
|
-
def inspect = notation
|
|
61
|
-
alias_method :to_s, :inspect
|
|
62
|
-
|
|
63
|
-
def activated_notation(date)
|
|
64
|
-
return notation unless dormant_capable?
|
|
65
|
-
|
|
66
|
-
self.class.load(to_h.merge(from_date: date.to_date)).notation
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def ==(other) = other.to_h == to_h
|
|
70
|
-
|
|
71
|
-
def to_h
|
|
72
|
-
{
|
|
73
|
-
volume:,
|
|
74
|
-
kind:,
|
|
75
|
-
period_count:,
|
|
76
|
-
period_key:,
|
|
77
|
-
from_date:
|
|
78
|
-
}
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def parses?(notation_id) = kind == notation_id
|
|
82
|
-
|
|
83
|
-
def active? = !dormant?
|
|
84
|
-
|
|
85
|
-
def dormant? = dormant_capable? && from_date.nil?
|
|
86
|
-
|
|
87
|
-
def dormant_capable? = kind.in?(dormant_capable_kinds)
|
|
88
|
-
|
|
89
|
-
def period_count = match[:period_count]
|
|
90
|
-
|
|
91
|
-
def period_key = match[:period_key]
|
|
92
|
-
|
|
93
|
-
def vol = match[:vol] || "V1"
|
|
94
|
-
|
|
95
|
-
def volume = (match[:volume] || 1).to_i
|
|
96
|
-
|
|
97
|
-
def from_data
|
|
98
|
-
return {} unless from
|
|
99
|
-
|
|
100
|
-
{from: from}
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def from_date = match[:from_date]
|
|
104
|
-
|
|
105
|
-
def from = match[:from]
|
|
106
|
-
|
|
107
|
-
def kind = match[:kind]
|
|
108
|
-
end
|
|
109
|
-
end
|