repeatable 0.2.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.git-blame-ignore-revs +13 -0
- data/.gitignore +0 -1
- data/.rspec +0 -1
- data/.standard.yml +3 -0
- data/.travis.yml +16 -6
- data/CHANGELOG.md +105 -0
- data/Gemfile +9 -3
- data/Gemfile.lock +76 -0
- data/README.md +74 -8
- data/Rakefile +5 -3
- data/lib/repeatable.rb +21 -17
- data/lib/repeatable/expression/base.rb +25 -0
- data/lib/repeatable/expression/biweekly.rb +28 -0
- data/lib/repeatable/expression/date.rb +37 -0
- data/lib/repeatable/expression/day_in_month.rb +8 -6
- data/lib/repeatable/expression/difference.rb +28 -0
- data/lib/repeatable/expression/exact_date.rb +17 -0
- data/lib/repeatable/expression/intersection.rb +5 -0
- data/lib/repeatable/expression/range_in_year.rb +10 -4
- data/lib/repeatable/expression/set.rb +10 -8
- data/lib/repeatable/expression/union.rb +5 -0
- data/lib/repeatable/expression/weekday.rb +1 -5
- data/lib/repeatable/expression/weekday_in_month.rb +22 -8
- data/lib/repeatable/last_date_of_month.rb +7 -0
- data/lib/repeatable/parser.rb +17 -2
- data/lib/repeatable/schedule.rb +21 -11
- data/lib/repeatable/version.rb +1 -1
- data/repeatable.gemspec +13 -18
- metadata +17 -65
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9f6976240c976a027e08e04dc46dff2a7a420894f089ae7fa8c9fded126bafc6
|
4
|
+
data.tar.gz: 4e0e0e4d45d72b543549904a9a6f4b041430b9808075ccb64a81583366a4e414
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2698038cb582f612642c1b2f9c595e3830ff42556fb89eef56d4bbf5e3c34396d318175699ecb74a2c589aefe861cc5c7e17a6bde0b4950b6939421fedcb1260
|
7
|
+
data.tar.gz: 6a98285ed9058b186651055e5afecae2b2abcb64ca0dbc488b92b76ff6a20da02d4ab1d96f204bd864b210a5d183fdff34db31b82d4b06595e0b7c16f2dc3dcf
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# These commits should be ignored by git-blame, making it much easier to step through the history
|
2
|
+
# of a line without hitting formatting commits (esp. large, update-all-the-files sorts of commits).
|
3
|
+
#
|
4
|
+
# For git-blame to ignore these revisions, you'll need to tell it about this file.
|
5
|
+
#
|
6
|
+
# You can either pass it to the command each time:
|
7
|
+
# git blame --ignore-rev-file=.git-blame-ignore-revs -L 12,14 lib/repeatable/conversions.rb
|
8
|
+
#
|
9
|
+
# Or you can set it as a config value, which will be used by all git-blame commands henceforth:
|
10
|
+
# git config blame.ignoreRevsFile .git-blame-ignore-revs
|
11
|
+
|
12
|
+
# Initial style fix after introducing Standard
|
13
|
+
80611e9be4fe12e6b9bdb937d37902c12ac3a53b
|
data/.gitignore
CHANGED
data/.rspec
CHANGED
data/.standard.yml
ADDED
data/.travis.yml
CHANGED
@@ -1,8 +1,18 @@
|
|
1
1
|
language: ruby
|
2
|
+
env:
|
3
|
+
global:
|
4
|
+
- COVERAGE=true
|
2
5
|
rvm:
|
3
|
-
- 2.
|
4
|
-
- 2.
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
- 2.5
|
7
|
+
- 2.6
|
8
|
+
- 2.7
|
9
|
+
- 3.0
|
10
|
+
before_install:
|
11
|
+
- yes | gem update --system --force
|
12
|
+
- gem install bundler
|
13
|
+
before_script:
|
14
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
15
|
+
- chmod +x ./cc-test-reporter
|
16
|
+
- ./cc-test-reporter before-build
|
17
|
+
after_script:
|
18
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
## CHANGELOG
|
2
|
+
|
3
|
+
### Unreleased
|
4
|
+
|
5
|
+
[Commits](https://github.com/molawson/repeatable/compare/v1.0.0...main)
|
6
|
+
|
7
|
+
### 1.0.0 (2021-03-25)
|
8
|
+
|
9
|
+
Breaking Changes:
|
10
|
+
|
11
|
+
* Change `Expression::DayInMonth` to take negative numbers instead of special `:last` symbol (or string)
|
12
|
+
* Stop including `Conversions` in top level namespace (must now be accessed via `Repeatable::Conversions::Date()` or by calling `include Repeatable::Conversions` in whatever namespace(s) it's needed)
|
13
|
+
* Require Ruby 2.5.0 or greater
|
14
|
+
|
15
|
+
Changes:
|
16
|
+
|
17
|
+
* Flatten nested `Expression::Union` elements during initialization ([@cmoel][])
|
18
|
+
* Flatten nested `Expression::Intersection` elements during initialization
|
19
|
+
|
20
|
+
Chores:
|
21
|
+
|
22
|
+
* Add support for Ruby 2.5, 2.6, 2.7, 3.0
|
23
|
+
* Introduce [standard](https://github.com/testdouble/standard) for code formatting
|
24
|
+
|
25
|
+
[Commits](https://github.com/molawson/repeatable/compare/v0.6.0...v1.0.0)
|
26
|
+
|
27
|
+
|
28
|
+
### 0.6.0 (2017-05-04)
|
29
|
+
|
30
|
+
Features:
|
31
|
+
|
32
|
+
* Add `Expression::Difference` for set differences between 2 schedules ([@danott][])
|
33
|
+
* Allow `Expression::DayInMonth` to take `:last` (or `'last'`) for its `day:` argument ([@PatrickLerner][])
|
34
|
+
* Allow `Expression::WeekdayInMonth` to take negative `count` argument for last, second-to-last, etc. of a given weekday ([@danielma][])
|
35
|
+
|
36
|
+
Bug Fixes:
|
37
|
+
|
38
|
+
* Fix `Expression::RangeInYear` to properly handle using `start_day` and `end_day` when `start_month == end_month` ([@danielma][])
|
39
|
+
|
40
|
+
[Commits](https://github.com/molawson/repeatable/compare/v0.5.0...v0.6.0)
|
41
|
+
|
42
|
+
### 0.5.0 (2016-01-27)
|
43
|
+
|
44
|
+
Features:
|
45
|
+
|
46
|
+
* Add `Expression::Biweekly` for "every other week" recurrence
|
47
|
+
|
48
|
+
[Commits](https://github.com/molawson/repeatable/compare/v0.4.0...v0.5.0)
|
49
|
+
|
50
|
+
### 0.4.0 (2015-06-29)
|
51
|
+
|
52
|
+
Features:
|
53
|
+
|
54
|
+
* Define equivalence `#==` for `Expression` and `Schedule` objects
|
55
|
+
* Define hash equality `#eql?` for `Expression::Date` objects
|
56
|
+
* Remove `ActiveSupport` dependency
|
57
|
+
|
58
|
+
[Commits](https://github.com/molawson/repeatable/compare/v0.3.0...v0.4.0)
|
59
|
+
|
60
|
+
### 0.3.0 (2015-03-11)
|
61
|
+
|
62
|
+
Features:
|
63
|
+
|
64
|
+
* Ensure `end_date` on or after `start_date` for `Schedule#occurrences` ([@danott][])
|
65
|
+
* Consider any invalid argument to `Schedule.new` a `ParseError` ([@danott][])
|
66
|
+
|
67
|
+
[Commits](https://github.com/molawson/repeatable/compare/v0.2.1...v0.3.0)
|
68
|
+
|
69
|
+
### 0.2.1 (2015-03-09)
|
70
|
+
|
71
|
+
Features:
|
72
|
+
|
73
|
+
* Add `ParseError` class for better error handling
|
74
|
+
* Extract `Parser` class from `Schedule`
|
75
|
+
|
76
|
+
Bug Fixes:
|
77
|
+
|
78
|
+
* Enable `Schedule` to take a hash with string keys
|
79
|
+
|
80
|
+
[Commits](https://github.com/molawson/repeatable/compare/v0.2.0...v0.2.1)
|
81
|
+
|
82
|
+
### 0.2.0 (2015-03-03)
|
83
|
+
|
84
|
+
Features:
|
85
|
+
|
86
|
+
* Add `Schedule#to_h` and `Expression#to_h` methods
|
87
|
+
* Enable building a `Schedule` from composed `Expression` objects
|
88
|
+
|
89
|
+
Bug Fixes:
|
90
|
+
|
91
|
+
* Fix default case equality for `Expression::Base` to work with classes and instances
|
92
|
+
|
93
|
+
[Commits](https://github.com/molawson/repeatable/compare/v0.1.0...v0.2.0)
|
94
|
+
|
95
|
+
### 0.1.0 (2015-02-23)
|
96
|
+
|
97
|
+
Initial Release
|
98
|
+
|
99
|
+
[Commits](https://github.com/molawson/repeatable/compare/531d40c...v0.1.0)
|
100
|
+
|
101
|
+
|
102
|
+
[@danott]: https://github.com/danott
|
103
|
+
[@PatrickLerner]: https://github.com/PatrickLerner
|
104
|
+
[@danielma]: https://github.com/danielma
|
105
|
+
[@cmoel]: https://github.com/cmoel
|
data/Gemfile
CHANGED
@@ -1,7 +1,13 @@
|
|
1
|
-
source
|
1
|
+
source "https://rubygems.org"
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in repeatable.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
-
gem
|
7
|
-
gem
|
6
|
+
gem "pry", "~> 0.13"
|
7
|
+
gem "rake", ">= 12.3.3"
|
8
|
+
gem "standard", "~> 1.0"
|
9
|
+
|
10
|
+
group :test do
|
11
|
+
gem "rspec", "~> 3.0"
|
12
|
+
gem "simplecov", "~> 0.18"
|
13
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
repeatable (1.0.0)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: https://rubygems.org/
|
8
|
+
specs:
|
9
|
+
ast (2.4.2)
|
10
|
+
coderay (1.1.3)
|
11
|
+
diff-lcs (1.4.4)
|
12
|
+
docile (1.3.5)
|
13
|
+
method_source (1.0.0)
|
14
|
+
parallel (1.20.1)
|
15
|
+
parser (3.0.0.0)
|
16
|
+
ast (~> 2.4.1)
|
17
|
+
pry (0.14.0)
|
18
|
+
coderay (~> 1.1)
|
19
|
+
method_source (~> 1.0)
|
20
|
+
rainbow (3.0.0)
|
21
|
+
rake (13.0.3)
|
22
|
+
regexp_parser (2.1.1)
|
23
|
+
rexml (3.2.4)
|
24
|
+
rspec (3.10.0)
|
25
|
+
rspec-core (~> 3.10.0)
|
26
|
+
rspec-expectations (~> 3.10.0)
|
27
|
+
rspec-mocks (~> 3.10.0)
|
28
|
+
rspec-core (3.10.1)
|
29
|
+
rspec-support (~> 3.10.0)
|
30
|
+
rspec-expectations (3.10.1)
|
31
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
32
|
+
rspec-support (~> 3.10.0)
|
33
|
+
rspec-mocks (3.10.2)
|
34
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
35
|
+
rspec-support (~> 3.10.0)
|
36
|
+
rspec-support (3.10.2)
|
37
|
+
rubocop (1.11.0)
|
38
|
+
parallel (~> 1.10)
|
39
|
+
parser (>= 3.0.0.0)
|
40
|
+
rainbow (>= 2.2.2, < 4.0)
|
41
|
+
regexp_parser (>= 1.8, < 3.0)
|
42
|
+
rexml
|
43
|
+
rubocop-ast (>= 1.2.0, < 2.0)
|
44
|
+
ruby-progressbar (~> 1.7)
|
45
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
46
|
+
rubocop-ast (1.4.1)
|
47
|
+
parser (>= 2.7.1.5)
|
48
|
+
rubocop-performance (1.10.1)
|
49
|
+
rubocop (>= 0.90.0, < 2.0)
|
50
|
+
rubocop-ast (>= 0.4.0)
|
51
|
+
ruby-progressbar (1.11.0)
|
52
|
+
simplecov (0.21.2)
|
53
|
+
docile (~> 1.1)
|
54
|
+
simplecov-html (~> 0.11)
|
55
|
+
simplecov_json_formatter (~> 0.1)
|
56
|
+
simplecov-html (0.12.3)
|
57
|
+
simplecov_json_formatter (0.1.2)
|
58
|
+
standard (1.0.4)
|
59
|
+
rubocop (= 1.11.0)
|
60
|
+
rubocop-performance (= 1.10.1)
|
61
|
+
unicode-display_width (2.0.0)
|
62
|
+
|
63
|
+
PLATFORMS
|
64
|
+
x86_64-darwin-19
|
65
|
+
x86_64-linux
|
66
|
+
|
67
|
+
DEPENDENCIES
|
68
|
+
pry (~> 0.13)
|
69
|
+
rake (>= 12.3.3)
|
70
|
+
repeatable!
|
71
|
+
rspec (~> 3.0)
|
72
|
+
simplecov (~> 0.18)
|
73
|
+
standard (~> 1.0)
|
74
|
+
|
75
|
+
BUNDLED WITH
|
76
|
+
2.2.14
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Repeatable
|
2
2
|
|
3
|
-
[![Build Status](https://
|
4
|
-
[![
|
5
|
-
[![
|
3
|
+
[![Build Status](https://travis-ci.org/molawson/repeatable.svg?branch=main)](https://travis-ci.org/molawson/repeatable)
|
4
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/73707efd5eeffd364c0d/maintainability)](https://codeclimate.com/github/molawson/repeatable/maintainability)
|
5
|
+
[![Test Coverage](https://api.codeclimate.com/v1/badges/73707efd5eeffd364c0d/test_coverage)](https://codeclimate.com/github/molawson/repeatable/test_coverage)
|
6
6
|
|
7
7
|
Ruby implementation of Martin Fowler's [Recurring Events for Calendars](http://martinfowler.com/apsupp/recurring.pdf) paper.
|
8
8
|
|
@@ -22,6 +22,10 @@ Or install it yourself as:
|
|
22
22
|
|
23
23
|
$ gem install repeatable
|
24
24
|
|
25
|
+
## Requirements
|
26
|
+
|
27
|
+
Because this gem relies heavily on required keyword arguments, especially to make dumping and parsing of schedules simpler, this code will only work on **Ruby 2.2** and higher.
|
28
|
+
|
25
29
|
## Usage
|
26
30
|
|
27
31
|
### Building a Schedule
|
@@ -33,9 +37,9 @@ You can create a schedule in one of two ways.
|
|
33
37
|
Instantiate and compose each of the `Repeatable::Expression` objects manually.
|
34
38
|
|
35
39
|
```ruby
|
36
|
-
second_monday =
|
40
|
+
second_monday = Repeatable::Expression::WeekdayInMonth.new(weekday: 1, count: 2)
|
37
41
|
oct_thru_dec = Repeatable::Expression::RangeInYear.new(start_month: 10, end_month: 12)
|
38
|
-
intersection = Repeatable::
|
42
|
+
intersection = Repeatable::Expression::Intersection.new(second_monday, oct_thru_dec)
|
39
43
|
|
40
44
|
schedule = Repeatable::Schedule.new(intersection)
|
41
45
|
```
|
@@ -49,7 +53,8 @@ Or describe the same structure with a `Hash`, and the gem will handle instantiat
|
|
49
53
|
arg = {
|
50
54
|
intersection: [
|
51
55
|
{ weekday_in_month: { weekday: 1, count: 2 } },
|
52
|
-
{ range_in_year: { start_month: 10, end_month: 12 } }
|
56
|
+
{ range_in_year: { start_month: 10, end_month: 12 } },
|
57
|
+
{ exact_date: { date: "2015-08-01" } }
|
53
58
|
]
|
54
59
|
}
|
55
60
|
|
@@ -73,6 +78,10 @@ Repeatable::Expression::Union.new(expressions)
|
|
73
78
|
{ intersection: [] }
|
74
79
|
Repeatable::Expression::Intersection.new(expressions)
|
75
80
|
|
81
|
+
# Date is part of the first set (`included`) but not part of the second set (`excluded`)
|
82
|
+
{ difference: { included: expression, excluded: another_expression } }
|
83
|
+
Repeatable::Expression::Difference.new(included: expression, excluded: another_expression)
|
84
|
+
|
76
85
|
|
77
86
|
# DATES
|
78
87
|
|
@@ -84,10 +93,22 @@ Repeatable::Expression::Weekday.new(weekday: 0)
|
|
84
93
|
{ weekday_in_month: { weekday: 1, count: 3 } }
|
85
94
|
Repeatable::Expression::WeekdayInMonth.new(weekday: 1, count: 3)
|
86
95
|
|
96
|
+
# The last Thursday of every month
|
97
|
+
{ weekday_in_month: { weekday: 4, count: -1 } }
|
98
|
+
Repeatable::Expression::WeekdayInMonth.new(weekday: 4, count: -1)
|
99
|
+
|
100
|
+
# Every other Monday, starting from December 1, 2015
|
101
|
+
{ biweekly: { weekday: 1, start_date: '2015-12-01' } }
|
102
|
+
Repeatable::Expression::Biweekly.new(weekday: 1, start_date: Date.new(2015, 12, 1))
|
103
|
+
|
87
104
|
# The 13th of every month
|
88
105
|
{ day_in_month: { day: 13 } }
|
89
106
|
Repeatable::Expression::DayInMonth.new(day: 13)
|
90
107
|
|
108
|
+
# The last day of every month
|
109
|
+
{ day_in_month: { day: -1 } }
|
110
|
+
Repeatable::Expression::DayInMonth.new(day: -1)
|
111
|
+
|
91
112
|
# All days in October
|
92
113
|
{ range_in_year: { start_month: 10 } }
|
93
114
|
Repeatable::Expression::RangeInYear.new(start_month: 10)
|
@@ -99,16 +120,38 @@ Repeatable::Expression::RangeInYear.new(start_month: 10, end_month: 12)
|
|
99
120
|
# All days from October 1 through December 20
|
100
121
|
{ range_in_year: { start_month: 10, end_month: 12, start_day: 1, end_day: 20 } }
|
101
122
|
Repeatable::Expression::RangeInYear.new(start_month: 10, end_month: 12, start_day: 1, end_day: 20)
|
123
|
+
|
124
|
+
# only December 21, 2012
|
125
|
+
{ exact_date: { date: '2012-12-21' } }
|
126
|
+
Repeatable::Expression::ExactDate.new(date: Date.new(2012, 12, 21)
|
102
127
|
```
|
103
128
|
|
129
|
+
#### Schedule Errors
|
130
|
+
|
131
|
+
If something in the argument passed into `Repeatable::Schedule.new` can't be handled by the `Schedule` or `Parser` (e.g. an expression hash key that doesn't match an existing expression class), a `Repeatable::ParseError` will be raised with a (hopefully) helpful error message.
|
132
|
+
|
104
133
|
### Getting information from a Schedule
|
105
134
|
|
106
|
-
Ask a schedule
|
135
|
+
Ask a schedule to do a number of things.
|
107
136
|
|
108
137
|
```ruby
|
109
138
|
schedule.next_occurrence
|
110
139
|
# => Date of next occurrence
|
111
140
|
|
141
|
+
# By default, it will find the next occurrence after Date.today.
|
142
|
+
# You can also specify a start date.
|
143
|
+
schedule.next_occurrence(Date.new(2015, 1, 1))
|
144
|
+
# => Date of next occurrence after Jan 1, 2015
|
145
|
+
|
146
|
+
# You also have the option of including the start date as a possible result.
|
147
|
+
schedule.next_occurrence(Date.new(2015, 1, 1), include_start: true)
|
148
|
+
# => Date of next occurrence on or after Jan 1, 2015
|
149
|
+
|
150
|
+
# By default, searches for the next occurrence are limited to the next 36,525 days (about 100 years).
|
151
|
+
# That limit can also be specified in number of days.
|
152
|
+
schedule.next_occurrence(limit: 365)
|
153
|
+
# => Date of next occurrence within the next 365 days
|
154
|
+
|
112
155
|
schedule.occurrences(Date.new(2015, 1, 1), Date.new(2016, 6, 30))
|
113
156
|
# => Dates of all occurrences between Jan 1, 2015 and June 30, 2016
|
114
157
|
|
@@ -117,7 +160,30 @@ schedule.include?(Date.new(2015, 10, 10))
|
|
117
160
|
|
118
161
|
schedule.to_h
|
119
162
|
# => Hash representation of the Schedule, which is useful for storage and
|
120
|
-
# can be used to
|
163
|
+
# can be used to recreate an identical Schedule object at a later time
|
164
|
+
```
|
165
|
+
|
166
|
+
#### Equivalence
|
167
|
+
|
168
|
+
Both `Repeatable::Schedule` and all `Repeatable::Expression` classes have equivalence `#==` defined according to what's appropriate for each class, so regardless of the order of arguments passed to each, you can tell whether one object is equivalent to the other in terms of whether or not, when asked the same questions, you'd receive the same results from each.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
Repeatable::Expression::DayInMonth.new(day: 1) == Repeatable::Expression::DayInMonth.new(day: 1)
|
172
|
+
# => true
|
173
|
+
|
174
|
+
first = Repeatable::Expression::DayInMonth.new(day: 1)
|
175
|
+
fifteenth = Repeatable::Expression::DayInMonth.new(day: 15)
|
176
|
+
first == fifteenth
|
177
|
+
# => false
|
178
|
+
|
179
|
+
union = Repeatable::Expression::Union.new(first, fifteenth)
|
180
|
+
another_union = Repeatable::Expression::Union.new(fifteenth, first)
|
181
|
+
union == another_union
|
182
|
+
# => true (order of Union and Intersection arguments doesn't their affect output)
|
183
|
+
|
184
|
+
Repeatable::Schedule.new(union) == Repeatable::Schedule.new(another_union)
|
185
|
+
# => true (their expressions are equivalent, so they'll produce the same results)
|
186
|
+
|
121
187
|
```
|
122
188
|
|
123
189
|
## Development
|
data/Rakefile
CHANGED
data/lib/repeatable.rb
CHANGED
@@ -1,26 +1,30 @@
|
|
1
|
-
require
|
1
|
+
require "date"
|
2
2
|
|
3
|
-
require
|
4
|
-
|
5
|
-
require 'repeatable/conversions'
|
6
|
-
include Repeatable::Conversions
|
3
|
+
require "repeatable/version"
|
7
4
|
|
8
5
|
module Repeatable
|
9
6
|
end
|
10
7
|
|
11
|
-
require
|
8
|
+
require "repeatable/parse_error"
|
9
|
+
|
10
|
+
require "repeatable/conversions"
|
11
|
+
require "repeatable/last_date_of_month"
|
12
12
|
|
13
|
-
require
|
14
|
-
require
|
13
|
+
require "repeatable/expression"
|
14
|
+
require "repeatable/expression/base"
|
15
15
|
|
16
|
-
require
|
17
|
-
require
|
18
|
-
require
|
19
|
-
require
|
16
|
+
require "repeatable/expression/date"
|
17
|
+
require "repeatable/expression/exact_date"
|
18
|
+
require "repeatable/expression/weekday"
|
19
|
+
require "repeatable/expression/biweekly"
|
20
|
+
require "repeatable/expression/weekday_in_month"
|
21
|
+
require "repeatable/expression/day_in_month"
|
22
|
+
require "repeatable/expression/range_in_year"
|
20
23
|
|
21
|
-
require
|
22
|
-
require
|
23
|
-
require
|
24
|
+
require "repeatable/expression/set"
|
25
|
+
require "repeatable/expression/union"
|
26
|
+
require "repeatable/expression/intersection"
|
27
|
+
require "repeatable/expression/difference"
|
24
28
|
|
25
|
-
require
|
26
|
-
require
|
29
|
+
require "repeatable/schedule"
|
30
|
+
require "repeatable/parser"
|
@@ -23,6 +23,31 @@ module Repeatable
|
|
23
23
|
"Don't use Expression::Base directly. Subclasses must implement `#to_h`"
|
24
24
|
)
|
25
25
|
end
|
26
|
+
|
27
|
+
def union(other)
|
28
|
+
Union.new(self, other)
|
29
|
+
end
|
30
|
+
alias_method :+, :union
|
31
|
+
alias_method :|, :union
|
32
|
+
|
33
|
+
def intersection(other)
|
34
|
+
Intersection.new(self, other)
|
35
|
+
end
|
36
|
+
alias_method :&, :intersection
|
37
|
+
|
38
|
+
def difference(other)
|
39
|
+
Difference.new(included: self, excluded: other)
|
40
|
+
end
|
41
|
+
alias_method :-, :difference
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def hash_key
|
46
|
+
self.class.name.split("::").last
|
47
|
+
.gsub(/(?<!\b)[A-Z]/) { "_#{Regexp.last_match[0]}" }
|
48
|
+
.downcase
|
49
|
+
.to_sym
|
50
|
+
end
|
26
51
|
end
|
27
52
|
end
|
28
53
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Repeatable
|
2
|
+
module Expression
|
3
|
+
class Biweekly < Date
|
4
|
+
def initialize(weekday:, start_after: ::Date.today)
|
5
|
+
@weekday = weekday
|
6
|
+
@start_after = Conversions::Date(start_after)
|
7
|
+
end
|
8
|
+
|
9
|
+
def include?(date)
|
10
|
+
date >= start_after && (date - first_occurrence) % 14 == 0
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
attr_reader :weekday, :start_after
|
16
|
+
|
17
|
+
def first_occurrence
|
18
|
+
@first_occurrence ||= find_first_occurrence
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_first_occurrence
|
22
|
+
days_away = weekday - start_after.wday
|
23
|
+
days_away += 7 if days_away <= 0
|
24
|
+
start_after + days_away
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Repeatable
|
2
|
+
module Expression
|
3
|
+
class Date < Base
|
4
|
+
def to_h
|
5
|
+
Hash[hash_key, attributes]
|
6
|
+
end
|
7
|
+
|
8
|
+
def ==(other)
|
9
|
+
other.is_a?(self.class) && attributes == other.attributes
|
10
|
+
end
|
11
|
+
|
12
|
+
alias_method :eql?, :==
|
13
|
+
|
14
|
+
def hash
|
15
|
+
[attributes.values, self.class.name].hash
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def attributes
|
21
|
+
instance_variables.each_with_object({}) do |name, hash|
|
22
|
+
key = name.to_s.gsub(/^@/, "").to_sym
|
23
|
+
hash[key] = normalize_attribute_value(instance_variable_get(name))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def normalize_attribute_value(value)
|
28
|
+
case value
|
29
|
+
when ::Date
|
30
|
+
value.to_s
|
31
|
+
else
|
32
|
+
value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,16 +1,18 @@
|
|
1
1
|
module Repeatable
|
2
2
|
module Expression
|
3
|
-
class DayInMonth <
|
3
|
+
class DayInMonth < Date
|
4
|
+
include LastDateOfMonth
|
5
|
+
|
4
6
|
def initialize(day:)
|
5
7
|
@day = day
|
6
8
|
end
|
7
9
|
|
8
10
|
def include?(date)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
if day < 0
|
12
|
+
date - last_date_of_month(date) - 1 == day
|
13
|
+
else
|
14
|
+
date.day == day
|
15
|
+
end
|
14
16
|
end
|
15
17
|
|
16
18
|
private
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Repeatable
|
2
|
+
module Expression
|
3
|
+
class Difference < Base
|
4
|
+
def initialize(included:, excluded:)
|
5
|
+
@included = included
|
6
|
+
@excluded = excluded
|
7
|
+
end
|
8
|
+
|
9
|
+
def include?(date)
|
10
|
+
included.include?(date) && !excluded.include?(date)
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_h
|
14
|
+
Hash[hash_key, {included: included.to_h, excluded: excluded.to_h}]
|
15
|
+
end
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
other.is_a?(self.class) &&
|
19
|
+
included == other.included &&
|
20
|
+
excluded == other.excluded
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
attr_reader :included, :excluded
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -1,6 +1,11 @@
|
|
1
1
|
module Repeatable
|
2
2
|
module Expression
|
3
3
|
class Intersection < Set
|
4
|
+
def initialize(*elements)
|
5
|
+
other_intersection, not_intersection = elements.partition { |e| e.is_a?(self.class) }
|
6
|
+
super(other_intersection.flat_map(&:elements) + not_intersection)
|
7
|
+
end
|
8
|
+
|
4
9
|
def include?(date)
|
5
10
|
elements.all? { |e| e.include?(date) }
|
6
11
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Repeatable
|
2
2
|
module Expression
|
3
|
-
class RangeInYear <
|
3
|
+
class RangeInYear < Date
|
4
4
|
def initialize(start_month:, end_month: start_month, start_day: 0, end_day: 0)
|
5
5
|
@start_month = start_month
|
6
6
|
@end_month = end_month
|
@@ -9,15 +9,21 @@ module Repeatable
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def include?(date)
|
12
|
-
|
12
|
+
return true if months_include?(date)
|
13
|
+
|
14
|
+
if start_month == end_month
|
15
|
+
start_month_include?(date) && end_month_include?(date)
|
16
|
+
else
|
17
|
+
start_month_include?(date) || end_month_include?(date)
|
18
|
+
end
|
13
19
|
end
|
14
20
|
|
15
21
|
def to_h
|
16
|
-
args = {
|
22
|
+
args = {start_month: start_month}
|
17
23
|
args[:end_month] = end_month unless end_month == start_month
|
18
24
|
args[:start_day] = start_day unless start_day.zero?
|
19
25
|
args[:end_day] = end_day unless end_day.zero?
|
20
|
-
{
|
26
|
+
{range_in_year: args}
|
21
27
|
end
|
22
28
|
|
23
29
|
private
|
@@ -1,24 +1,26 @@
|
|
1
1
|
module Repeatable
|
2
2
|
module Expression
|
3
3
|
class Set < Base
|
4
|
+
attr_reader :elements
|
5
|
+
|
4
6
|
def initialize(*elements)
|
5
|
-
@elements = elements.flatten
|
7
|
+
@elements = elements.flatten.uniq
|
6
8
|
end
|
7
9
|
|
8
10
|
def <<(element)
|
9
|
-
elements << element
|
11
|
+
elements << element unless elements.include?(element)
|
10
12
|
self
|
11
13
|
end
|
12
14
|
|
13
15
|
def to_h
|
14
|
-
|
15
|
-
hash[self.class.name.demodulize.underscore.to_sym] = elements.map(&:to_h)
|
16
|
-
hash
|
16
|
+
Hash[hash_key, elements.map(&:to_h)]
|
17
17
|
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
19
|
+
def ==(other)
|
20
|
+
other.is_a?(self.class) &&
|
21
|
+
elements.size == other.elements.size &&
|
22
|
+
other.elements.all? { |e| elements.include?(e) }
|
23
|
+
end
|
22
24
|
end
|
23
25
|
end
|
24
26
|
end
|
@@ -1,6 +1,11 @@
|
|
1
1
|
module Repeatable
|
2
2
|
module Expression
|
3
3
|
class Union < Set
|
4
|
+
def initialize(*elements)
|
5
|
+
other_unions, not_unions = elements.partition { |e| e.is_a?(self.class) }
|
6
|
+
super(other_unions.flat_map(&:elements) + not_unions)
|
7
|
+
end
|
8
|
+
|
4
9
|
def include?(date)
|
5
10
|
elements.any? { |e| e.include?(date) }
|
6
11
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Repeatable
|
2
2
|
module Expression
|
3
|
-
class Weekday <
|
3
|
+
class Weekday < Date
|
4
4
|
def initialize(weekday:)
|
5
5
|
@weekday = weekday
|
6
6
|
end
|
@@ -9,10 +9,6 @@ module Repeatable
|
|
9
9
|
date.wday == weekday
|
10
10
|
end
|
11
11
|
|
12
|
-
def to_h
|
13
|
-
{ weekday: { weekday: weekday } }
|
14
|
-
end
|
15
|
-
|
16
12
|
private
|
17
13
|
|
18
14
|
attr_reader :weekday
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module Repeatable
|
2
2
|
module Expression
|
3
|
-
class WeekdayInMonth <
|
3
|
+
class WeekdayInMonth < Date
|
4
|
+
include LastDateOfMonth
|
5
|
+
|
4
6
|
def initialize(weekday:, count:)
|
5
7
|
@weekday = weekday
|
6
8
|
@count = count
|
@@ -10,10 +12,6 @@ module Repeatable
|
|
10
12
|
day_matches?(date) && week_matches?(date)
|
11
13
|
end
|
12
14
|
|
13
|
-
def to_h
|
14
|
-
{ weekday_in_month: { weekday: weekday, count: count } }
|
15
|
-
end
|
16
|
-
|
17
15
|
private
|
18
16
|
|
19
17
|
attr_reader :weekday, :count
|
@@ -23,11 +21,27 @@ module Repeatable
|
|
23
21
|
end
|
24
22
|
|
25
23
|
def week_matches?(date)
|
26
|
-
|
24
|
+
if negative_count?
|
25
|
+
week_from_end(date) == count
|
26
|
+
else
|
27
|
+
week_from_beginning(date) == count
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def week_from_beginning(date)
|
32
|
+
week_in_month(date.day - 1)
|
33
|
+
end
|
34
|
+
|
35
|
+
def week_from_end(date)
|
36
|
+
-week_in_month(last_date_of_month(date).day - date.day)
|
37
|
+
end
|
38
|
+
|
39
|
+
def week_in_month(zero_indexed_day)
|
40
|
+
(zero_indexed_day / 7) + 1
|
27
41
|
end
|
28
42
|
|
29
|
-
def
|
30
|
-
|
43
|
+
def negative_count?
|
44
|
+
count < 0
|
31
45
|
end
|
32
46
|
end
|
33
47
|
end
|
data/lib/repeatable/parser.rb
CHANGED
@@ -25,20 +25,35 @@ module Repeatable
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def expression_for(key, value)
|
28
|
-
klass =
|
28
|
+
klass = expression_klass(key.to_s)
|
29
29
|
case klass
|
30
30
|
when nil
|
31
31
|
fail(ParseError, "Unknown mapping: Can't map key '#{key.inspect}' to an expression class")
|
32
32
|
when Repeatable::Expression::Set
|
33
33
|
args = value.map { |hash| build_expression(hash) }
|
34
34
|
klass.new(*args)
|
35
|
+
when Repeatable::Expression::Difference
|
36
|
+
value = symbolize_keys(value)
|
37
|
+
klass.new(
|
38
|
+
included: build_expression(value[:included]),
|
39
|
+
excluded: build_expression(value[:excluded])
|
40
|
+
)
|
35
41
|
else
|
36
|
-
klass.new(symbolize_keys(value))
|
42
|
+
klass.new(**symbolize_keys(value))
|
37
43
|
end
|
38
44
|
end
|
39
45
|
|
40
46
|
def symbolize_keys(hash)
|
41
47
|
hash.each_with_object({}) { |(k, v), a| a[k.to_sym] = v }
|
42
48
|
end
|
49
|
+
|
50
|
+
def expression_klass(string)
|
51
|
+
camel_cased_string = string
|
52
|
+
.capitalize
|
53
|
+
.gsub(/(?:_)(?<word>[a-z\d]+)/i) { Regexp.last_match[:word].capitalize }
|
54
|
+
Repeatable::Expression.const_get(camel_cased_string)
|
55
|
+
rescue NameError => e
|
56
|
+
raise if e.name && e.name.to_s != camel_cased_string
|
57
|
+
end
|
43
58
|
end
|
44
59
|
end
|
data/lib/repeatable/schedule.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'active_support/core_ext/string/inflections'
|
2
|
-
|
3
1
|
module Repeatable
|
4
2
|
class Schedule
|
5
3
|
def initialize(arg)
|
@@ -9,26 +7,34 @@ module Repeatable
|
|
9
7
|
when Hash
|
10
8
|
@expression = Parser.call(arg)
|
11
9
|
else
|
12
|
-
fail(
|
10
|
+
fail(ParseError, "Can't build a Repeatable::Schedule from #{arg.class}")
|
13
11
|
end
|
14
12
|
end
|
15
13
|
|
16
14
|
def occurrences(start_date, end_date)
|
17
|
-
start_date = Date(start_date)
|
18
|
-
end_date = Date(end_date)
|
15
|
+
start_date = Conversions::Date(start_date)
|
16
|
+
end_date = Conversions::Date(end_date)
|
17
|
+
|
18
|
+
fail(ArgumentError, "end_date must be equal to or after start_date") if end_date < start_date
|
19
|
+
|
19
20
|
(start_date..end_date).select { |date| include?(date) }
|
20
21
|
end
|
21
22
|
|
22
|
-
def next_occurrence(start_date = Date.today)
|
23
|
-
date = Date(start_date)
|
24
|
-
|
23
|
+
def next_occurrence(start_date = Date.today, include_start: false, limit: 36525)
|
24
|
+
date = Conversions::Date(start_date)
|
25
|
+
|
26
|
+
return date if include_start && include?(date)
|
27
|
+
|
28
|
+
1.step do |i|
|
25
29
|
date = date.next_day
|
30
|
+
|
31
|
+
break date if include?(date)
|
32
|
+
break if i == limit.to_i
|
26
33
|
end
|
27
|
-
date
|
28
34
|
end
|
29
35
|
|
30
36
|
def include?(date = Date.today)
|
31
|
-
date = Date(date)
|
37
|
+
date = Conversions::Date(date)
|
32
38
|
expression.include?(date)
|
33
39
|
end
|
34
40
|
|
@@ -36,7 +42,11 @@ module Repeatable
|
|
36
42
|
expression.to_h
|
37
43
|
end
|
38
44
|
|
39
|
-
|
45
|
+
def ==(other)
|
46
|
+
other.is_a?(self.class) && expression == other.expression
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
40
50
|
|
41
51
|
attr_reader :expression
|
42
52
|
end
|
data/lib/repeatable/version.rb
CHANGED
data/repeatable.gemspec
CHANGED
@@ -1,24 +1,19 @@
|
|
1
|
-
lib = File.expand_path(
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
-
require
|
3
|
+
require "repeatable/version"
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name
|
7
|
-
spec.version
|
8
|
-
spec.authors
|
9
|
-
spec.email
|
6
|
+
spec.name = "repeatable"
|
7
|
+
spec.version = Repeatable::VERSION
|
8
|
+
spec.authors = ["Mo Lawson"]
|
9
|
+
spec.email = ["mo@molawson.com"]
|
10
10
|
|
11
|
-
spec.summary
|
12
|
-
spec.description
|
13
|
-
spec.homepage
|
14
|
-
spec.license
|
11
|
+
spec.summary = "Describe recurring event schedules and calculate their occurrences"
|
12
|
+
spec.description = "Ruby implementation of Martin Fowler's 'Recurring Events for Calendars' paper."
|
13
|
+
spec.homepage = "https://github.com/molawson/repeatable"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
15
16
|
|
16
|
-
spec.files
|
17
|
-
spec.require_paths = [
|
18
|
-
|
19
|
-
spec.add_dependency 'activesupport', '>= 3.0'
|
20
|
-
|
21
|
-
spec.add_development_dependency 'bundler', '~> 1.6'
|
22
|
-
spec.add_development_dependency 'rake', '~> 10.0'
|
23
|
-
spec.add_development_dependency 'rspec', '~> 3.0'
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.require_paths = ["lib"]
|
24
19
|
end
|
metadata
CHANGED
@@ -1,71 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: repeatable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mo Lawson
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
12
|
-
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: activesupport
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '3.0'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '3.0'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: bundler
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '1.6'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '1.6'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: rake
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '10.0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '10.0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: rspec
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '3.0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '3.0'
|
11
|
+
date: 2021-03-25 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
69
13
|
description: Ruby implementation of Martin Fowler's 'Recurring Events for Calendars'
|
70
14
|
paper.
|
71
15
|
email:
|
@@ -74,10 +18,14 @@ executables: []
|
|
74
18
|
extensions: []
|
75
19
|
extra_rdoc_files: []
|
76
20
|
files:
|
21
|
+
- ".git-blame-ignore-revs"
|
77
22
|
- ".gitignore"
|
78
23
|
- ".rspec"
|
24
|
+
- ".standard.yml"
|
79
25
|
- ".travis.yml"
|
26
|
+
- CHANGELOG.md
|
80
27
|
- Gemfile
|
28
|
+
- Gemfile.lock
|
81
29
|
- LICENSE.txt
|
82
30
|
- README.md
|
83
31
|
- Rakefile
|
@@ -87,13 +35,18 @@ files:
|
|
87
35
|
- lib/repeatable/conversions.rb
|
88
36
|
- lib/repeatable/expression.rb
|
89
37
|
- lib/repeatable/expression/base.rb
|
38
|
+
- lib/repeatable/expression/biweekly.rb
|
39
|
+
- lib/repeatable/expression/date.rb
|
90
40
|
- lib/repeatable/expression/day_in_month.rb
|
41
|
+
- lib/repeatable/expression/difference.rb
|
42
|
+
- lib/repeatable/expression/exact_date.rb
|
91
43
|
- lib/repeatable/expression/intersection.rb
|
92
44
|
- lib/repeatable/expression/range_in_year.rb
|
93
45
|
- lib/repeatable/expression/set.rb
|
94
46
|
- lib/repeatable/expression/union.rb
|
95
47
|
- lib/repeatable/expression/weekday.rb
|
96
48
|
- lib/repeatable/expression/weekday_in_month.rb
|
49
|
+
- lib/repeatable/last_date_of_month.rb
|
97
50
|
- lib/repeatable/parse_error.rb
|
98
51
|
- lib/repeatable/parser.rb
|
99
52
|
- lib/repeatable/schedule.rb
|
@@ -103,7 +56,7 @@ homepage: https://github.com/molawson/repeatable
|
|
103
56
|
licenses:
|
104
57
|
- MIT
|
105
58
|
metadata: {}
|
106
|
-
post_install_message:
|
59
|
+
post_install_message:
|
107
60
|
rdoc_options: []
|
108
61
|
require_paths:
|
109
62
|
- lib
|
@@ -111,16 +64,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
111
64
|
requirements:
|
112
65
|
- - ">="
|
113
66
|
- !ruby/object:Gem::Version
|
114
|
-
version:
|
67
|
+
version: 2.5.0
|
115
68
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
69
|
requirements:
|
117
70
|
- - ">="
|
118
71
|
- !ruby/object:Gem::Version
|
119
72
|
version: '0'
|
120
73
|
requirements: []
|
121
|
-
|
122
|
-
|
123
|
-
signing_key:
|
74
|
+
rubygems_version: 3.1.4
|
75
|
+
signing_key:
|
124
76
|
specification_version: 4
|
125
77
|
summary: Describe recurring event schedules and calculate their occurrences
|
126
78
|
test_files: []
|