recurify 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e49301641e6d8a01709443a6913e785fdc1e7526
4
- data.tar.gz: 6d88089a3785340cd9d31d922315ae7b8dc58bde
3
+ metadata.gz: a343e37a72dc1fc5319317532fe1af000e31c41e
4
+ data.tar.gz: d50087d4a127bce77547365be1b4b2a2f694b640
5
5
  SHA512:
6
- metadata.gz: 75785e0c9c2a40e4015b66a56e6bbe1a3a3a3ba9206d40bb9304702ffd453cd5f1fd1e7e4f11a000a62eaf36e50653ab2b8db873a526b1c8cacd8bc6728ac0b0
7
- data.tar.gz: c8885eaccc8bec7875498aa61352dc57a9338472c5d9d7c93f88626d3e419f82174e61a12e1509b75b5d8bab7b6cdea88a3661326c016372d8657ede46f166d2
6
+ metadata.gz: 93687456b41c7422e1731da5205dc337664e2ddf205b3210f80421b9a0c81940b185db9f05835b5422bfcf2d787fb9be99c5a90b2edeb44652a35993ddd3838c
7
+ data.tar.gz: 14ce0c66d4a0e9c77c2ee94fd50c59b0c73cd78f5c97239f30d103338444f60b5cb2254002a0b3582a131e2c79341d021152cebfd7231a6b923c0cff4b64d6b9
data/README.md CHANGED
@@ -1,6 +1,48 @@
1
1
  # Recurify
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/recurify.svg)](https://badge.fury.io/rb/recurify)
3
4
  [![Build Status](https://img.shields.io/travis/cs/recurify/master.svg)](https://travis-ci.org/cs/recurify)
5
+ [![Code Climate](https://codeclimate.com/github/cs/recurify/badges/gpa.svg)](https://codeclimate.com/github/cs/recurify)
6
+
7
+ **Recurify is very much "work in progress" and shoudn't be used yet.**
8
+
9
+ Recurify is a simple, light-weight, evaluator for Recurrence Rules. It takes a
10
+ Recurrence Rule as input and returns an [`Enumerable`][enumerable], containing
11
+ all [`Date`][date] objects resulting from this Rule in chronlogical order.
12
+
13
+ [enumerable]: http://ruby-doc.org/core-2.2.3/Enumerable.html
14
+ [date]: http://ruby-doc.org/stdlib-2.2.3/libdoc/date/rdoc/Date.html
15
+
16
+ ## Contributions
17
+
18
+ 1. **Everything has to be tested.** No excuses. Pending tests are forbidden.
19
+ 2. Ensure the project's test suite is still passing (run `rspec`).
20
+ 3. Ensure the code is still passing all rubocop checks (run `rubocop`).
21
+ 4. Document your changes.
22
+ 5. All commits messages have to be prefixed as follows:
23
+ * `F` - **F**eature commits with functional changes.
24
+ Example commit message: `F implement Rule#normalize`.
25
+ * `B` - **B**ugfix commits for fixing broken stuff.
26
+ Example commit message: `B consider Rule#count in Rule#==`
27
+ * `R` - **R**efactoring commits without functional changes.
28
+ Example commit message: `R memoize Rule#normalize for performance`.
29
+ * `C` - **C**hore commits are for everything else.
30
+ Example commit message: `C upgrade to latest version of rubocop`.
31
+
32
+ Prefixed commit messages may seem a bit strange at first. The intention is
33
+ simply to keep functional and non-functional changes clearly apart. Therefore,
34
+ `R` and `C` commits must **never** modify the code in `lib` and in `spec` at the
35
+ same time.
36
+
37
+ ## Sponsor
38
+
39
+ Recurify is sponsored by [Shore][shore]. They are currently [hiring][shore-jobs]
40
+ Ruby, JavaScript, iOS, and Android developers for their headquarters in
41
+ Munich/Germany as well as their top-notch development teams in Sofia/Bulgaria
42
+ and Madrid/Spain.
43
+
44
+ [shore]: http://www.shore.com
45
+ [shore-jobs]: http://www.shore.com/en/careers
4
46
 
5
47
  ## License
6
48
 
@@ -0,0 +1,6 @@
1
+ module Recurify
2
+ InvalidRuleError = Class.new(StandardError)
3
+ InvalidRuleFrequency = Class.new(InvalidRuleError)
4
+ InvalidRuleInterval = Class.new(InvalidRuleError)
5
+ InvalidRuleCount = Class.new(InvalidRuleError)
6
+ end
@@ -0,0 +1,132 @@
1
+ require_relative 'rule_analysis'
2
+ require_relative 'rule_translation'
3
+
4
+ module Recurify
5
+ # Represents a Recurrence Rule. Note that +Rule+ objects are *immutable* once
6
+ # 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
+ class Rule
19
+ include RuleAnalysis
20
+ include RuleTranslation
21
+
22
+ BASE_FREQUENCIES = %w(daily monthly).freeze
23
+ SUGAR_FREQUENCIES = %w(weekly quarterly yearly).freeze
24
+ SUPPORTED_FREQUENCIES = (BASE_FREQUENCIES + SUGAR_FREQUENCIES).freeze
25
+ MIN_INTERVAL = 1
26
+ MIN_COUNT = 1
27
+
28
+ attr_reader :frequency, :interval, :count, :starts_on, :ends_on
29
+
30
+ # Returns a new instance of +Rule+.
31
+ #
32
+ # @param attributes [Hash<Symbol,Object>] attributes for the new +Rule+
33
+ # @option attributes [#to_s] :frequency
34
+ # @option attributes [#to_i] :interval
35
+ # @option attributes [#to_i, nil] :count
36
+ # @option attributes [#to_date] :starts_on
37
+ # @option attributes [#to_date, nil] :ends_on
38
+ #
39
+ # @raise [InvalidRuleFrequency] if the provided +#frequency+ is not
40
+ # supported
41
+ # @raise [InvalidRuleInterval] if the provided +#interval+ is not supported
42
+ # @raise [InvalidRuleCount] if the provided +#count+ is not supported
43
+ def initialize(attributes = {})
44
+ self.attributes = self.class.default_attributes.merge(attributes)
45
+ validate!
46
+ end
47
+
48
+ # Creates a new instance of +Rule+ with substituted attributes. Attributes
49
+ # that haven't been specified are copied from +self+.
50
+ #
51
+ # @see #initialize
52
+ #
53
+ # @param substitutions [Hash<Symbol,Object>] for the the new +Rule+
54
+ # @return [Rule]
55
+ def substitute(substitutions = {})
56
+ self.class.new(attributes.merge(substitutions))
57
+ end
58
+
59
+ # Tests for equality with +other+.
60
+ #
61
+ # Two +Rule+ objects are considered to be equal, if and only if they both
62
+ # evaluate to the same same set of +Date+ objects.
63
+ #
64
+ # @param other [Rule]
65
+ # @return [Boolean]
66
+ def ==(other)
67
+ normalize.attributes == other.normalize.attributes
68
+ end
69
+
70
+ # Convert +self+ to a +Hash+, representing the same +Rule+. Note, that
71
+ # +#attributes+ is essentially the inverse of +#initialize+.
72
+ #
73
+ # @return [Hash<Symbol,Object>]
74
+ def attributes
75
+ @_attributes ||= {
76
+ frequency: frequency,
77
+ interval: interval,
78
+ count: count,
79
+ starts_on: starts_on,
80
+ ends_on: ends_on
81
+ }
82
+ end
83
+
84
+ private
85
+
86
+ def self.default_attributes
87
+ {
88
+ frequency: SUPPORTED_FREQUENCIES.first,
89
+ interval: MIN_INTERVAL,
90
+ count: nil,
91
+ starts_on: Date.today,
92
+ ends_on: nil
93
+ }
94
+ end
95
+
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"
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,43 @@
1
+ module Recurify
2
+ module RuleAnalysis # :nodoc:
3
+ # Returns +true+ if +self+ is normalized. By definition, a +Rule+ is
4
+ # normalized if and only if it is not denormalized.
5
+ #
6
+ # @see #denormalized?
7
+ #
8
+ # @return [Boolean]
9
+ def normalized?
10
+ Rule::BASE_FREQUENCIES.include?(frequency)
11
+ end
12
+
13
+ # Returns +true+ if +self+ is denormalized. By definition, a +Rule+ is
14
+ # denormalized if and only if it is not normalized.
15
+ #
16
+ # @see #normalized?
17
+ #
18
+ # @return [Boolean]
19
+ def denormalized?
20
+ !normalized?
21
+ end
22
+
23
+ # Returns +true+ if +self+ is finite. By definition, a +Rule+ is finite if
24
+ # and only if it is not infinite.
25
+ #
26
+ # @see #infinite?
27
+ #
28
+ # @retrun [Boolean]
29
+ def finite?
30
+ !count.nil? || !ends_on.nil?
31
+ end
32
+
33
+ # Returns +true+ if +self+ is infinite. By definition, a +Rule+ is infinite
34
+ # if and only if it is not finite.
35
+ #
36
+ # @see #finite?
37
+ #
38
+ # @return [Boolean]
39
+ def infinite?
40
+ !finite?
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,99 @@
1
+ module Recurify
2
+ module RuleTranslation # :nodoc:
3
+ NORMALIZATION_MATRIX = {
4
+ # Normalization is a no-op for base-frequencies ...
5
+ 'daily' => { target_frequency: 'daily', interval_multiplier: 1 },
6
+ 'monthly' => { target_frequency: 'monthly', interval_multiplier: 1 },
7
+ # But, it is more interesting for sugar-frequencies ...
8
+ 'weekly' => { target_frequency: 'daily', interval_multiplier: 7 },
9
+ 'quarterly' => { target_frequency: 'monthly', interval_multiplier: 3 },
10
+ 'yearly' => { target_frequency: 'monthly', interval_multiplier: 12 }
11
+ }.freeze
12
+
13
+ DENORMALIZATION_MATRIX = {
14
+ 'daily' => [
15
+ # Applicable denormalizitions ordered by decreasing preferability ...
16
+ { target_frequency: 'weekly', interval_divsor: 7 },
17
+ { target_frequency: 'daily', interval_divsor: 1 }
18
+ ],
19
+ 'weekly' => [
20
+ # Applicable denormalizitions ordered by decreasing preferability ...
21
+ { target_frequency: 'weekly', interval_divsor: 1 }
22
+ ],
23
+ 'monthly' => [
24
+ # 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 }
28
+ ],
29
+ 'quarterly' => [
30
+ # Applicable denormalizitions ordered by decreasing preferability ...
31
+ { target_frequency: 'yearly', interval_divsor: 4 },
32
+ { target_frequency: 'quarterly', interval_divsor: 1 }
33
+ ],
34
+ 'yearly' => [
35
+ # Applicable denormalizitions ordered by decreasing preferability ...
36
+ { target_frequency: 'yearly', interval_divsor: 1 }
37
+ ]
38
+ }.freeze
39
+
40
+ # Creates a new +Rule+, similar to +self+, but normalized. Note that
41
+ # +#normalize+ is idempotent.
42
+ #
43
+ # Normalization means that +Rule+s with +#frequency+ ...
44
+ # * ... "weekly" are translated to +Rule+s with +#frequency+ "daily".
45
+ # * ... "quarterly" are translated to +Rule+s with +#frequency+ "monthly".
46
+ # * ... "yearly" are translated to +Rule+s with +#frequency+ "monthly".
47
+ #
48
+ # Additional translations may be added in the future.
49
+ #
50
+ # @return [Rule]
51
+ def normalize
52
+ @_normalized_rule ||= begin
53
+ translate_with = NORMALIZATION_MATRIX[frequency]
54
+
55
+ substitute(
56
+ frequency: translate_with[:target_frequency],
57
+ interval: interval * translate_with[:interval_multiplier]
58
+ )
59
+ end
60
+ end
61
+
62
+ # Creates a new +Rule+, similar to +self+, but denormalized. Note that
63
+ # +#denormalized+ is idempotent.
64
+ #
65
+ # Denormalizion means that +Rule+s with +#frequency+ ...
66
+ # * ... "daily" are translated to +Rule+s with +#frequency+ "weekly".
67
+ # * ... "monthly" are translated to +Rule+s with +#frequency+ "yearly".
68
+ # * ... "monthly" are translated to +Rule+s with +#frequency+ "quarterly".
69
+ #
70
+ # Additional translations may be added in the future.
71
+ #
72
+ # Why +#denormalize+ at all? The idea is that denormalized +Rule+s are
73
+ # easier to parse for humans. For instance, "every 7th week" is easier to
74
+ # understand than "every 49th day".
75
+ #
76
+ # @return [Rule]
77
+ def denormalize
78
+ @_denormalized_rule ||= begin
79
+ translate_with = find_best_denormalization
80
+
81
+ substitute(
82
+ frequency: translate_with[:target_frequency],
83
+ interval: interval / translate_with[:interval_divsor]
84
+ )
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ # @see DENORMALIZATION_MATRIX
91
+ # @return [Hash<Symbol, Object>]
92
+ def find_best_denormalization
93
+ contenders = DENORMALIZATION_MATRIX[frequency]
94
+ contenders.find do |contender|
95
+ normalize.interval % contender[:interval_divsor] == 0
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,3 +1,3 @@
1
1
  module Recurify # :nodoc:
2
- VERSION = '0.0.0'.freeze
2
+ VERSION = '0.0.1'.freeze
3
3
  end
data/lib/recurify.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require_relative 'recurify/version'
2
+ require_relative 'recurify/errors'
3
+ require_relative 'recurify/rule'
2
4
 
3
5
  module Recurify # :nodoc:
4
6
  end
metadata CHANGED
@@ -1,15 +1,71 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recurify
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.0.1
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-11-22 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2015-12-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.4'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.35.1
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.35.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: yard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.8.7
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.8.7
55
+ - !ruby/object:Gem::Dependency
56
+ name: redcarpet
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.3.3
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.3.3
13
69
  description:
14
70
  email: cs@proactive.cc
15
71
  executables: []
@@ -19,13 +75,18 @@ files:
19
75
  - LICENSE
20
76
  - README.md
21
77
  - lib/recurify.rb
78
+ - lib/recurify/errors.rb
79
+ - lib/recurify/rule.rb
80
+ - lib/recurify/rule_analysis.rb
81
+ - lib/recurify/rule_translation.rb
22
82
  - lib/recurify/version.rb
23
83
  homepage: https://github.com/cs/recurify
24
84
  licenses:
25
85
  - MIT
26
86
  metadata: {}
27
87
  post_install_message:
28
- rdoc_options: []
88
+ rdoc_options:
89
+ - "--markup-provider=redcarpet"
29
90
  require_paths:
30
91
  - lib
31
92
  required_ruby_version: !ruby/object:Gem::Requirement