recurify 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|