recurify 0.0.1 → 0.0.2
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/lib/recurify/rule.rb +30 -51
- data/lib/recurify/rule_analysis.rb +105 -1
- data/lib/recurify/rule_translation.rb +67 -13
- data/lib/recurify/rule_validation.rb +36 -0
- data/lib/recurify/version.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f67467f617bc9f0301b2adea071f0399f6764157
|
4
|
+
data.tar.gz: b1406f9990dc5ce45a8b176b623f7ade80d0c2de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2866e6d97a2077b9161075adb1212aa83c6524f06a241b3803dd069c4c9c8a8c2e1d897834ffa612482689c662db88518fbf201174e0733d484320ae87b661c2
|
7
|
+
data.tar.gz: 41175f2ce891a1dbc2e3b522ebdba54f0d10cacfbbed277fd2454fe6ab4776cd2ccf36a6d9f49f0592c5322c12561b01791ce559a5170426187581aa2f18b107
|
data/lib/recurify/rule.rb
CHANGED
@@ -1,31 +1,40 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
require_relative 'rule_validation'
|
1
4
|
require_relative 'rule_analysis'
|
2
5
|
require_relative 'rule_translation'
|
3
6
|
|
4
7
|
module Recurify
|
5
8
|
# Represents a Recurrence Rule. Note that +Rule+ objects are *immutable* once
|
6
9
|
# they are instantiated (i.e. they are value objects).
|
7
|
-
#
|
8
|
-
# @!attribute [r] frequency
|
9
|
-
# @return [String]
|
10
|
-
# @!attribute [r] interval
|
11
|
-
# @return [Fixnum]
|
12
|
-
# @!attribute [r] count
|
13
|
-
# @return [Fixnum, nil]
|
14
|
-
# @!attribute [r] starts_on
|
15
|
-
# @return [Date]
|
16
|
-
# @!attribute [r] ends_on
|
17
|
-
# @return [Date, nil]
|
18
10
|
class Rule
|
11
|
+
include RuleValidation
|
19
12
|
include RuleAnalysis
|
20
13
|
include RuleTranslation
|
21
14
|
|
22
15
|
BASE_FREQUENCIES = %w(daily monthly).freeze
|
23
16
|
SUGAR_FREQUENCIES = %w(weekly quarterly yearly).freeze
|
24
17
|
SUPPORTED_FREQUENCIES = (BASE_FREQUENCIES + SUGAR_FREQUENCIES).freeze
|
25
|
-
MIN_INTERVAL = 1
|
26
|
-
MIN_COUNT = 1
|
27
18
|
|
28
|
-
attr_reader :frequency
|
19
|
+
attr_reader :frequency
|
20
|
+
# @!attribute [r] frequency
|
21
|
+
# @return [String]
|
22
|
+
|
23
|
+
attr_reader :interval
|
24
|
+
# @!attribute [r] interval
|
25
|
+
# @return [Fixnum]
|
26
|
+
|
27
|
+
attr_reader :count
|
28
|
+
# @!attribute [r] count
|
29
|
+
# @return [Fixnum, nil]
|
30
|
+
|
31
|
+
attr_reader :starts_on
|
32
|
+
# @!attribute [r] starts_on
|
33
|
+
# @return [Date]
|
34
|
+
|
35
|
+
attr_reader :ends_on
|
36
|
+
# @!attribute [r] ends_on
|
37
|
+
# @return [Date, nil]
|
29
38
|
|
30
39
|
# Returns a new instance of +Rule+.
|
31
40
|
#
|
@@ -87,46 +96,16 @@ module Recurify
|
|
87
96
|
{
|
88
97
|
frequency: SUPPORTED_FREQUENCIES.first,
|
89
98
|
interval: MIN_INTERVAL,
|
90
|
-
|
91
|
-
starts_on: Date.today,
|
92
|
-
ends_on: nil
|
99
|
+
starts_on: Date.today
|
93
100
|
}
|
94
101
|
end
|
95
102
|
|
96
|
-
def attributes=(
|
97
|
-
@frequency =
|
98
|
-
@interval
|
99
|
-
@count
|
100
|
-
@starts_on =
|
101
|
-
@ends_on
|
102
|
-
end
|
103
|
-
|
104
|
-
# @return [void]
|
105
|
-
def validate!
|
106
|
-
validate_frequency!
|
107
|
-
validate_interval!
|
108
|
-
validate_count!
|
109
|
-
end
|
110
|
-
|
111
|
-
# @raise [InvalidRuleFrequency]
|
112
|
-
# @return [void]
|
113
|
-
def validate_frequency!
|
114
|
-
return if SUPPORTED_FREQUENCIES.include?(frequency)
|
115
|
-
fail InvalidRuleFrequency, "'#{frequency}' is not supported"
|
116
|
-
end
|
117
|
-
|
118
|
-
# @raise [InvalidRuleInterval]
|
119
|
-
# @return [void]
|
120
|
-
def validate_interval!
|
121
|
-
return if interval >= MIN_INTERVAL
|
122
|
-
fail InvalidRuleInterval, "'#{interval}' is not supported"
|
123
|
-
end
|
124
|
-
|
125
|
-
# @raise [InvalidRuleCount]
|
126
|
-
# @return [void]
|
127
|
-
def validate_count!
|
128
|
-
return if count.nil? || count >= MIN_COUNT
|
129
|
-
fail InvalidRuleCount, "'#{count}' is not supported"
|
103
|
+
def attributes=(attrs)
|
104
|
+
@frequency = attrs[:frequency].to_s
|
105
|
+
@interval = attrs[:interval].to_i
|
106
|
+
@count = attrs[:count].nil? ? nil : attrs[:count].to_i
|
107
|
+
@starts_on = attrs[:starts_on].to_date
|
108
|
+
@ends_on = attrs[:ends_on].nil? ? nil : attrs[:ends_on].to_date
|
130
109
|
end
|
131
110
|
end
|
132
111
|
end
|
@@ -27,7 +27,9 @@ module Recurify
|
|
27
27
|
#
|
28
28
|
# @retrun [Boolean]
|
29
29
|
def finite?
|
30
|
-
|
30
|
+
# @todo Checking of the #count is not needed anymore, once #normalize
|
31
|
+
# supports minifying of #ends_on.
|
32
|
+
!count.nil? || !normalized_ends_on.nil?
|
31
33
|
end
|
32
34
|
|
33
35
|
# Returns +true+ if +self+ is infinite. By definition, a +Rule+ is infinite
|
@@ -39,5 +41,107 @@ module Recurify
|
|
39
41
|
def infinite?
|
40
42
|
!finite?
|
41
43
|
end
|
44
|
+
|
45
|
+
# Returns +true+ if +self+ starts before or on +upper+. If +upper == nil+,
|
46
|
+
# it is interpreted as "positive infinity". In that case, +#starts_before?+
|
47
|
+
# always returns +true+, because all +Rule+ objects must have a finite
|
48
|
+
# +#starts_on+ attribute.
|
49
|
+
#
|
50
|
+
# @param upper [#to_date, nil]
|
51
|
+
#
|
52
|
+
# @return [Boolean]
|
53
|
+
def starts_before?(upper)
|
54
|
+
upper.nil? || starts_on <= upper.to_date
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns +true+ if +self+ starts after or on +lower+. If +lower == nil+, it
|
58
|
+
# is interpreted as "negative infinity". In that case, +#starts_after?+
|
59
|
+
# always returns +true+, because all +Rule+ objects must have a finite
|
60
|
+
# +#starts_on+ attribute.
|
61
|
+
#
|
62
|
+
# @param lower [#to_date, nil]
|
63
|
+
#
|
64
|
+
# @return [Boolean]
|
65
|
+
def starts_after?(lower)
|
66
|
+
lower.nil? || starts_on >= lower.to_date
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns +true+ if +self+ starts after +lower+ *and* starts before
|
70
|
+
# +upper+ (including boundaries).
|
71
|
+
#
|
72
|
+
# @see #starts_before?
|
73
|
+
# @see #starts_after?
|
74
|
+
#
|
75
|
+
# @param lower [#to_date, nil]
|
76
|
+
# @param upper [#to_date, nil]
|
77
|
+
#
|
78
|
+
# @return [Boolean]
|
79
|
+
def starts_between?(lower: nil, upper: nil)
|
80
|
+
starts_after?(lower) && starts_before?(upper)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns +true+ if +self+ ends before or on +upper+. If +upper == nil+, it
|
84
|
+
# is interpreted as "positive infinity". In that case, +#ends_before?+
|
85
|
+
# always returns +true+.
|
86
|
+
#
|
87
|
+
# @param upper [#to_date, nil]
|
88
|
+
#
|
89
|
+
# @return [Boolean]
|
90
|
+
def ends_before?(upper)
|
91
|
+
false ||
|
92
|
+
# Case (1): [anything] <= 'infinite' ==> true
|
93
|
+
upper.nil? ||
|
94
|
+
# Case (2): 'finite' <= 'finite' ==> depends on actual comparison
|
95
|
+
!normalized_ends_on.nil? && normalized_ends_on <= upper.to_date
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns +true+ if +self+ ends after or on +lower+. If +lower == nil+, it
|
99
|
+
# is interpreted as "negative infinity". In that case, +#ends_after?+ always
|
100
|
+
# returns +true+.
|
101
|
+
#
|
102
|
+
# @param lower [#to_date, nil]
|
103
|
+
#
|
104
|
+
# @return [Boolean]
|
105
|
+
def ends_after?(lower)
|
106
|
+
false ||
|
107
|
+
# Case (1): 'infinite' >= [anything] ==> true
|
108
|
+
normalized_ends_on.nil? ||
|
109
|
+
# Case (2): 'finite' >= 'infinite' ==> true
|
110
|
+
lower.nil? ||
|
111
|
+
# Case (3): 'finite' >= 'finite' ==> depends on actual comparison
|
112
|
+
normalized_ends_on >= lower.to_date
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns +true+ if +self+ ends after +lower+ *and* ends before +upper+
|
116
|
+
# (including boundaries).
|
117
|
+
#
|
118
|
+
# @see #ends_before?
|
119
|
+
# @see #ends_after?
|
120
|
+
#
|
121
|
+
# @param lower [#to_date, nil]
|
122
|
+
# @param upper [#to_date, nil]
|
123
|
+
#
|
124
|
+
# @return [Boolean]
|
125
|
+
def ends_between?(lower: nil, upper: nil)
|
126
|
+
ends_after?(lower) && ends_before?(upper)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns +true+ if +self+ starts *and/or* ends between +lower+ and +upper+
|
130
|
+
# (including boundaries).
|
131
|
+
#
|
132
|
+
# @see #starts_between?
|
133
|
+
# @see #ends_between?
|
134
|
+
#
|
135
|
+
# @param lower [#to_date, nil]
|
136
|
+
# @param upper [#to_date, nil]
|
137
|
+
#
|
138
|
+
# @return [Boolean]
|
139
|
+
def overlaps?(lower: nil, upper: nil)
|
140
|
+
false ||
|
141
|
+
# Case (1): +lower+ <= starts_on <= +upper+ ==> true
|
142
|
+
starts_between?(lower: lower, upper: upper) ||
|
143
|
+
# Case (2): +lower+ <= normalized_ends_on <= +upper+ ==> true
|
144
|
+
ends_between?(lower: lower, upper: upper)
|
145
|
+
end
|
42
146
|
end
|
43
147
|
end
|
@@ -1,5 +1,9 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
1
3
|
module Recurify
|
2
4
|
module RuleTranslation # :nodoc:
|
5
|
+
extend Forwardable
|
6
|
+
|
3
7
|
NORMALIZATION_MATRIX = {
|
4
8
|
# Normalization is a no-op for base-frequencies ...
|
5
9
|
'daily' => { target_frequency: 'daily', interval_multiplier: 1 },
|
@@ -13,27 +17,27 @@ module Recurify
|
|
13
17
|
DENORMALIZATION_MATRIX = {
|
14
18
|
'daily' => [
|
15
19
|
# Applicable denormalizitions ordered by decreasing preferability ...
|
16
|
-
{ target_frequency: 'weekly',
|
17
|
-
{ target_frequency: 'daily',
|
20
|
+
{ target_frequency: 'weekly', interval_divisor: 7 },
|
21
|
+
{ target_frequency: 'daily', interval_divisor: 1 }
|
18
22
|
],
|
19
23
|
'weekly' => [
|
20
24
|
# Applicable denormalizitions ordered by decreasing preferability ...
|
21
|
-
{ target_frequency: 'weekly',
|
25
|
+
{ target_frequency: 'weekly', interval_divisor: 1 }
|
22
26
|
],
|
23
27
|
'monthly' => [
|
24
28
|
# Applicable denormalizitions ordered by decreasing preferability ...
|
25
|
-
{ target_frequency: 'yearly',
|
26
|
-
{ target_frequency: 'quarterly',
|
27
|
-
{ target_frequency: 'monthly',
|
29
|
+
{ target_frequency: 'yearly', interval_divisor: 12 },
|
30
|
+
{ target_frequency: 'quarterly', interval_divisor: 3 },
|
31
|
+
{ target_frequency: 'monthly', interval_divisor: 1 }
|
28
32
|
],
|
29
33
|
'quarterly' => [
|
30
34
|
# Applicable denormalizitions ordered by decreasing preferability ...
|
31
|
-
{ target_frequency: 'yearly',
|
32
|
-
{ target_frequency: 'quarterly',
|
35
|
+
{ target_frequency: 'yearly', interval_divisor: 4 },
|
36
|
+
{ target_frequency: 'quarterly', interval_divisor: 1 }
|
33
37
|
],
|
34
38
|
'yearly' => [
|
35
39
|
# Applicable denormalizitions ordered by decreasing preferability ...
|
36
|
-
{ target_frequency: 'yearly',
|
40
|
+
{ target_frequency: 'yearly', interval_divisor: 1 }
|
37
41
|
]
|
38
42
|
}.freeze
|
39
43
|
|
@@ -47,6 +51,9 @@ module Recurify
|
|
47
51
|
#
|
48
52
|
# Additional translations may be added in the future.
|
49
53
|
#
|
54
|
+
# @todo This method is not yet complete, because +#normalize+ should also
|
55
|
+
# minify the +#ends_on+ and +#count+ attributes (if possible).
|
56
|
+
#
|
50
57
|
# @return [Rule]
|
51
58
|
def normalize
|
52
59
|
@_normalized_rule ||= begin
|
@@ -59,6 +66,30 @@ module Recurify
|
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
69
|
+
# @!method normalized_frequency
|
70
|
+
# @see Rule#frequency
|
71
|
+
# @see #normalize
|
72
|
+
# @return [String]
|
73
|
+
def_delegator :normalize, :frequency, :normalized_frequency
|
74
|
+
|
75
|
+
# @!method normalized_interval
|
76
|
+
# @see Rule#interval
|
77
|
+
# @see #normalize
|
78
|
+
# @return [Fixnum]
|
79
|
+
def_delegator :normalize, :interval, :normalized_interval
|
80
|
+
|
81
|
+
# @!method normalized_count
|
82
|
+
# @see Rule#count
|
83
|
+
# @see #normalize
|
84
|
+
# @return [Fixnum, nil]
|
85
|
+
def_delegator :normalize, :count, :normalized_count
|
86
|
+
|
87
|
+
# @!method normalized_ends_on
|
88
|
+
# @see Rule#ends_on
|
89
|
+
# @see #normalize
|
90
|
+
# @return [Date, nil]
|
91
|
+
def_delegator :normalize, :ends_on, :normalized_ends_on
|
92
|
+
|
62
93
|
# Creates a new +Rule+, similar to +self+, but denormalized. Note that
|
63
94
|
# +#denormalized+ is idempotent.
|
64
95
|
#
|
@@ -80,19 +111,42 @@ module Recurify
|
|
80
111
|
|
81
112
|
substitute(
|
82
113
|
frequency: translate_with[:target_frequency],
|
83
|
-
interval: interval / translate_with[:
|
114
|
+
interval: interval / translate_with[:interval_divisor]
|
84
115
|
)
|
85
116
|
end
|
86
117
|
end
|
87
118
|
|
119
|
+
# @!method denormalized_frequency
|
120
|
+
# @see Rule#frequency
|
121
|
+
# @see #denormalize
|
122
|
+
# @return [String]
|
123
|
+
def_delegator :denormalize, :frequency, :denormalized_frequency
|
124
|
+
|
125
|
+
# @!method denormalized_interval
|
126
|
+
# @see Rule#interval
|
127
|
+
# @see #denormalize
|
128
|
+
# @return [Fixnum]
|
129
|
+
def_delegator :denormalize, :interval, :denormalized_interval
|
130
|
+
|
131
|
+
# @!method denormalized_count
|
132
|
+
# @see Rule#count
|
133
|
+
# @see #denormalize
|
134
|
+
# @return [Fixnum, nil]
|
135
|
+
def_delegator :denormalize, :count, :denormalized_count
|
136
|
+
|
137
|
+
# @!method denormalized_ends_on
|
138
|
+
# @see Rule#ends_on
|
139
|
+
# @see #denormalize
|
140
|
+
# @return [Date, nil]
|
141
|
+
def_delegator :denormalize, :ends_on, :denormalized_ends_on
|
142
|
+
|
88
143
|
private
|
89
144
|
|
90
145
|
# @see DENORMALIZATION_MATRIX
|
91
146
|
# @return [Hash<Symbol, Object>]
|
92
147
|
def find_best_denormalization
|
93
|
-
|
94
|
-
|
95
|
-
normalize.interval % contender[:interval_divsor] == 0
|
148
|
+
DENORMALIZATION_MATRIX[frequency].find do |contender|
|
149
|
+
normalize.interval % contender[:interval_divisor] == 0
|
96
150
|
end
|
97
151
|
end
|
98
152
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Recurify
|
2
|
+
module RuleValidation # :nodoc:
|
3
|
+
MIN_INTERVAL = 1
|
4
|
+
MIN_COUNT = 1
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
# @return [void]
|
9
|
+
def validate!
|
10
|
+
validate_frequency!
|
11
|
+
validate_interval!
|
12
|
+
validate_count!
|
13
|
+
end
|
14
|
+
|
15
|
+
# @raise [InvalidRuleFrequency]
|
16
|
+
# @return [void]
|
17
|
+
def validate_frequency!
|
18
|
+
return if Rule::SUPPORTED_FREQUENCIES.include?(frequency)
|
19
|
+
fail InvalidRuleFrequency, "'#{frequency}' is not supported"
|
20
|
+
end
|
21
|
+
|
22
|
+
# @raise [InvalidRuleInterval]
|
23
|
+
# @return [void]
|
24
|
+
def validate_interval!
|
25
|
+
return if interval >= MIN_INTERVAL
|
26
|
+
fail InvalidRuleInterval, "'#{interval}' is not supported"
|
27
|
+
end
|
28
|
+
|
29
|
+
# @raise [InvalidRuleCount]
|
30
|
+
# @return [void]
|
31
|
+
def validate_count!
|
32
|
+
return if count.nil? || count >= MIN_COUNT
|
33
|
+
fail InvalidRuleCount, "'#{count}' is not supported"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/recurify/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: recurify
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christoph Schiessl
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-12-
|
11
|
+
date: 2015-12-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: 3.3.3
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pry
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.10.3
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.10.3
|
69
83
|
description:
|
70
84
|
email: cs@proactive.cc
|
71
85
|
executables: []
|
@@ -79,6 +93,7 @@ files:
|
|
79
93
|
- lib/recurify/rule.rb
|
80
94
|
- lib/recurify/rule_analysis.rb
|
81
95
|
- lib/recurify/rule_translation.rb
|
96
|
+
- lib/recurify/rule_validation.rb
|
82
97
|
- lib/recurify/version.rb
|
83
98
|
homepage: https://github.com/cs/recurify
|
84
99
|
licenses:
|