icalendar-rrule 0.1.5
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 +7 -0
- data/.gitignore +100 -0
- data/.rspec +3 -0
- data/.rubocop.yml +45 -0
- data/.travis.yml +5 -0
- data/.yardopts +1 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +79 -0
- data/LICENSE.txt +21 -0
- data/README.md +112 -0
- data/Rakefile +8 -0
- data/icalendar-rrule.gemspec +38 -0
- data/lib/icalendar-rrule.rb +3 -0
- data/lib/icalendar/rrule.rb +8 -0
- data/lib/icalendar/rrule/occurrence.rb +140 -0
- data/lib/icalendar/rrule/version.rb +7 -0
- data/lib/icalendar/scannable-calendar.rb +64 -0
- data/lib/icalendar/schedulable-component.rb +437 -0
- metadata +188 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5bc3ceb9790452476e8f32de6f5c5f6f9780fb13f7fb667d002a76d8a0438774
|
|
4
|
+
data.tar.gz: a910d639df4e221adb306571005e3eb23a4f68753de363946cb8a6befb906080
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 32f11de521b34a9caeda104a49cf428e9c9f3f23938e804c057a5f0a8371bffbb96eb8cbc9cbe5ed890c5813a0a3d5240994d72db175b86f2f18e2b5f8108896
|
|
7
|
+
data.tar.gz: 440e88786c1ed54ee3c090a709e0fd145d930147bfb00c62cd8d8b98fafda00a0680880727e68d0f2c13ba9845010d04718261eb924ac81e06c2efe6c96f5e75
|
data/.gitignore
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
*.gem
|
|
2
|
+
*.rbc
|
|
3
|
+
/.config
|
|
4
|
+
/coverage/
|
|
5
|
+
/InstalledFiles
|
|
6
|
+
/pkg/
|
|
7
|
+
/spec/reports/
|
|
8
|
+
/spec/examples.txt
|
|
9
|
+
/test/tmp/
|
|
10
|
+
/test/version_tmp/
|
|
11
|
+
/tmp/
|
|
12
|
+
|
|
13
|
+
# Used by dotenv library to load environment variables.
|
|
14
|
+
# .env
|
|
15
|
+
|
|
16
|
+
## Specific to RubyMotion:
|
|
17
|
+
.dat*
|
|
18
|
+
.repl_history
|
|
19
|
+
build/
|
|
20
|
+
*.bridgesupport
|
|
21
|
+
build-iPhoneOS/
|
|
22
|
+
build-iPhoneSimulator/
|
|
23
|
+
|
|
24
|
+
## Specific to RubyMotion (use of CocoaPods):
|
|
25
|
+
#
|
|
26
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
|
27
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
|
28
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
|
29
|
+
#
|
|
30
|
+
# vendor/Pods/
|
|
31
|
+
|
|
32
|
+
## Documentation cache and generated files:
|
|
33
|
+
/.yardoc/
|
|
34
|
+
/_yardoc/
|
|
35
|
+
/doc/
|
|
36
|
+
/rdoc/
|
|
37
|
+
|
|
38
|
+
## Environment normalization:
|
|
39
|
+
/.bundle/
|
|
40
|
+
/vendor/bundle
|
|
41
|
+
/lib/bundler/man/
|
|
42
|
+
|
|
43
|
+
# for a library or gem, you might want to ignore these files since the code is
|
|
44
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
45
|
+
.ruby-version
|
|
46
|
+
.ruby-gemset
|
|
47
|
+
|
|
48
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
|
49
|
+
.rvmrc
|
|
50
|
+
### JetBrains template
|
|
51
|
+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
|
52
|
+
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
|
53
|
+
|
|
54
|
+
# User-specific stuff:
|
|
55
|
+
.idea/**/workspace.xml
|
|
56
|
+
.idea/**/tasks.xml
|
|
57
|
+
.idea/dictionaries
|
|
58
|
+
|
|
59
|
+
# Sensitive or high-churn files:
|
|
60
|
+
.idea/**/dataSources/
|
|
61
|
+
.idea/**/dataSources.ids
|
|
62
|
+
.idea/**/dataSources.local.xml
|
|
63
|
+
.idea/**/sqlDataSources.xml
|
|
64
|
+
.idea/**/dynamic.xml
|
|
65
|
+
.idea/**/uiDesigner.xml
|
|
66
|
+
|
|
67
|
+
# Gradle:
|
|
68
|
+
.idea/**/gradle.xml
|
|
69
|
+
.idea/**/libraries
|
|
70
|
+
|
|
71
|
+
# CMake
|
|
72
|
+
cmake-build-debug/
|
|
73
|
+
cmake-build-release/
|
|
74
|
+
|
|
75
|
+
# Mongo Explorer plugin:
|
|
76
|
+
.idea
|
|
77
|
+
|
|
78
|
+
## File-based project format:
|
|
79
|
+
*.iws
|
|
80
|
+
|
|
81
|
+
## Plugin-specific files:
|
|
82
|
+
|
|
83
|
+
# IntelliJ
|
|
84
|
+
out/
|
|
85
|
+
|
|
86
|
+
# mpeltonen/sbt-idea plugin
|
|
87
|
+
.idea_modules/
|
|
88
|
+
|
|
89
|
+
# JIRA plugin
|
|
90
|
+
atlassian-ide-plugin.xml
|
|
91
|
+
|
|
92
|
+
# Cursive Clojure plugin
|
|
93
|
+
.idea/replstate.xml
|
|
94
|
+
|
|
95
|
+
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
96
|
+
com_crashlytics_export_strings.xml
|
|
97
|
+
crashlytics.properties
|
|
98
|
+
crashlytics-build.properties
|
|
99
|
+
fabric.properties
|
|
100
|
+
/.rspec_status
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require: rubocop-rspec
|
|
2
|
+
|
|
3
|
+
AllCops:
|
|
4
|
+
TargetRubyVersion: 2.5.0
|
|
5
|
+
|
|
6
|
+
Style/Documentation:
|
|
7
|
+
Exclude:
|
|
8
|
+
- 'spec/**/*'
|
|
9
|
+
- 'test/**/*'
|
|
10
|
+
|
|
11
|
+
Metrics/BlockLength:
|
|
12
|
+
Exclude:
|
|
13
|
+
- 'spec/**/*'
|
|
14
|
+
- 'test/**/*'
|
|
15
|
+
|
|
16
|
+
Metrics/LineLength:
|
|
17
|
+
Max: 122
|
|
18
|
+
|
|
19
|
+
Metrics/MethodLength:
|
|
20
|
+
Max: 16
|
|
21
|
+
|
|
22
|
+
Naming/FileName:
|
|
23
|
+
Exclude:
|
|
24
|
+
- 'lib/icalendar-rrule.rb'
|
|
25
|
+
- 'lib/icalendar/schedulable-component.rb'
|
|
26
|
+
- 'lib/icalendar/scannable-calendar.rb'
|
|
27
|
+
- 'spec/icalendar/scannable-calendar_spec.rb'
|
|
28
|
+
- 'spec/icalendar/schedulable-component_spec.rb'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
Metrics/ModuleLength:
|
|
33
|
+
Enabled: false
|
|
34
|
+
|
|
35
|
+
Style/DocumentationMethod:
|
|
36
|
+
Description: 'Public methods.'
|
|
37
|
+
Enabled: true
|
|
38
|
+
Exclude:
|
|
39
|
+
- 'spec/**/*'
|
|
40
|
+
- 'test/**/*'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# inherit_from: .rubocop_todo.yml
|
data/.travis.yml
ADDED
data/.yardopts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--markup=markdown
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
icalendar-rrule (0.1.5)
|
|
5
|
+
activesupport (~> 5.1)
|
|
6
|
+
icalendar (~> 2.4)
|
|
7
|
+
ice_cube (~> 0.16)
|
|
8
|
+
|
|
9
|
+
GEM
|
|
10
|
+
remote: https://rubygems.org/
|
|
11
|
+
specs:
|
|
12
|
+
activesupport (5.2.0)
|
|
13
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
14
|
+
i18n (>= 0.7, < 2)
|
|
15
|
+
minitest (~> 5.1)
|
|
16
|
+
tzinfo (~> 1.1)
|
|
17
|
+
ast (2.4.0)
|
|
18
|
+
concurrent-ruby (1.0.5)
|
|
19
|
+
diff-lcs (1.3)
|
|
20
|
+
docile (1.3.0)
|
|
21
|
+
i18n (1.0.1)
|
|
22
|
+
concurrent-ruby (~> 1.0)
|
|
23
|
+
icalendar (2.4.1)
|
|
24
|
+
ice_cube (0.16.2)
|
|
25
|
+
json (2.1.0)
|
|
26
|
+
minitest (5.11.3)
|
|
27
|
+
parallel (1.12.1)
|
|
28
|
+
parser (2.5.1.0)
|
|
29
|
+
ast (~> 2.4.0)
|
|
30
|
+
powerpack (0.1.1)
|
|
31
|
+
rainbow (3.0.0)
|
|
32
|
+
rake (10.5.0)
|
|
33
|
+
rspec (3.7.0)
|
|
34
|
+
rspec-core (~> 3.7.0)
|
|
35
|
+
rspec-expectations (~> 3.7.0)
|
|
36
|
+
rspec-mocks (~> 3.7.0)
|
|
37
|
+
rspec-core (3.7.1)
|
|
38
|
+
rspec-support (~> 3.7.0)
|
|
39
|
+
rspec-expectations (3.7.0)
|
|
40
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
41
|
+
rspec-support (~> 3.7.0)
|
|
42
|
+
rspec-mocks (3.7.0)
|
|
43
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
44
|
+
rspec-support (~> 3.7.0)
|
|
45
|
+
rspec-support (3.7.1)
|
|
46
|
+
rubocop (0.55.0)
|
|
47
|
+
parallel (~> 1.10)
|
|
48
|
+
parser (>= 2.5)
|
|
49
|
+
powerpack (~> 0.1)
|
|
50
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
51
|
+
ruby-progressbar (~> 1.7)
|
|
52
|
+
unicode-display_width (~> 1.0, >= 1.0.1)
|
|
53
|
+
rubocop-rspec (1.25.1)
|
|
54
|
+
rubocop (>= 0.53.0)
|
|
55
|
+
ruby-progressbar (1.9.0)
|
|
56
|
+
simplecov (0.16.1)
|
|
57
|
+
docile (~> 1.1)
|
|
58
|
+
json (>= 1.8, < 3)
|
|
59
|
+
simplecov-html (~> 0.10.0)
|
|
60
|
+
simplecov-html (0.10.2)
|
|
61
|
+
thread_safe (0.3.6)
|
|
62
|
+
tzinfo (1.2.5)
|
|
63
|
+
thread_safe (~> 0.1)
|
|
64
|
+
unicode-display_width (1.3.2)
|
|
65
|
+
|
|
66
|
+
PLATFORMS
|
|
67
|
+
ruby
|
|
68
|
+
|
|
69
|
+
DEPENDENCIES
|
|
70
|
+
bundler (~> 1.16)
|
|
71
|
+
icalendar-rrule!
|
|
72
|
+
rake (~> 10.0)
|
|
73
|
+
rspec (~> 3.7)
|
|
74
|
+
rubocop (~> 0.55.0)
|
|
75
|
+
rubocop-rspec (~> 1.24)
|
|
76
|
+
simplecov (~> 0.16)
|
|
77
|
+
|
|
78
|
+
BUNDLED WITH
|
|
79
|
+
1.16.1
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018 Harald
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# icalendar-rrule
|
|
2
|
+
This is an add-on to the [iCalendar Gem](https://github.com/icalendar/icalendar).
|
|
3
|
+
It helps to handle calendars in iCalendar format with __repeating events__.
|
|
4
|
+
|
|
5
|
+
According to the [RFC 5545](https://tools.ietf.org/html/rfc5545) specification,
|
|
6
|
+
repeating events are represented by one single entry, the repetitions being shown by
|
|
7
|
+
an attached _repeat rule_. Thus when we iterate through a calendar with, for example,
|
|
8
|
+
a daily repeating event,
|
|
9
|
+
we'll only see one single event where for a month there would be many more events in reality.
|
|
10
|
+
|
|
11
|
+
The _icalendar-rrule gem_ patches an additional function called `scan` into the _iCalendar Gem_.
|
|
12
|
+
The _scan_ shows all events by unrolling the _repeat rule_ for a
|
|
13
|
+
given time period.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add this line to your application's Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'icalendar-rrule'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
and run `bundle install` from your shell.
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
For explanations on how to parse and process RFC 5545 compatible calendars, please
|
|
28
|
+
have a look at the [iCalendar gem](http://github.com/icalendar/icalendar).
|
|
29
|
+
|
|
30
|
+
To use this gem we'll first have to require it:
|
|
31
|
+
|
|
32
|
+
`require 'icalendar-rrule'`
|
|
33
|
+
|
|
34
|
+
Further we have to declare the use of the "Scannable" namespace.
|
|
35
|
+
This is called a "[Refinement](https://ruby-doc.org/core-2.5.0/doc/syntax/refinements_rdoc.html)"
|
|
36
|
+
which is a _new Ruby core feature_ since Ruby 2.0.
|
|
37
|
+
|
|
38
|
+
`using Icalendar::Scannable`
|
|
39
|
+
|
|
40
|
+
Now we can inquire a calendar for all events (or tasks) within in a time span.
|
|
41
|
+
|
|
42
|
+
`scan = calendar.scan(begin_time, closing_time)`
|
|
43
|
+
|
|
44
|
+
Here is a simple example:
|
|
45
|
+
```ruby
|
|
46
|
+
require 'icalendar-rrule' # this will require all needed GEMS including the icalendar gem
|
|
47
|
+
|
|
48
|
+
using Icalendar::Scannable # this will make the function Icalendar::Calendar.scan available
|
|
49
|
+
|
|
50
|
+
# we create a calendar with one single event
|
|
51
|
+
calendar = Icalendar::Calendar.new
|
|
52
|
+
calendar.event do |e|
|
|
53
|
+
# the event starts on January first and lasts from half past eight to five o' clock
|
|
54
|
+
e.dtstart = DateTime.civil(2018, 1, 1, 8, 30)
|
|
55
|
+
e.dtend = DateTime.civil(2018, 1, 1, 17, 00)
|
|
56
|
+
e.summary = 'Working'
|
|
57
|
+
# the event repeats all working days
|
|
58
|
+
e.rrule = 'FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
begin_time = Date.new(2018, 4, 22)
|
|
62
|
+
closing_time = Date.new(2018, 4, 29)
|
|
63
|
+
|
|
64
|
+
# we are interested in the calendar entries in the last week of April
|
|
65
|
+
scan = calendar.scan(begin_time, closing_time) # that's where the magic happens
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
scan.each do |occurrence|
|
|
69
|
+
puts "#{occurrence.start_time.strftime('%a. %b. %d. %k:%M')}-#{occurrence.end_time.strftime('%k:%M')}"
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
This will produce:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
Mon. Apr. 23. 8:30-17:00
|
|
76
|
+
Tue. Apr. 24. 8:30-17:00
|
|
77
|
+
Wed. Apr. 25. 8:30-17:00
|
|
78
|
+
Thu. Apr. 26. 8:30-17:00
|
|
79
|
+
Fri. Apr. 27. 8:30-17:00
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
For a more elaborate example, please have a look at
|
|
83
|
+
<https://github.com/free-creations/sk_calendar>
|
|
84
|
+
|
|
85
|
+
## Used Libraries
|
|
86
|
+
|
|
87
|
+
- [iCalendar Gem](https://github.com/icalendar/icalendar).
|
|
88
|
+
- [Ice cube](https://github.com/seejohnrun/ice_cube)
|
|
89
|
+
- **Active Support:** see also
|
|
90
|
+
[How to Load Core Extensions](http://edgeguides.rubyonrails.org/active_support_core_extensions.html#how-to-load-core-extensions)
|
|
91
|
+
|
|
92
|
+
## Links
|
|
93
|
+
- [Wikipedia](https://en.wikipedia.org/wiki/ICalendar) article explaining the _iCalendar_ format.
|
|
94
|
+
- [RFC 5545](https://tools.ietf.org/html/rfc5545) Internet
|
|
95
|
+
Calendaring and Scheduling Core Object Specification.
|
|
96
|
+
- The Ruby [iCalendar gem](http://github.com/icalendar/icalendar) is used here as a base for
|
|
97
|
+
handling ical data.
|
|
98
|
+
- [RI_CAL](https://github.com/rubyredrick/ri_cal) is a project similar
|
|
99
|
+
to this one that aims to
|
|
100
|
+
"support important things like enumerating occurrences of repeating events".
|
|
101
|
+
A newer fork is available here: [kdgm/ri_cal](https://github.com/kdgm/ri_cal)
|
|
102
|
+
- [The deceptively complex world of calendar events and RRULEs](https://www.nylas.com/blog/calendar-events-rrules/).
|
|
103
|
+
A Blog of Jennie Lees.
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
## Contributing
|
|
107
|
+
|
|
108
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/free-creations/icalendar-rrule.
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
lib = File.expand_path('lib', __dir__)
|
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
6
|
+
# note: requiring the version file here, will make this file invisible to 'SimpleCov' the code coverage analysis tool.
|
|
7
|
+
require 'icalendar/rrule/version'
|
|
8
|
+
|
|
9
|
+
Gem::Specification.new do |gem_spec|
|
|
10
|
+
gem_spec.name = 'icalendar-rrule'
|
|
11
|
+
gem_spec.version = Icalendar::Rrule::VERSION
|
|
12
|
+
gem_spec.authors = ['Harald Postner']
|
|
13
|
+
gem_spec.email = ['harald@free-creations.de']
|
|
14
|
+
|
|
15
|
+
gem_spec.summary = 'Use this module if you want to iterate over an ICalendars with recurring events. '
|
|
16
|
+
gem_spec.description = 'This Gem adds a view to ICalendar class which expands ' \
|
|
17
|
+
'all recurring events.'
|
|
18
|
+
gem_spec.homepage = 'https://github.com/free-creations/icalendar-rrule'
|
|
19
|
+
gem_spec.license = 'MIT'
|
|
20
|
+
|
|
21
|
+
gem_spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
22
|
+
f.match(%r{^(test|spec|features)/})
|
|
23
|
+
end
|
|
24
|
+
gem_spec.bindir = 'exe'
|
|
25
|
+
gem_spec.executables = gem_spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
26
|
+
gem_spec.require_paths = ['lib']
|
|
27
|
+
|
|
28
|
+
gem_spec.add_dependency 'activesupport', '~> 5.1'
|
|
29
|
+
gem_spec.add_dependency 'icalendar', '~> 2.4'
|
|
30
|
+
gem_spec.add_dependency 'ice_cube', '~> 0.16'
|
|
31
|
+
|
|
32
|
+
gem_spec.add_development_dependency 'bundler', '~> 1.16'
|
|
33
|
+
gem_spec.add_development_dependency 'rake', '~> 10.0'
|
|
34
|
+
gem_spec.add_development_dependency 'rspec', '~> 3.7'
|
|
35
|
+
gem_spec.add_development_dependency 'rubocop', '~> 0.55.0'
|
|
36
|
+
gem_spec.add_development_dependency 'rubocop-rspec', '~> 1.24'
|
|
37
|
+
gem_spec.add_development_dependency 'simplecov', '~> 0.16'
|
|
38
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Icalendar
|
|
4
|
+
module Rrule
|
|
5
|
+
##
|
|
6
|
+
# An Occurrence represents the point of time where an event or any other component of an iCalendar happens.
|
|
7
|
+
#
|
|
8
|
+
# A component with a _repeat rule_ happens several times.
|
|
9
|
+
# Such a component is represented by a set of many __Occurrence instances__.
|
|
10
|
+
# All these occurrence instances refer to the same component called herein the __base_component__.
|
|
11
|
+
#
|
|
12
|
+
# The base_component can be one of the following:
|
|
13
|
+
#
|
|
14
|
+
# - Icalendar::Event
|
|
15
|
+
# - Icalendar::Todo
|
|
16
|
+
#
|
|
17
|
+
# furthermore it can also be one of the following:
|
|
18
|
+
#
|
|
19
|
+
# - Icalendar::Freebusy
|
|
20
|
+
# - Icalendar::Journal
|
|
21
|
+
#
|
|
22
|
+
# but these are not tested in the current version of this GEM.
|
|
23
|
+
#
|
|
24
|
+
# The Occurrence delegates reading of attributes to its underlying
|
|
25
|
+
# base_component.
|
|
26
|
+
# @example
|
|
27
|
+
# occurrence_of_event.description #=> "Meeting with Mr. X"
|
|
28
|
+
# occurrence_of_event.not_an_attribute #=> Error
|
|
29
|
+
# occurrence_of_event.description = 'new description' #=> Error
|
|
30
|
+
#
|
|
31
|
+
#
|
|
32
|
+
#
|
|
33
|
+
class Occurrence
|
|
34
|
+
include Comparable
|
|
35
|
+
using Icalendar::Schedulable
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# @return [Icalendar::Calendar] the calendar this occurrence is taken from.
|
|
39
|
+
attr_reader :base_calendar
|
|
40
|
+
##
|
|
41
|
+
# @return [Icalendar::Component] the calendar-component (an event or a task) this occurrence refers to.
|
|
42
|
+
attr_reader :base_component
|
|
43
|
+
##
|
|
44
|
+
# @return [ActiveSupport::TimeWithZone] the start of this occurrence.
|
|
45
|
+
attr_reader :start_time
|
|
46
|
+
##
|
|
47
|
+
# @return [ActiveSupport::TimeWithZone] the end of this occurrence.
|
|
48
|
+
attr_reader :end_time
|
|
49
|
+
##
|
|
50
|
+
|
|
51
|
+
##
|
|
52
|
+
# Create a new Occurrence instance.
|
|
53
|
+
#
|
|
54
|
+
# @param [Icalendar::Calendar] base_calendar the calendar that holds the component.
|
|
55
|
+
# @param [Icalendar::Component] base_component the underlying calendar-component.
|
|
56
|
+
# @param [ActiveSupport::TimeWithZone] start_time the time when this occurrence starts.
|
|
57
|
+
# (might be different to the start time of the base_component)
|
|
58
|
+
# @param [ActiveSupport::TimeWithZone] end_time the time when this occurrence starts.
|
|
59
|
+
# (might be different to the end time of the base_component)
|
|
60
|
+
#
|
|
61
|
+
def initialize(base_calendar, base_component, start_time, end_time)
|
|
62
|
+
raise ArgumentError, "'base_calendar' not of class 'Icalendar::Calendar'" unless
|
|
63
|
+
base_calendar.nil? || base_calendar.is_a?(Icalendar::Calendar)
|
|
64
|
+
raise ArgumentError, "'base_component' not of class 'Icalendar::Component'" unless
|
|
65
|
+
base_component.is_a?(Icalendar::Component)
|
|
66
|
+
raise ArgumentError, "'start_time' not of class 'ActiveSupport::TimeWithZone'" unless
|
|
67
|
+
start_time.is_a?(ActiveSupport::TimeWithZone)
|
|
68
|
+
raise ArgumentError, "'end_time' not of class 'ActiveSupport::TimeWithZone'" unless
|
|
69
|
+
end_time.is_a?(ActiveSupport::TimeWithZone)
|
|
70
|
+
|
|
71
|
+
@base_calendar = base_calendar
|
|
72
|
+
@base_component = base_component
|
|
73
|
+
@start_time = start_time
|
|
74
|
+
@end_time = end_time
|
|
75
|
+
|
|
76
|
+
super()
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# Invoked by Ruby when the Occurrence-object is sent a message it cannot handle.
|
|
81
|
+
#
|
|
82
|
+
# Reading of attributes will be delegated all to the _base component_.
|
|
83
|
+
#
|
|
84
|
+
# An attempt to set an attribute will result in an error.
|
|
85
|
+
#
|
|
86
|
+
# @param [String] method_name the symbol for the method called
|
|
87
|
+
# @param [*object] arguments the arguments that were passed to the method.
|
|
88
|
+
# @param [] block the block that was passed to the method.
|
|
89
|
+
def method_missing(method_name, *arguments, &block)
|
|
90
|
+
if method_name.to_s[-1, 1] == '='
|
|
91
|
+
# do not allow for setter methods
|
|
92
|
+
super
|
|
93
|
+
else
|
|
94
|
+
# delegate all other requests to the base component
|
|
95
|
+
base_component.send(method_name, *arguments, &block)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# @return [ActiveSupport::TimeZone] the timezone used in this component
|
|
101
|
+
def time_zone
|
|
102
|
+
base_component.component_timezone
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# Returns true if the Occurrence can respond to the given method, that is, the
|
|
107
|
+
# base component responds to the given method.
|
|
108
|
+
#
|
|
109
|
+
# @param [String] method_name the symbol for the method called
|
|
110
|
+
# @param include_private
|
|
111
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
112
|
+
if method_name.to_s[-1, 1] == '='
|
|
113
|
+
# do not allow to set attributes
|
|
114
|
+
super ## throws no method error
|
|
115
|
+
else
|
|
116
|
+
# delegate all read requests to the base component
|
|
117
|
+
base_component.respond_to?(method_name, include_private)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
# Compares this occurrence to the other.
|
|
123
|
+
# Comparison is on:
|
|
124
|
+
#
|
|
125
|
+
# 1. @start_time
|
|
126
|
+
# 2. @end_time
|
|
127
|
+
#
|
|
128
|
+
def <=>(other)
|
|
129
|
+
return nil unless other.respond_to? :start_time
|
|
130
|
+
return nil unless other.start_time.is_a?(ActiveSupport::TimeWithZone)
|
|
131
|
+
start_compare = @start_time <=> other.start_time
|
|
132
|
+
return start_compare unless start_compare.zero?
|
|
133
|
+
|
|
134
|
+
return 0 unless other.respond_to? :end_time
|
|
135
|
+
return 0 unless other.end_time.is_a?(ActiveSupport::TimeWithZone)
|
|
136
|
+
@end_time <=> other.end_time
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Icalendar
|
|
4
|
+
##
|
|
5
|
+
# Refines the Icalendar::Calendar class by adding
|
|
6
|
+
# the `scan` function to this class.
|
|
7
|
+
#
|
|
8
|
+
# @note [Refinement](https://ruby-doc.org/core-2.5.0/doc/syntax/refinements_rdoc.html)
|
|
9
|
+
# is a _Ruby core feature_ since Ruby 2.0
|
|
10
|
+
#
|
|
11
|
+
module Scannable
|
|
12
|
+
##
|
|
13
|
+
# Provides _mixin_ methods for the
|
|
14
|
+
# Icalendar::Calendar class
|
|
15
|
+
refine Icalendar::Calendar do # rubocop:disable Metrics/BlockLength
|
|
16
|
+
using Icalendar::Schedulable
|
|
17
|
+
##
|
|
18
|
+
# @param[date_time] begin_time
|
|
19
|
+
# @param[date_time] closing_time
|
|
20
|
+
# @param [Set] component_types a list of components that shall be retrieved these can be
|
|
21
|
+
# - :events
|
|
22
|
+
# - :todos
|
|
23
|
+
# Note: `:journals` and `:freebusys` are currently not tested.
|
|
24
|
+
#
|
|
25
|
+
# @return [Array] all occurrences between begin_time and closing_time
|
|
26
|
+
def scan(begin_time, closing_time, component_types = Set[:events])
|
|
27
|
+
component_types = component_types.to_set
|
|
28
|
+
result = []
|
|
29
|
+
component_types.each do |component_type|
|
|
30
|
+
result += _occurrences_between(_components(component_type), begin_time, closing_time)
|
|
31
|
+
end
|
|
32
|
+
result ||= [] # stop RubyMine to complain about uninitialized result.
|
|
33
|
+
result.sort!
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private def _components(component_type)
|
|
37
|
+
# note: events(), todos(), journals(), freebusys() are attributes added
|
|
38
|
+
# to Icalendar::Calendar by meta-programming.
|
|
39
|
+
case component_type
|
|
40
|
+
when :events then events
|
|
41
|
+
when :todos then todos
|
|
42
|
+
# :nocov:
|
|
43
|
+
when :journals then journals
|
|
44
|
+
when :freebusys then freebusys
|
|
45
|
+
# :nocov:
|
|
46
|
+
else
|
|
47
|
+
raise ArgumentError, "Unknown Component type: `#{component_type}`."
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private def _occurrences_between(components, begin_time, closing_time)
|
|
52
|
+
result = []
|
|
53
|
+
components.each do |comp|
|
|
54
|
+
occurrences = comp.schedule.occurrences_between(begin_time, closing_time)
|
|
55
|
+
occurrences.each do |oc|
|
|
56
|
+
new_oc = Icalendar::Rrule::Occurrence.new(self, comp, oc.start_time, oc.end_time)
|
|
57
|
+
result << new_oc
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ice_cube'
|
|
4
|
+
require 'active_support/time_with_zone'
|
|
5
|
+
|
|
6
|
+
module Icalendar
|
|
7
|
+
##
|
|
8
|
+
# Refines the Icalendar::Component class by adding
|
|
9
|
+
# an interface between the IceCube Gem and the the Icalendar::Component-class.
|
|
10
|
+
#
|
|
11
|
+
# __Note:__ _Refinement_ is a Ruby core feature since Ruby 2.0.
|
|
12
|
+
# @see: https://ruby-doc.org/core-2.5.0/doc/syntax/refinements_rdoc.html
|
|
13
|
+
#
|
|
14
|
+
# There are some shortcomings, the
|
|
15
|
+
# [documentation](http://ruby-doc.org/core-2.2.2/doc/syntax/refinements_rdoc.html#label-Indirect+Method+Calls)
|
|
16
|
+
# says:
|
|
17
|
+
#
|
|
18
|
+
# >When using indirect method access such as Kernel#send, Kernel#method or Kernel#respond_to?
|
|
19
|
+
# >refinements are not honored for the caller context during method lookup.
|
|
20
|
+
# >
|
|
21
|
+
# >This behavior may be changed in the future.
|
|
22
|
+
#
|
|
23
|
+
# The purpose of this module is:
|
|
24
|
+
#
|
|
25
|
+
# - normalise the handling of date and time by using ActiveSupport::TimeWithZone everywhere.
|
|
26
|
+
# - provide the methods *start_time* and *end_time* that always return something sensible, no matter
|
|
27
|
+
# how these times were defined in the original component.
|
|
28
|
+
# - provide a schedule for repeating events created by the so called **rrule**.
|
|
29
|
+
#
|
|
30
|
+
#
|
|
31
|
+
module Schedulable
|
|
32
|
+
##
|
|
33
|
+
# @!method start_time()
|
|
34
|
+
# The time when the event or task shall start.
|
|
35
|
+
# @return [ActiveSupport::TimeWithZone] a valid DateTime object
|
|
36
|
+
#
|
|
37
|
+
#
|
|
38
|
+
# @!method end_time()
|
|
39
|
+
# The time when the event or task shall end.
|
|
40
|
+
# @return [ActiveSupport::TimeWithZone] a valid DateTime object
|
|
41
|
+
#
|
|
42
|
+
|
|
43
|
+
# The start of the Unix Epoch (January 1, 1970 00:00 UTC).
|
|
44
|
+
NULL_TIME = 0
|
|
45
|
+
|
|
46
|
+
# the number of seconds in a minute
|
|
47
|
+
SEC_MIN = 60
|
|
48
|
+
# the number of seconds in an hour
|
|
49
|
+
SEC_HOUR = 60 * SEC_MIN
|
|
50
|
+
# the number of seconds in a day
|
|
51
|
+
SEC_DAY = 24 * SEC_HOUR
|
|
52
|
+
# the number of seconds in a week
|
|
53
|
+
SEC_WEEK = 7 * SEC_DAY
|
|
54
|
+
##
|
|
55
|
+
# Provides _mixin_ methods for the
|
|
56
|
+
# Icalendar::Component class
|
|
57
|
+
refine Icalendar::Component do # rubocop:disable Metrics/BlockLength
|
|
58
|
+
##
|
|
59
|
+
# Make sure, that we can always query for a _dtstart_ time.
|
|
60
|
+
# @return [Icalendar::Value, nil] a valid DateTime object or nil.
|
|
61
|
+
# @api private
|
|
62
|
+
private def _dtstart
|
|
63
|
+
dtstart
|
|
64
|
+
rescue StandardError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
##
|
|
68
|
+
# Make sure, that we can always query for a _dtend_ time.
|
|
69
|
+
# @return [Icalendar::Value, nil] a valid DateTime object or nil.
|
|
70
|
+
# @api private
|
|
71
|
+
private def _dtend
|
|
72
|
+
dtend
|
|
73
|
+
rescue StandardError
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
##
|
|
77
|
+
# Make sure, that we can always query for a _due_ date.
|
|
78
|
+
# @return [Icalendar::Value, nil] a valid DateTime object or nil.
|
|
79
|
+
# @api private
|
|
80
|
+
private def _due
|
|
81
|
+
due
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
##
|
|
86
|
+
# Make sure, that we can always query for a _the duration value.
|
|
87
|
+
# @return [Icalendar::Values::Duration, nil] a valid Duration object or nil.
|
|
88
|
+
# @api private
|
|
89
|
+
def _duration
|
|
90
|
+
duration
|
|
91
|
+
rescue StandardError
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
##
|
|
96
|
+
# @return [Integer] the number of seconds this task will last.
|
|
97
|
+
# If no duration for this task is specified, this function returns zero.
|
|
98
|
+
# @api private
|
|
99
|
+
def _duration_seconds # rubocop:disable Metrics/AbcSize
|
|
100
|
+
return _guessed_duration unless _duration
|
|
101
|
+
d = _duration
|
|
102
|
+
return _guessed_duration unless d.is_a?(Icalendar::Values::Duration)
|
|
103
|
+
d.seconds + (d.minutes * SEC_MIN) + (d.hours * SEC_HOUR) + (d.days * SEC_DAY) + (d.weeks * SEC_WEEK)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
##
|
|
107
|
+
# Make an educated guess how long this event might last according to the following definition from RFC 5545:
|
|
108
|
+
#
|
|
109
|
+
# > For cases where a "VEVENT" calendar component
|
|
110
|
+
# > specifies a "DTSTART" property with a DATE value type but no
|
|
111
|
+
# > "DTEND" nor "DURATION" property, the event's duration is taken to
|
|
112
|
+
# > be one day.
|
|
113
|
+
#
|
|
114
|
+
# @return [Integer] the number of seconds this task might last.
|
|
115
|
+
# @api private
|
|
116
|
+
def _guessed_duration
|
|
117
|
+
if _dtstart.is_a?(Icalendar::Values::Date) && _dtend.nil? && _duration.nil? && _due.nil?
|
|
118
|
+
SEC_DAY
|
|
119
|
+
else
|
|
120
|
+
0
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
##
|
|
125
|
+
# The time when the event or task shall start.
|
|
126
|
+
# @return [ActiveSupport::TimeWithZone] a valid DateTime object
|
|
127
|
+
def start_time
|
|
128
|
+
if _dtstart
|
|
129
|
+
_to_time_with_zone(_dtstart)
|
|
130
|
+
elsif _due
|
|
131
|
+
_to_time_with_zone(_due.to_i - _duration_seconds)
|
|
132
|
+
else
|
|
133
|
+
_to_time_with_zone(NULL_TIME)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
##
|
|
138
|
+
# The time when the event or task shall end.
|
|
139
|
+
# @return [ActiveSupport::TimeWithZone] a valid DateTime object
|
|
140
|
+
def end_time # rubocop:disable Metrics/AbcSize
|
|
141
|
+
if _due
|
|
142
|
+
_to_time_with_zone(_due)
|
|
143
|
+
elsif _dtend
|
|
144
|
+
_to_time_with_zone(_dtend)
|
|
145
|
+
elsif _dtstart
|
|
146
|
+
_to_time_with_zone(_dtstart.to_i + _duration_seconds)
|
|
147
|
+
else
|
|
148
|
+
_to_time_with_zone(NULL_TIME + _duration_seconds)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
##
|
|
153
|
+
# Heuristic to determine whether the event is scheduled
|
|
154
|
+
# for a date without precising the exact time of day.
|
|
155
|
+
# @return [Boolean] true if the component is scheduled for a date, false otherwise.
|
|
156
|
+
def all_day?
|
|
157
|
+
_dtstart.is_a?(Icalendar::Values::Date) ||
|
|
158
|
+
(start_time == start_time.beginning_of_day && end_time == end_time.beginning_of_day)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
##
|
|
162
|
+
# @return [Boolean] true if the duration of the event spans more than one day.
|
|
163
|
+
def multi_day?
|
|
164
|
+
start_time.next_day.beginning_of_day < end_time
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
##
|
|
168
|
+
# Make sure, that we can always query for a _rrule_ array.
|
|
169
|
+
# @return [array] an array of _ical repeat-rules_ (or an empty array
|
|
170
|
+
# if no repeat-rules are defined for this component).
|
|
171
|
+
# @api private
|
|
172
|
+
def _rrules
|
|
173
|
+
Array(rrule).flatten.map(&:value_ical)
|
|
174
|
+
rescue StandardError
|
|
175
|
+
[]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
##
|
|
179
|
+
# Make sure, that we can always query for an _exdate_ array.
|
|
180
|
+
# @return [array<ActiveSupport::TimeWithZone>] an array of _ical exdates_ (or an empty array
|
|
181
|
+
# if no repeat-rules are defined for this component).
|
|
182
|
+
# @api private
|
|
183
|
+
def _exdates
|
|
184
|
+
Array(exdate).flatten
|
|
185
|
+
rescue StandardError
|
|
186
|
+
[]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
##
|
|
190
|
+
# Make sure, that we can always query for the Recurrence ID.
|
|
191
|
+
#
|
|
192
|
+
# From RFC 5545:
|
|
193
|
+
#
|
|
194
|
+
# ```
|
|
195
|
+
# 3.8.4.4. Recurrence ID
|
|
196
|
+
#
|
|
197
|
+
# This property is used in conjunction with the "UID" and
|
|
198
|
+
# "SEQUENCE" properties to identify a specific instance of a
|
|
199
|
+
# recurring "VEVENT", "VTODO", or "VJOURNAL" calendar component.
|
|
200
|
+
#
|
|
201
|
+
# ```
|
|
202
|
+
#
|
|
203
|
+
# @return [ActiveSupport::TimeWithZone] the original value of the "DTSTART" property
|
|
204
|
+
# of the recurrence instance.
|
|
205
|
+
# @api private
|
|
206
|
+
def _recurrence_id
|
|
207
|
+
recurrence_id
|
|
208
|
+
rescue StandardError
|
|
209
|
+
nil
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
##
|
|
213
|
+
# Is this component a replacement for a certain repeat- occurrence.
|
|
214
|
+
# @return [Boolean] true if this component replaces a repeat- occurrence.
|
|
215
|
+
# @api private
|
|
216
|
+
def _is_substitute?
|
|
217
|
+
!_recurrence_id.nil?
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
##
|
|
221
|
+
# @return [Array<Icalendar::Component>] the container that holds this component.
|
|
222
|
+
# @api private
|
|
223
|
+
def _parent_set
|
|
224
|
+
return [] unless respond_to?(:parent)
|
|
225
|
+
return [] unless parent.is_a?(Icalendar::Calendar)
|
|
226
|
+
|
|
227
|
+
case self
|
|
228
|
+
when Icalendar::Event then parent.events
|
|
229
|
+
when Icalendar::Todo then parent.todos
|
|
230
|
+
when Icalendar::Journal then parent.journals
|
|
231
|
+
else
|
|
232
|
+
[]
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
##
|
|
237
|
+
# Like the for _exdates, also for these dates do not schedule recurrence items.
|
|
238
|
+
#
|
|
239
|
+
# @return [array<ActiveSupport::TimeWithZone>] an array of dates.
|
|
240
|
+
# @api private
|
|
241
|
+
def _overwritten_dates
|
|
242
|
+
result = []
|
|
243
|
+
_parent_set.each do |event|
|
|
244
|
+
next unless uid == event.uid
|
|
245
|
+
next unless event._is_substitute?
|
|
246
|
+
next if _recurrence_id == event._recurrence_id # do not add myself
|
|
247
|
+
result << event._recurrence_id
|
|
248
|
+
end
|
|
249
|
+
result
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
##
|
|
253
|
+
# Make sure, that we can always query for an rdate(Recurrence Date) array.
|
|
254
|
+
# @return [array] an array of _ical rdates_ (or an empty array
|
|
255
|
+
# if no repeat-rules are defined for this component).
|
|
256
|
+
# @api private
|
|
257
|
+
def _rdates
|
|
258
|
+
Array(rdate).flatten
|
|
259
|
+
rescue StandardError
|
|
260
|
+
[]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
##
|
|
264
|
+
# Creates a schedule for this event
|
|
265
|
+
# @return [IceCube::Schedule]
|
|
266
|
+
def schedule # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
267
|
+
schedule = IceCube::Schedule.new
|
|
268
|
+
schedule.start_time = start_time
|
|
269
|
+
schedule.end_time = end_time
|
|
270
|
+
_rrules.each do |rrule|
|
|
271
|
+
ice_cube_recurrence_rule = IceCube::Rule.from_ical(rrule)
|
|
272
|
+
schedule.add_recurrence_rule(ice_cube_recurrence_rule)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
_exdates.each do |time|
|
|
276
|
+
schedule.add_exception_time(_to_time_with_zone(time))
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
_overwritten_dates.each do |time|
|
|
280
|
+
schedule.add_exception_time(_to_time_with_zone(time))
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
rdates = _rdates
|
|
284
|
+
rdates.each do |time|
|
|
285
|
+
schedule.add_recurrence_time(_to_time_with_zone(time))
|
|
286
|
+
end
|
|
287
|
+
schedule
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
##
|
|
291
|
+
# Transform the given object into an object of type `ActiveSupport::TimeWithZone`.
|
|
292
|
+
#
|
|
293
|
+
# Further, try to make sure, that all time-objects of this component are defined in the same timezone.
|
|
294
|
+
#
|
|
295
|
+
# @param [Object] date_time an object that represents a time.
|
|
296
|
+
# @param [ActiveSupport::TimeZone] timezone the timezone to be used. If nil, the timezone will be guessed.
|
|
297
|
+
# @return [ActiveSupport::TimeWithZone] if the given object satisfies all conditions it is returned unchanged.
|
|
298
|
+
# Otherwise the method attempts to "correct" the given Object.
|
|
299
|
+
#
|
|
300
|
+
# rubocop:disable Metrics/MethodLength,Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
301
|
+
def _to_time_with_zone(date_time, timezone = nil)
|
|
302
|
+
timezone ||= component_timezone
|
|
303
|
+
|
|
304
|
+
# For Icalendar::Values::DateTime, we can extract the ical value. Which probably is already what we want.
|
|
305
|
+
date_time_value = if date_time.is_a?(Icalendar::Values::DateTime)
|
|
306
|
+
date_time.value
|
|
307
|
+
else
|
|
308
|
+
date_time
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
if date_time_value.is_a?(ActiveSupport::TimeWithZone)
|
|
312
|
+
# the class is correct
|
|
313
|
+
# if the timezone is also correct, we'll give back the input object.
|
|
314
|
+
return date_time_value if date_time_value.time_zone == timezone
|
|
315
|
+
|
|
316
|
+
# convert to the requested timezone and return it.
|
|
317
|
+
return date_time_value.in_time_zone(timezone)
|
|
318
|
+
|
|
319
|
+
elsif date_time_value.is_a?(DateTime)
|
|
320
|
+
return date_time_value.in_time_zone(timezone)
|
|
321
|
+
|
|
322
|
+
elsif date_time_value.is_a?(Icalendar::Values::Date)
|
|
323
|
+
return _date_to_time_with_zone(date_time_value, timezone)
|
|
324
|
+
|
|
325
|
+
elsif date_time_value.is_a?(Date)
|
|
326
|
+
return _date_to_time_with_zone(date_time_value, timezone)
|
|
327
|
+
|
|
328
|
+
elsif date_time_value.respond_to?(:to_time)
|
|
329
|
+
return timezone.at(date_time_value.to_time)
|
|
330
|
+
|
|
331
|
+
elsif date_time_value.respond_to?(:to_i)
|
|
332
|
+
# lets interpret the given value as the number of seconds since the Epoch (January 1, 1970 00:00 UTC).
|
|
333
|
+
return timezone.at(date_time_value.to_i)
|
|
334
|
+
|
|
335
|
+
end
|
|
336
|
+
# Oops, the given object is unusable, we'll give back the NULL_DATE
|
|
337
|
+
timezone.at(NULL_TIME)
|
|
338
|
+
end
|
|
339
|
+
# rubocop:enable Metrics/MethodLength,Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
|
|
340
|
+
|
|
341
|
+
##
|
|
342
|
+
# Convert a date into the corresponding TimeWithZone value.
|
|
343
|
+
# @param [#to_date] date a calendar date.
|
|
344
|
+
# @param [ActiveSupport::TimeZone] timezone the timezone to be used.
|
|
345
|
+
# @return [ActiveSupport::TimeWithZone] mid-night in the given timezone at the given date.
|
|
346
|
+
def _date_to_time_with_zone(date, timezone)
|
|
347
|
+
d = date.to_date
|
|
348
|
+
timezone.local(d.year, d.month, d.day)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
##
|
|
352
|
+
# Heuristic to determine the best timezone that shall be used in this component.
|
|
353
|
+
# @return [ActiveSupport::TimeZone] the unique timezone used in this component
|
|
354
|
+
def component_timezone
|
|
355
|
+
# let's try sequentially, the first non-nil wins.
|
|
356
|
+
timezone ||= _extract_timezone(_dtend)
|
|
357
|
+
timezone ||= _extract_timezone(_dtstart)
|
|
358
|
+
timezone ||= _extract_timezone(_due)
|
|
359
|
+
timezone ||= _extract_calendar_timezone
|
|
360
|
+
|
|
361
|
+
# as a last resort we'll use the Coordinated Universal Time (UTC).
|
|
362
|
+
timezone || ActiveSupport::TimeZone['UTC']
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
##
|
|
366
|
+
# Try to determine this components time zone by inspecting the parents calendar.
|
|
367
|
+
# @return[ActiveSupport::TimeZone, nil] the first valid timezone found in the
|
|
368
|
+
# parent calender or nil if none could be found.
|
|
369
|
+
#
|
|
370
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
371
|
+
def _extract_calendar_timezone
|
|
372
|
+
return nil unless parent
|
|
373
|
+
return nil unless parent.is_a?(Icalendar::Calendar)
|
|
374
|
+
calendar_timezones = parent.timezones
|
|
375
|
+
calendar_timezones.each do |tz|
|
|
376
|
+
break unless tz.valid?(true)
|
|
377
|
+
ugly_tzid = tz.tzid
|
|
378
|
+
break unless ugly_tzid
|
|
379
|
+
tzid = Array(ugly_tzid).first.to_s.gsub(/^(["'])|(["'])$/, '')
|
|
380
|
+
tz_found = ActiveSupport::TimeZone[tzid]
|
|
381
|
+
return tz_found if tz_found
|
|
382
|
+
end
|
|
383
|
+
nil
|
|
384
|
+
rescue StandardError
|
|
385
|
+
nil
|
|
386
|
+
end
|
|
387
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
388
|
+
|
|
389
|
+
##
|
|
390
|
+
# Get the timezone from the given object trying different methods to find an indication in the object.
|
|
391
|
+
# @param [Object] date_time an object from which we shall determine the time zone.
|
|
392
|
+
# @return [ActiveSupport::TimeZone, nil] the timezone used by the parameter or nil if no timezone has been set.
|
|
393
|
+
# @api private
|
|
394
|
+
def _extract_timezone(date_time)
|
|
395
|
+
timezone ||= _extract_ical_time_zone(date_time) # try with ical parameter
|
|
396
|
+
timezone ||= _extract_act_sup_timezone(date_time) # is the given value already ActiveSupport::TimeWithZone?
|
|
397
|
+
timezone || _extract_value_time_zone(date_time) # is the ical.value of type ActiveSupport::TimeWithZone?
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
##
|
|
401
|
+
# Get the timezone from the given object, assuming it is an ActiveSupport::TimeWithZone.
|
|
402
|
+
# @param [Object] date_time an object from which we shall determine the time zone.
|
|
403
|
+
# @return [ActiveSupport::TimeZone, nil] the timezone or nil if the operation could not be performed.
|
|
404
|
+
# @api private
|
|
405
|
+
def _extract_act_sup_timezone(date_time)
|
|
406
|
+
return nil unless date_time.is_a?(ActiveSupport::TimeWithZone)
|
|
407
|
+
date_time.time_zone
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
##
|
|
411
|
+
# Get the timezone from the given object, assuming it can be extracted from `ical_value.value.time_zone`
|
|
412
|
+
# @param [Object] ical_value an object from which we shall determine the time zone.
|
|
413
|
+
# @return [ActiveSupport::TimeZone, nil] the timezone used by the parameter
|
|
414
|
+
# or nil if the operation could not be performed.
|
|
415
|
+
# @api private
|
|
416
|
+
def _extract_value_time_zone(ical_value)
|
|
417
|
+
return nil unless ical_value.is_a?(Icalendar::Value)
|
|
418
|
+
return nil unless ical_value.value.is_a?(ActiveSupport::TimeWithZone)
|
|
419
|
+
ical_value.value.time_zone
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
##
|
|
423
|
+
# Get the timezone from the given object, assuming it can be extracted from ical params.
|
|
424
|
+
# @param [Icalendar::Value] ical_value an ical value that (probably) supports a time zone identifier.
|
|
425
|
+
# @return [ActiveSupport::TimeZone, nil] the timezone referred to by the ical_value or nil.
|
|
426
|
+
# @api private
|
|
427
|
+
def _extract_ical_time_zone(ical_value)
|
|
428
|
+
return nil unless ical_value.is_a?(Icalendar::Value)
|
|
429
|
+
return nil unless ical_value.respond_to?(:ical_params)
|
|
430
|
+
ugly_tzid = ical_value.ical_params.fetch('tzid', nil)
|
|
431
|
+
return nil if ugly_tzid.nil?
|
|
432
|
+
tzid = Array(ugly_tzid).first.to_s.gsub(/^(["'])|(["'])$/, '')
|
|
433
|
+
ActiveSupport::TimeZone[tzid]
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: icalendar-rrule
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.5
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Harald Postner
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2018-07-25 00:00:00.000000000 Z
|
|
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: '5.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: icalendar
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.4'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.4'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: ice_cube
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0.16'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0.16'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: bundler
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '1.16'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '1.16'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rake
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '10.0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '10.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: rspec
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '3.7'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '3.7'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rubocop
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 0.55.0
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: 0.55.0
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rubocop-rspec
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '1.24'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '1.24'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: simplecov
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0.16'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0.16'
|
|
139
|
+
description: This Gem adds a view to ICalendar class which expands all recurring events.
|
|
140
|
+
email:
|
|
141
|
+
- harald@free-creations.de
|
|
142
|
+
executables: []
|
|
143
|
+
extensions: []
|
|
144
|
+
extra_rdoc_files: []
|
|
145
|
+
files:
|
|
146
|
+
- ".gitignore"
|
|
147
|
+
- ".rspec"
|
|
148
|
+
- ".rubocop.yml"
|
|
149
|
+
- ".travis.yml"
|
|
150
|
+
- ".yardopts"
|
|
151
|
+
- Gemfile
|
|
152
|
+
- Gemfile.lock
|
|
153
|
+
- LICENSE.txt
|
|
154
|
+
- README.md
|
|
155
|
+
- Rakefile
|
|
156
|
+
- icalendar-rrule.gemspec
|
|
157
|
+
- lib/icalendar-rrule.rb
|
|
158
|
+
- lib/icalendar/rrule.rb
|
|
159
|
+
- lib/icalendar/rrule/occurrence.rb
|
|
160
|
+
- lib/icalendar/rrule/version.rb
|
|
161
|
+
- lib/icalendar/scannable-calendar.rb
|
|
162
|
+
- lib/icalendar/schedulable-component.rb
|
|
163
|
+
homepage: https://github.com/free-creations/icalendar-rrule
|
|
164
|
+
licenses:
|
|
165
|
+
- MIT
|
|
166
|
+
metadata: {}
|
|
167
|
+
post_install_message:
|
|
168
|
+
rdoc_options: []
|
|
169
|
+
require_paths:
|
|
170
|
+
- lib
|
|
171
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
172
|
+
requirements:
|
|
173
|
+
- - ">="
|
|
174
|
+
- !ruby/object:Gem::Version
|
|
175
|
+
version: '0'
|
|
176
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
177
|
+
requirements:
|
|
178
|
+
- - ">="
|
|
179
|
+
- !ruby/object:Gem::Version
|
|
180
|
+
version: '0'
|
|
181
|
+
requirements: []
|
|
182
|
+
rubyforge_project:
|
|
183
|
+
rubygems_version: 2.7.6
|
|
184
|
+
signing_key:
|
|
185
|
+
specification_version: 4
|
|
186
|
+
summary: Use this module if you want to iterate over an ICalendars with recurring
|
|
187
|
+
events.
|
|
188
|
+
test_files: []
|