recurify 0.0.0 → 0.0.1

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: 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