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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a343e37a72dc1fc5319317532fe1af000e31c41e
4
- data.tar.gz: d50087d4a127bce77547365be1b4b2a2f694b640
3
+ metadata.gz: f67467f617bc9f0301b2adea071f0399f6764157
4
+ data.tar.gz: b1406f9990dc5ce45a8b176b623f7ade80d0c2de
5
5
  SHA512:
6
- metadata.gz: 93687456b41c7422e1731da5205dc337664e2ddf205b3210f80421b9a0c81940b185db9f05835b5422bfcf2d787fb9be99c5a90b2edeb44652a35993ddd3838c
7
- data.tar.gz: 14ce0c66d4a0e9c77c2ee94fd50c59b0c73cd78f5c97239f30d103338444f60b5cb2254002a0b3582a131e2c79341d021152cebfd7231a6b923c0cff4b64d6b9
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, :interval, :count, :starts_on, :ends_on
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
- count: nil,
91
- starts_on: Date.today,
92
- ends_on: nil
99
+ starts_on: Date.today
93
100
  }
94
101
  end
95
102
 
96
- def attributes=(attributes)
97
- @frequency = attributes[:frequency].to_s
98
- @interval = attributes[:interval].to_i
99
- @count = attributes[:count].nil? ? nil : attributes[:count].to_i
100
- @starts_on = attributes[:starts_on].to_date
101
- @ends_on = attributes[:ends_on].nil? ? nil : attributes[:ends_on].to_date
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
- !count.nil? || !ends_on.nil?
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', interval_divsor: 7 },
17
- { target_frequency: 'daily', interval_divsor: 1 }
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', interval_divsor: 1 }
25
+ { target_frequency: 'weekly', interval_divisor: 1 }
22
26
  ],
23
27
  'monthly' => [
24
28
  # Applicable denormalizitions ordered by decreasing preferability ...
25
- { target_frequency: 'yearly', interval_divsor: 12 },
26
- { target_frequency: 'quarterly', interval_divsor: 3 },
27
- { target_frequency: 'monthly', interval_divsor: 1 }
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', interval_divsor: 4 },
32
- { target_frequency: 'quarterly', interval_divsor: 1 }
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', interval_divsor: 1 }
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[:interval_divsor]
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
- contenders = DENORMALIZATION_MATRIX[frequency]
94
- contenders.find do |contender|
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
@@ -1,3 +1,3 @@
1
1
  module Recurify # :nodoc:
2
- VERSION = '0.0.1'.freeze
2
+ VERSION = '0.0.2'.freeze
3
3
  end
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.1
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-01 00:00:00.000000000 Z
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: