Class: SOF::Cycle
- Inherits:
-
Object
- Object
- SOF::Cycle
- Extended by:
- Forwardable
- Defined in:
- lib/sof/cycle.rb,
lib/sof/cycle/parser.rb,
lib/sof/cycle/version.rb,
lib/sof/cycle/registry.rb,
lib/sof/cycle/time_span.rb
Direct Known Subclasses
SOF::Cycles::Calendar, SOF::Cycles::EndOf, SOF::Cycles::Interval, SOF::Cycles::Lookback, SOF::Cycles::LookbackEndOf, SOF::Cycles::VolumeOnly, SOF::Cycles::Within
Defined Under Namespace
Classes: InvalidInput, InvalidKind, InvalidPeriod, Parser, Registry, TimeSpan
Constant Summary collapse
- VERSION =
"0.1.15"
Class Attribute Summary collapse
-
.kind ⇒ Object
readonly
Returns the value of attribute kind.
-
.notation_id ⇒ Object
readonly
Returns the value of attribute notation_id.
-
.valid_periods ⇒ Object
readonly
Returns the value of attribute valid_periods.
Instance Attribute Summary collapse
-
#parser ⇒ Object
readonly
Returns the value of attribute parser.
Class Method Summary collapse
-
.class_for_kind(sym) ⇒ Object
Return the class handling the kind.
-
.class_for_notation_id(notation_id) ⇒ Object
Return the appropriate class for the give notation id.
- .dormant_capable? ⇒ Boolean
-
.dump(cycle_or_string) ⇒ Object
Turn a cycle or notation string into a hash.
-
.for(notation) ⇒ Cycle
Return a Cycle object from a notation string.
-
.handles(kind, notation: nil, periods: [], volume_only: false) ⇒ Object
Declare what kind of cycle this class handles, and register it.
- .handles?(sym) ⇒ Boolean
-
.legend ⇒ Hash
Return a legend explaining all notation components.
-
.load(hash) ⇒ Object
Return a Cycle object from a hash.
-
.notation(hash) ⇒ String
Retun a notation string from a hash.
- .recurring? ⇒ Boolean
-
.register(cycle_class) ⇒ Class
Register a handler that does not inherit from Cycle.
- .registry ⇒ Object
-
.unregister(cycle_class) ⇒ Object
Drop a handler, undoing
registeror ahandlesdeclaration. -
.validate_period(period) ⇒ Object
Raises an error if the given period isn't in the list of valid periods.
- .volume_only? ⇒ Boolean
Instance Method Summary collapse
-
#==(other) ⇒ Object
Cycles are considered equal if their hash representations are equal.
- #as_json ⇒ Object
- #considered_dates(completion_dates, anchor: Date.current) ⇒ Object
- #cover?(date, anchor: Date.current) ⇒ Boolean
- #covered_dates(dates, anchor: Date.current) ⇒ Object
-
#dormant_capable? ⇒ Boolean
Whether this kind of cycle can be written without a from date, and so can be anchored later.
- #expiration_of(_completion_dates, anchor: Date.current) ⇒ Object
- #extend_period(_ = nil) ⇒ Object
-
#final_date(_anchor) ⇒ Object
Return the final date of the cycle.
- #from_data ⇒ Object
- #humanized_span ⇒ Object
-
#initialize(notation, parser: Parser.new(notation)) ⇒ Cycle
constructor
A new instance of Cycle.
- #kind_inquiry ⇒ Object
-
#last_completed(dates) ⇒ Object
Return the most recent completion date from the supplied array of dates.
-
#notation ⇒ Object
Return the cycle representation as a notation string.
- #range(anchor) ⇒ Object
-
#reset_by(dates) ⇒ Cycle
The cycle restarted from the latest of
datespast its current anchor. -
#satisfied_by?(completion_dates, anchor: Date.current) ⇒ Boolean
From the supplied anchor date, are there enough in-window completions to satisfy the cycle?.
- #to_h ⇒ Object
- #validate_period ⇒ Object
- #volume_to_delay_expiration(_completion_dates, anchor:) ⇒ Object
Constructor Details
#initialize(notation, parser: Parser.new(notation)) ⇒ Cycle
Returns a new instance of Cycle.
252 253 254 255 256 257 258 259 260 |
# File 'lib/sof/cycle.rb', line 252 def initialize(notation, parser: Parser.new(notation)) @notation = notation @parser = parser validate_period return if @parser.valid? raise InvalidInput, "'#{notation}' is not a valid input" end |
Class Attribute Details
.kind ⇒ Object (readonly)
Returns the value of attribute kind.
124 125 126 |
# File 'lib/sof/cycle.rb', line 124 def kind @kind end |
.notation_id ⇒ Object (readonly)
Returns the value of attribute notation_id.
124 125 126 |
# File 'lib/sof/cycle.rb', line 124 def notation_id @notation_id end |
.valid_periods ⇒ Object (readonly)
Returns the value of attribute valid_periods.
124 125 126 |
# File 'lib/sof/cycle.rb', line 124 def valid_periods @valid_periods end |
Instance Attribute Details
#parser ⇒ Object (readonly)
Returns the value of attribute parser.
262 263 264 |
# File 'lib/sof/cycle.rb', line 262 def parser @parser end |
Class Method Details
.class_for_kind(sym) ⇒ Object
Return the class handling the kind
93 |
# File 'lib/sof/cycle.rb', line 93 def class_for_kind(sym) = registry.handling(sym) |
.class_for_notation_id(notation_id) ⇒ Object
Return the appropriate class for the give notation id
86 |
# File 'lib/sof/cycle.rb', line 86 def class_for_notation_id(notation_id) = registry.for_notation_id(notation_id) |
.dormant_capable? ⇒ Boolean
127 |
# File 'lib/sof/cycle.rb', line 127 def dormant_capable? = false |
.dump(cycle_or_string) ⇒ Object
Turn a cycle or notation string into a hash
19 20 21 22 23 24 25 |
# File 'lib/sof/cycle.rb', line 19 def dump(cycle_or_string) if cycle_or_string.is_a? Cycle cycle_or_string else Cycle.for(cycle_or_string) end.to_h end |
.for(notation) ⇒ Cycle
Return a Cycle object from a notation string
66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/sof/cycle.rb', line 66 def for(notation) return notation if notation.is_a? Cycle return notation if notation.is_a? Cycles::Dormant parser = Parser.new(notation) unless parser.valid? raise InvalidInput, "'#{notation}' is not a valid input" end cycle = registry.for_notation_id(parser.kind).new(notation, parser:) return cycle if parser.active? Cycles::Dormant.new(cycle, parser:) end |
.handles(kind, notation: nil, periods: [], volume_only: false) ⇒ Object
Declare what kind of cycle this class handles, and register it.
Registration is a consequence of declaring, so the two can never disagree, and subclassing Cycle for any other reason — an abstract intermediate, a test double — registers nothing.
An application can declare its own kind this way and the parser will recognise its notation, because the pattern is built from what is registered.
169 170 171 172 173 174 175 |
# File 'lib/sof/cycle.rb', line 169 def handles(kind, notation: nil, periods: [], volume_only: false) @kind = kind @notation_id = notation @valid_periods = periods @volume_only = volume_only register(self) end |
.handles?(sym) ⇒ Boolean
142 143 144 |
# File 'lib/sof/cycle.rb', line 142 def handles?(sym) kind.to_s == sym.to_s end |
.legend ⇒ Hash
Return a legend explaining all notation components
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/sof/cycle.rb', line 98 def legend { "quantity" => { "V" => { description: "Volume - the number of times something should occur", examples: ["V1L1D - once in the prior 1 day", "V3L3D - three times in the prior 3 days", "V10L10D - ten times in the prior 10 days"] } }, "kind" => build_kind_legend, "period" => build_period_legend, "date" => { "F" => { description: "From - specifies the anchor date for Within cycles", examples: ["F2024-01-01 - from January 1, 2024", "F2024-12-31 - from December 31, 2024"] } } } end |
.load(hash) ⇒ Object
Return a Cycle object from a hash
28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# File 'lib/sof/cycle.rb', line 28 def load(hash) symbolized_hash = hash.symbolize_keys cycle_class = class_for_kind(symbolized_hash[:kind]) unless cycle_class.valid_periods.empty? cycle_class.validate_period( TimeSpan.notation_id_from_name(symbolized_hash[:period]) ) end Cycle.for notation(symbolized_hash) rescue TimeSpan::InvalidPeriod => exc raise InvalidPeriod, exc. end |
.notation(hash) ⇒ String
Retun a notation string from a hash
47 48 49 50 51 52 53 54 55 56 57 58 |
# File 'lib/sof/cycle.rb', line 47 def notation(hash) volume_notation = "V#{hash.fetch(:volume) { 1 }}" return volume_notation if hash[:kind].nil? || hash[:kind].to_sym == :volume_only cycle_class = class_for_kind(hash[:kind].to_sym) [ volume_notation, cycle_class.notation_id, TimeSpan.notation(hash.slice(:period, :period_count)), hash.fetch(:from, nil) ].compact.join end |
.recurring? ⇒ Boolean
129 |
# File 'lib/sof/cycle.rb', line 129 def recurring? = raise "#{name} must implement #{__method__}" |
.register(cycle_class) ⇒ Class
Register a handler that does not inherit from Cycle. Inheriting and
declaring with handles is the easier path, because it supplies the
behaviour Cycle.for goes on to use; this is for a class that would
rather implement that itself.
The class must answer, at the class level:
kind the kind it handles, e.g. :lookback
notation_id the notation opening it, e.g. "L", or nil
valid_periods period keys it accepts, e.g. %w[D W M Y]
volume_only? whether it carries volume alone
handles?(kind) whether it answers to a kind
recurring? whether the window repeats
dormant_capable? whether it can be written without a from date
validate_period(key)
description, examples (for the legend)
and instances are built as new(notation, parser:).
Registering displaces any handler already answering to the same kind or notation id, so this replaces a built-in as readily as it adds one.
201 |
# File 'lib/sof/cycle.rb', line 201 def register(cycle_class) = registry.register(cycle_class) |
.unregister(cycle_class) ⇒ Object
Drop a handler, undoing register or a handles declaration.
204 |
# File 'lib/sof/cycle.rb', line 204 def unregister(cycle_class) = registry.unregister(cycle_class) |
.validate_period(period) ⇒ Object
Raises an error if the given period isn't in the list of valid periods.
135 136 137 138 139 140 |
# File 'lib/sof/cycle.rb', line 135 def validate_period(period) raise InvalidPeriod, <<~ERR.squish unless valid_periods.include?(period) Invalid period value of '#{period}' provided. Valid periods are: #{valid_periods.join(", ")} ERR end |
.volume_only? ⇒ Boolean
125 |
# File 'lib/sof/cycle.rb', line 125 def volume_only? = @volume_only |
Instance Method Details
#==(other) ⇒ Object
Cycles are considered equal if their hash representations are equal
292 |
# File 'lib/sof/cycle.rb', line 292 def ==(other) = to_h == other.to_h |
#as_json ⇒ Object
381 |
# File 'lib/sof/cycle.rb', line 381 def as_json(...) = notation |
#considered_dates(completion_dates, anchor: Date.current) ⇒ Object
340 341 342 |
# File 'lib/sof/cycle.rb', line 340 def considered_dates(completion_dates, anchor: Date.current) covered_dates(completion_dates, anchor:).max_by(volume) { it } end |
#cover?(date, anchor: Date.current) ⇒ Boolean
350 351 352 |
# File 'lib/sof/cycle.rb', line 350 def cover?(date, anchor: Date.current) range(anchor).cover?(date) end |
#covered_dates(dates, anchor: Date.current) ⇒ Object
344 345 346 347 348 |
# File 'lib/sof/cycle.rb', line 344 def covered_dates(dates, anchor: Date.current) dates.select do |date| cover?(date, anchor:) end end |
#dormant_capable? ⇒ Boolean
Whether this kind of cycle can be written without a from date, and so
can be anchored later. Answers on the instance so a dormant cycle can
be asked too — cycle.class is Cycles::Dormant there, which knows
nothing of the kind it wraps.
303 |
# File 'lib/sof/cycle.rb', line 303 def dormant_capable? = self.class.dormant_capable? |
#expiration_of(_completion_dates, anchor: Date.current) ⇒ Object
361 |
# File 'lib/sof/cycle.rb', line 361 def expiration_of(_completion_dates, anchor: Date.current) = nil |
#extend_period(_ = nil) ⇒ Object
297 |
# File 'lib/sof/cycle.rb', line 297 def extend_period(_ = nil) = self |
#final_date(_anchor) ⇒ Object
Return the final date of the cycle
359 |
# File 'lib/sof/cycle.rb', line 359 def final_date(_anchor) = nil |
#from_data ⇒ Object
375 376 377 378 379 |
# File 'lib/sof/cycle.rb', line 375 def from_data return {} unless from {from: from} end |
#humanized_span ⇒ Object
356 |
# File 'lib/sof/cycle.rb', line 356 def humanized_span = [period_count, humanized_period].join(" ") |
#kind_inquiry ⇒ Object
272 |
# File 'lib/sof/cycle.rb', line 272 def kind_inquiry = ActiveSupport::StringInquirer.new(kind.to_s) |
#last_completed(dates) ⇒ Object
Return the most recent completion date from the supplied array of dates
295 |
# File 'lib/sof/cycle.rb', line 295 def last_completed(dates) = dates.compact.map(&:to_date).max |
#notation ⇒ Object
Return the cycle representation as a notation string
281 282 283 284 285 286 287 288 289 |
# File 'lib/sof/cycle.rb', line 281 def notation hash = to_h [ "V#{volume}", self.class.notation_id, time_span.notation, hash.fetch(:from, nil) ].compact.join end |
#range(anchor) ⇒ Object
354 |
# File 'lib/sof/cycle.rb', line 354 def range(anchor) = start_date(anchor)..final_date(anchor) |
#reset_by(dates) ⇒ Cycle
The cycle restarted from the latest of dates past its current anchor.
A recurring, dormant-capable window (E and I) is reset by the act that satisfies it — "complete 1 by that date to reset the cycle". Nothing in the notation does that on its own, so this used to fall to each consuming app.
Dates on or before the current anchor are ignored: a back-dated act must not drag a forward-running window backwards. Every other kind returns itself — a lookback window already slides, a calendar window is pinned to the calendar, a one-shot window does not repeat, and an un-anchored one has no window to move until it is activated.
324 325 326 327 328 329 330 |
# File 'lib/sof/cycle.rb', line 324 def reset_by(dates) return self unless recurring? && dormant_capable? return self if from_date.nil? reset = reset_date(dates) reset ? Cycle.for(activated_notation(reset)) : self end |
#satisfied_by?(completion_dates, anchor: Date.current) ⇒ Boolean
From the supplied anchor date, are there enough in-window completions to satisfy the cycle?
336 337 338 |
# File 'lib/sof/cycle.rb', line 336 def satisfied_by?(completion_dates, anchor: Date.current) covered_dates(completion_dates, anchor:).size >= volume end |
#to_h ⇒ Object
365 366 367 368 369 370 371 372 373 |
# File 'lib/sof/cycle.rb', line 365 def to_h { kind:, volume:, period:, period_count:, **from_data } end |
#validate_period ⇒ Object
274 275 276 277 278 |
# File 'lib/sof/cycle.rb', line 274 def validate_period return if valid_periods.empty? self.class.validate_period(period_key) end |
#volume_to_delay_expiration(_completion_dates, anchor:) ⇒ Object
363 |
# File 'lib/sof/cycle.rb', line 363 def volume_to_delay_expiration(_completion_dates, anchor:) = 0 |