Class: SOF::Cycle

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/sof/cycle.rb,
lib/sof/cycle/version.rb

Defined Under Namespace

Classes: InvalidInput, InvalidKind, InvalidPeriod

Constant Summary collapse

VERSION =
"0.1.14"

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(notation, parser: Parser.new(notation)) ⇒ Cycle

Returns a new instance of Cycle.

Raises:



209
210
211
212
213
214
215
216
217
# File 'lib/sof/cycle.rb', line 209

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

.kindObject (readonly)

Returns the value of attribute kind.



131
132
133
# File 'lib/sof/cycle.rb', line 131

def kind
  @kind
end

.notation_idObject (readonly)

Returns the value of attribute notation_id.



131
132
133
# File 'lib/sof/cycle.rb', line 131

def notation_id
  @notation_id
end

.valid_periodsObject (readonly)

Returns the value of attribute valid_periods.



131
132
133
# File 'lib/sof/cycle.rb', line 131

def valid_periods
  @valid_periods
end

Instance Attribute Details

#parserObject (readonly)

Returns the value of attribute parser.



219
220
221
# File 'lib/sof/cycle.rb', line 219

def parser
  @parser
end

Class Method Details

.class_for_kind(sym) ⇒ Object

Return the class handling the kind

Examples:

class_for_kind(:lookback)

Parameters:

  • sym (Symbol)

    symbol matching the kind of Cycle class



98
99
100
101
102
# File 'lib/sof/cycle.rb', line 98

def class_for_kind(sym)
  Cycle.cycle_handlers.find do |klass|
    klass.handles?(sym)
  end || raise(InvalidKind, "':#{sym}' is not a valid kind of Cycle")
end

.class_for_notation_id(notation_id) ⇒ Object

Return the appropriate class for the give notation id

Examples:

class_for_notation_id('L')

Parameters:

  • notation (String)

    notation id matching the kind of Cycle class



87
88
89
90
91
# File 'lib/sof/cycle.rb', line 87

def class_for_notation_id(notation_id)
  Cycle.cycle_handlers.find do |klass|
    klass.notation_id == notation_id
  end || raise(InvalidKind, "'#{notation_id}' is not a valid kind of #{name}")
end

.cycle_handlersObject



153
154
155
# File 'lib/sof/cycle.rb', line 153

def cycle_handlers
  @cycle_handlers ||= Set.new
end

.dormant_capable?Boolean

Returns:

  • (Boolean)


134
# File 'lib/sof/cycle.rb', line 134

def dormant_capable? = false

.dump(cycle_or_string) ⇒ Object

Turn a cycle or notation string into a hash



18
19
20
21
22
23
24
# File 'lib/sof/cycle.rb', line 18

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

Examples:

Cycle.for('V2C1Y)

Parameters:

  • notation (String)

    a string notation representing a Cycle

Returns:

  • (Cycle)

    a Cycle object representing the provide string notation



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/sof/cycle.rb', line 65

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 = Cycle.cycle_handlers.find do |klass|
    parser.parses?(klass.notation_id)
  end.new(notation, parser:)
  return cycle if parser.active?

  Cycles::Dormant.new(cycle, parser:)
end

.handles?(sym) ⇒ Boolean

Returns:

  • (Boolean)


149
150
151
# File 'lib/sof/cycle.rb', line 149

def handles?(sym)
  kind.to_s == sym.to_s
end

.inherited(klass) ⇒ Object



157
158
159
# File 'lib/sof/cycle.rb', line 157

def inherited(klass)
  Cycle.cycle_handlers << klass
end

.legendHash

Return a legend explaining all notation components

Returns:

  • (Hash)

    hash with notation components organized by category



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/sof/cycle.rb', line 107

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



27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/sof/cycle.rb', line 27

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.message
end

.notation(hash) ⇒ String

Retun a notation string from a hash

Parameters:

  • hash (Hash)

    hash of data for a valid Cycle

Returns:

  • (String)

    string representation of a Cycle



46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/sof/cycle.rb', line 46

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

Returns:

  • (Boolean)


136
# File 'lib/sof/cycle.rb', line 136

def recurring? = raise "#{name} must implement #{__method__}"

.validate_period(period) ⇒ Object

Raises an error if the given period isn’t in the list of valid periods.

Parameters:

  • period (String)

    period matching the class valid periods

Raises:



142
143
144
145
146
147
# File 'lib/sof/cycle.rb', line 142

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

Returns:

  • (Boolean)


132
# File 'lib/sof/cycle.rb', line 132

def volume_only? = @volume_only

Instance Method Details

#==(other) ⇒ Object

Cycles are considered equal if their hash representations are equal



249
# File 'lib/sof/cycle.rb', line 249

def ==(other) = to_h == other.to_h

#as_jsonObject



305
# File 'lib/sof/cycle.rb', line 305

def as_json(...) = notation

#considered_dates(completion_dates, anchor: Date.current) ⇒ Object



264
265
266
# File 'lib/sof/cycle.rb', line 264

def considered_dates(completion_dates, anchor: Date.current)
  covered_dates(completion_dates, anchor:).max_by(volume) { it }
end

#cover?(date, anchor: Date.current) ⇒ Boolean

Returns:

  • (Boolean)


274
275
276
# File 'lib/sof/cycle.rb', line 274

def cover?(date, anchor: Date.current)
  range(anchor).cover?(date)
end

#covered_dates(dates, anchor: Date.current) ⇒ Object



268
269
270
271
272
# File 'lib/sof/cycle.rb', line 268

def covered_dates(dates, anchor: Date.current)
  dates.select do |date|
    cover?(date, anchor:)
  end
end

#expiration_of(_completion_dates, anchor: Date.current) ⇒ Object



285
# File 'lib/sof/cycle.rb', line 285

def expiration_of(_completion_dates, anchor: Date.current) = nil

#extend_period(_ = nil) ⇒ Object



254
# File 'lib/sof/cycle.rb', line 254

def extend_period(_ = nil) = self

#final_date(_anchor) ⇒ Object

Return the final date of the cycle



283
# File 'lib/sof/cycle.rb', line 283

def final_date(_anchor) = nil

#from_dataObject



299
300
301
302
303
# File 'lib/sof/cycle.rb', line 299

def from_data
  return {} unless from

  {from: from}
end

#humanized_spanObject



280
# File 'lib/sof/cycle.rb', line 280

def humanized_span = [period_count, humanized_period].join(" ")

#kind_inquiryObject



229
# File 'lib/sof/cycle.rb', line 229

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



252
# File 'lib/sof/cycle.rb', line 252

def last_completed(dates) = dates.compact.map(&:to_date).max

#notationObject

Return the cycle representation as a notation string



238
239
240
241
242
243
244
245
246
# File 'lib/sof/cycle.rb', line 238

def notation
  hash = to_h
  [
    "V#{volume}",
    self.class.notation_id,
    time_span.notation,
    hash.fetch(:from, nil)
  ].compact.join
end

#range(anchor) ⇒ Object



278
# File 'lib/sof/cycle.rb', line 278

def range(anchor) = start_date(anchor)..final_date(anchor)

#satisfied_by?(completion_dates, anchor: Date.current) ⇒ Boolean

From the supplied anchor date, are there enough in-window completions to satisfy the cycle?

Returns:

  • (Boolean)

    true if the cycle is satisfied, false otherwise



260
261
262
# File 'lib/sof/cycle.rb', line 260

def satisfied_by?(completion_dates, anchor: Date.current)
  covered_dates(completion_dates, anchor:).size >= volume
end

#to_hObject



289
290
291
292
293
294
295
296
297
# File 'lib/sof/cycle.rb', line 289

def to_h
  {
    kind:,
    volume:,
    period:,
    period_count:,
    **from_data
  }
end

#validate_periodObject



231
232
233
234
235
# File 'lib/sof/cycle.rb', line 231

def validate_period
  return if valid_periods.empty?

  self.class.validate_period(period_key)
end

#volume_to_delay_expiration(_completion_dates, anchor:) ⇒ Object



287
# File 'lib/sof/cycle.rb', line 287

def volume_to_delay_expiration(_completion_dates, anchor:) = 0