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.12"

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:



206
207
208
209
210
211
212
213
214
# File 'lib/sof/cycle.rb', line 206

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.



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

def kind
  @kind
end

.notation_idObject (readonly)

Returns the value of attribute notation_id.



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

def notation_id
  @notation_id
end

.valid_periodsObject (readonly)

Returns the value of attribute valid_periods.



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

def valid_periods
  @valid_periods
end

Instance Attribute Details

#parserObject (readonly)

Returns the value of attribute parser.



216
217
218
# File 'lib/sof/cycle.rb', line 216

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



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

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



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

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



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

def cycle_handlers
  @cycle_handlers ||= Set.new
end

.dump(cycle_or_string) ⇒ Object

Turn a cycle or notation string into a hash



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

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



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

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)


146
147
148
# File 'lib/sof/cycle.rb', line 146

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

.inherited(klass) ⇒ Object



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

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



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

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



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

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



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

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)


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

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:



139
140
141
142
143
144
# File 'lib/sof/cycle.rb', line 139

def validate_period(period)
  raise InvalidPeriod, "    Invalid period value of '\#{period}' provided. Valid periods are:\n    \#{valid_periods.join(\", \")}\n  ERR\nend\n".squish unless valid_periods.include?(period)

.volume_only?Boolean

Returns:

  • (Boolean)


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

def volume_only? = @volume_only

Instance Method Details

#==(other) ⇒ Object

Cycles are considered equal if their hash representations are equal



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

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

#as_jsonObject



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

def as_json(...) = notation

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



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

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)


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

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

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



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

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

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



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

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

#extend_period(_ = nil) ⇒ Object



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

def extend_period(_ = nil) = self

#final_date(_anchor) ⇒ Object

Return the final date of the cycle



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

def final_date(_anchor) = nil

#from_dataObject



295
296
297
298
299
# File 'lib/sof/cycle.rb', line 295

def from_data
  return {} unless from

  {from: from}
end

#humanized_spanObject



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

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

#kind_inquiryObject



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

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



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

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

#notationObject

Return the cycle representation as a notation string



234
235
236
237
238
239
240
241
242
# File 'lib/sof/cycle.rb', line 234

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

#range(anchor) ⇒ Object



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

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



256
257
258
# File 'lib/sof/cycle.rb', line 256

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

#to_hObject



285
286
287
288
289
290
291
292
293
# File 'lib/sof/cycle.rb', line 285

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

#validate_periodObject



227
228
229
230
231
# File 'lib/sof/cycle.rb', line 227

def validate_period
  return if valid_periods.empty?

  self.class.validate_period(period_key)
end

#volume_to_delay_expiration(_completion_dates, anchor:) ⇒ Object



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

def volume_to_delay_expiration(_completion_dates, anchor:) = 0