sof-cycle 0.1.3 → 0.1.4
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/CHANGELOG.md +6 -6
- data/lib/sof/cycle/version.rb +1 -1
- data/lib/sof/cycle.rb +116 -113
- data/lib/sof/cycles/end_of.rb +3 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f9b1ae30ef2bd0d0fb9dbc08efb701b9813d997c216a22bf1df67fcbed7efe29
|
|
4
|
+
data.tar.gz: ac73534e2d48354722e929e41fccf0bae4a913466b181802daa8afe3daa9beae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6b4a0d60e48feed105e71c6410e4ba22152e88ed8502b43b32974e585cc1abce432867b0c635ada9376258df79d799d2f3f383e4499742f5023db623a4ac8ebe
|
|
7
|
+
data.tar.gz: e506926aed85332f02e8f5790ec752416679fe9a0c1542bd750f6097d204c4ed4237a866cc4552e67f612218ae58dbb819f68260de67e84fcf32b3d78351e0ba
|
data/CHANGELOG.md
CHANGED
|
@@ -5,14 +5,14 @@ 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.
|
|
8
|
+
## [0.1.4] - 2024-09-02
|
|
9
9
|
|
|
10
|
-
###
|
|
10
|
+
### Added
|
|
11
11
|
|
|
12
|
-
- `
|
|
12
|
+
- `Cycle#last_completed` to return the last completion given the cycle and an array of dates
|
|
13
13
|
|
|
14
|
-
## [0.1.
|
|
14
|
+
## [0.1.3] - 2024-09-01
|
|
15
15
|
|
|
16
|
-
###
|
|
16
|
+
### Fixed
|
|
17
17
|
|
|
18
|
-
- `
|
|
18
|
+
- `Cycles::EndOf` to have the correct behavior
|
data/lib/sof/cycle/version.rb
CHANGED
data/lib/sof/cycle.rb
CHANGED
|
@@ -11,142 +11,142 @@ module SOF
|
|
|
11
11
|
|
|
12
12
|
class InvalidKind < InvalidInput; end
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
class << self
|
|
15
|
+
# Turn a cycle or notation string into a hash
|
|
16
|
+
def dump(cycle_or_string)
|
|
17
|
+
if cycle_or_string.is_a? Cycle
|
|
18
|
+
cycle_or_string
|
|
19
|
+
else
|
|
20
|
+
Cycle.for(cycle_or_string)
|
|
21
|
+
end.to_h
|
|
22
|
+
end
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
# Return a Cycle object from a hash
|
|
25
|
+
def load(hash)
|
|
26
|
+
symbolized_hash = hash.symbolize_keys
|
|
27
|
+
cycle_class = class_for_kind(symbolized_hash[:kind])
|
|
20
28
|
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
unless cycle_class.valid_periods.empty?
|
|
30
|
+
cycle_class.validate_period(
|
|
31
|
+
TimeSpan.notation_id_from_name(symbolized_hash[:period])
|
|
32
|
+
)
|
|
33
|
+
end
|
|
23
34
|
|
|
24
|
-
|
|
35
|
+
Cycle.for notation(symbolized_hash)
|
|
36
|
+
rescue TimeSpan::InvalidPeriod => exc
|
|
37
|
+
raise InvalidPeriod, exc.message
|
|
38
|
+
end
|
|
25
39
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
# Retun a notation string from a hash
|
|
41
|
+
#
|
|
42
|
+
# @param hash [Hash] hash of data for a valid Cycle
|
|
43
|
+
# @return [String] string representation of a Cycle
|
|
44
|
+
def notation(hash)
|
|
45
|
+
volume_notation = "V#{hash.fetch(:volume) { 1 }}"
|
|
46
|
+
return volume_notation if hash[:kind].nil? || hash[:kind].to_sym == :volume_only
|
|
47
|
+
|
|
48
|
+
cycle_class = class_for_kind(hash[:kind].to_sym)
|
|
49
|
+
[
|
|
50
|
+
volume_notation,
|
|
51
|
+
cycle_class.notation_id,
|
|
52
|
+
TimeSpan.notation(hash.slice(:period, :period_count)),
|
|
53
|
+
hash.fetch(:from, nil)
|
|
54
|
+
].compact.join
|
|
55
|
+
end
|
|
32
56
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
# Return a Cycle object from a notation string
|
|
58
|
+
#
|
|
59
|
+
# @param notation [String] a string notation representing a Cycle
|
|
60
|
+
# @example
|
|
61
|
+
# Cycle.for('V2C1Y)
|
|
62
|
+
# @return [Cycle] a Cycle object representing the provide string notation
|
|
63
|
+
def for(notation)
|
|
64
|
+
return notation if notation.is_a? Cycle
|
|
65
|
+
return notation if notation.is_a? Cycles::Dormant
|
|
66
|
+
parser = Parser.new(notation)
|
|
67
|
+
unless parser.valid?
|
|
68
|
+
raise InvalidInput, "'#{notation}' is not a valid input"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
cycle = cycle_handlers.find do |klass|
|
|
72
|
+
parser.parses?(klass.notation_id)
|
|
73
|
+
end.new(notation, parser:)
|
|
74
|
+
return cycle if parser.active?
|
|
75
|
+
|
|
76
|
+
Cycles::Dormant.new(cycle, parser:)
|
|
77
|
+
end
|
|
41
78
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
79
|
+
# Return the appropriate class for the give notation id
|
|
80
|
+
#
|
|
81
|
+
# @param notation [String] notation id matching the kind of Cycle class
|
|
82
|
+
# @example
|
|
83
|
+
# class_for_notation_id('L')
|
|
84
|
+
#
|
|
85
|
+
def class_for_notation_id(notation_id)
|
|
86
|
+
cycle_handlers.find do |klass|
|
|
87
|
+
klass.notation_id == notation_id
|
|
88
|
+
end || raise(InvalidKind, "'#{notation_id}' is not a valid kind of #{name}")
|
|
89
|
+
end
|
|
46
90
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
91
|
+
# Return the class handling the kind
|
|
92
|
+
#
|
|
93
|
+
# @param sym [Symbol] symbol matching the kind of Cycle class
|
|
94
|
+
# @example
|
|
95
|
+
# class_for_kind(:lookback)
|
|
96
|
+
def class_for_kind(sym)
|
|
97
|
+
Cycle.cycle_handlers.find do |klass|
|
|
98
|
+
klass.handles?(sym)
|
|
99
|
+
end || raise(InvalidKind, "':#{sym}' is not a valid kind of Cycle")
|
|
51
100
|
end
|
|
52
101
|
|
|
53
|
-
|
|
54
|
-
rescue TimeSpan::InvalidPeriod => exc
|
|
55
|
-
raise InvalidPeriod, exc.message
|
|
56
|
-
end
|
|
102
|
+
def cycle_handlers = @cycle_handlers ||= Set.new
|
|
57
103
|
|
|
58
|
-
|
|
59
|
-
#
|
|
60
|
-
# @param hash [Hash] hash of data for a valid Cycle
|
|
61
|
-
# @return [String] string representation of a Cycle
|
|
62
|
-
def self.notation(hash)
|
|
63
|
-
volume_notation = "V#{hash.fetch(:volume) { 1 }}"
|
|
64
|
-
return volume_notation if hash[:kind].nil? || hash[:kind].to_sym == :volume_only
|
|
65
|
-
|
|
66
|
-
cycle_class = class_for_kind(hash[:kind].to_sym)
|
|
67
|
-
[
|
|
68
|
-
volume_notation,
|
|
69
|
-
cycle_class.notation_id,
|
|
70
|
-
TimeSpan.notation(hash.slice(:period, :period_count)),
|
|
71
|
-
hash.fetch(:from, nil)
|
|
72
|
-
].compact.join
|
|
73
|
-
end
|
|
104
|
+
def inherited(klass) = cycle_handlers << klass
|
|
74
105
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
# @param notation [String] a string notation representing a Cycle
|
|
78
|
-
# @example
|
|
79
|
-
# Cycle.for('V2C1Y)
|
|
80
|
-
# @return [Cycle] a Cycle object representing the provide string notation
|
|
81
|
-
def self.for(notation)
|
|
82
|
-
return notation if notation.is_a? Cycle
|
|
83
|
-
return notation if notation.is_a? Cycles::Dormant
|
|
84
|
-
parser = Parser.new(notation)
|
|
85
|
-
unless parser.valid?
|
|
86
|
-
raise InvalidInput, "'#{notation}' is not a valid input"
|
|
106
|
+
def handles?(sym)
|
|
107
|
+
sym && kind == sym.to_sym
|
|
87
108
|
end
|
|
88
109
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
110
|
+
@volume_only = false
|
|
111
|
+
@notation_id = nil
|
|
112
|
+
@kind = nil
|
|
113
|
+
@valid_periods = []
|
|
93
114
|
|
|
94
|
-
|
|
95
|
-
|
|
115
|
+
attr_reader :notation_id, :kind, :valid_periods
|
|
116
|
+
def volume_only? = @volume_only
|
|
96
117
|
|
|
97
|
-
|
|
98
|
-
#
|
|
99
|
-
# @param notation [String] notation id matching the kind of Cycle class
|
|
100
|
-
# @example
|
|
101
|
-
# class_for_notation_id('L')
|
|
102
|
-
#
|
|
103
|
-
def self.class_for_notation_id(notation_id)
|
|
104
|
-
cycle_handlers.find do |klass|
|
|
105
|
-
klass.notation_id == notation_id
|
|
106
|
-
end || raise(InvalidKind, "'#{notation_id}' is not a valid kind of #{name}")
|
|
107
|
-
end
|
|
118
|
+
def recurring? = raise "#{name} must implement #{__method__}"
|
|
108
119
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
120
|
+
# Raises an error if the given period isn't in the list of valid periods.
|
|
121
|
+
#
|
|
122
|
+
# @param period [String] period matching the class valid periods
|
|
123
|
+
# @raise [InvalidPeriod]
|
|
124
|
+
def validate_period(period)
|
|
125
|
+
raise InvalidPeriod, <<~ERR.squish unless valid_periods.include?(period)
|
|
126
|
+
Invalid period value of '#{period}' provided. Valid periods are:
|
|
127
|
+
#{valid_periods.join(", ")}
|
|
128
|
+
ERR
|
|
129
|
+
end
|
|
118
130
|
end
|
|
119
131
|
|
|
120
|
-
def
|
|
132
|
+
def initialize(notation, parser: Parser.new(notation))
|
|
133
|
+
@notation = notation
|
|
134
|
+
@parser = parser
|
|
135
|
+
validate_period
|
|
121
136
|
|
|
122
|
-
|
|
137
|
+
return if @parser.valid?
|
|
123
138
|
|
|
124
|
-
|
|
125
|
-
sym && kind == sym.to_sym
|
|
139
|
+
raise InvalidInput, "'#{notation}' is not a valid input"
|
|
126
140
|
end
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
@notation_id = nil
|
|
130
|
-
@kind = nil
|
|
131
|
-
@valid_periods = []
|
|
132
|
-
|
|
133
|
-
class << self
|
|
134
|
-
attr_reader :notation_id, :kind, :valid_periods
|
|
135
|
-
def volume_only? = @volume_only
|
|
136
|
-
|
|
137
|
-
def recurring? = raise "#{name} must implement #{__method__}"
|
|
138
|
-
end
|
|
142
|
+
attr_reader :parser
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
Invalid period value of '#{period}' provided. Valid periods are:
|
|
147
|
-
#{valid_periods.join(", ")}
|
|
148
|
-
ERR
|
|
149
|
-
end
|
|
144
|
+
delegate [:activated_notation, :volume, :from, :from_date, :time_span, :period,
|
|
145
|
+
:humanized_period, :period_key, :active?] => :@parser
|
|
146
|
+
delegate [:kind, :recurring?, :volume_only?, :valid_periods] => "self.class"
|
|
147
|
+
delegate [:period_count, :duration] => :time_span
|
|
148
|
+
delegate [:calendar?, :dormant?, :end_of?, :lookback?, :volume_only?,
|
|
149
|
+
:within?] => :kind_inquiry
|
|
150
150
|
|
|
151
151
|
def kind_inquiry = ActiveSupport::StringInquirer.new(kind.to_s)
|
|
152
152
|
|
|
@@ -162,6 +162,9 @@ module SOF
|
|
|
162
162
|
# Cycles are considered equal if their hash representations are equal
|
|
163
163
|
def ==(other) = to_h == other.to_h
|
|
164
164
|
|
|
165
|
+
# Return the most recent completion date from the supplied array of dates
|
|
166
|
+
def last_completed(dates) = dates.compact.map(&:to_date).max
|
|
167
|
+
|
|
165
168
|
# From the supplied anchor date, are there enough in-window completions to
|
|
166
169
|
# satisfy the cycle?
|
|
167
170
|
#
|
data/lib/sof/cycles/end_of.rb
CHANGED
|
@@ -24,6 +24,9 @@ module SOF
|
|
|
24
24
|
"#{volume}x by #{final_date.to_fs(:american)}"
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Always returns the from_date
|
|
28
|
+
def last_completed(_) = from_date.to_date
|
|
29
|
+
|
|
27
30
|
# Returns the expiration date for the cycle
|
|
28
31
|
#
|
|
29
32
|
# @param [nil] _ Unused parameter, maintained for compatibility
|
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.
|
|
4
|
+
version: 0.1.4
|
|
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-09-
|
|
11
|
+
date: 2024-09-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: forwardable
|