ics_parser 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6cd9cb4de61ebafff4c36462820aac8f46fa839f
4
+ data.tar.gz: 5b8ae88900375e3cd0102ac70ab840dc6afa7edd
5
+ SHA512:
6
+ metadata.gz: 04285a82a6adbabfb8b3ac1154a016a7e02e28be06cb5df1f1f102dac557f9f372b27894ad8b410fa96ead56fee9d6a9850b48e218a89de4f1a4ca4e9fe89056
7
+ data.tar.gz: 7e0d3503fa4228dad2df895c1f1f69c87bbf4bd898c5ab12715cfe42d958b58a3191d5a77a2a7a1fb1b352019703ff4516a606aaa572f2525c00ed82861814d3
data/.gitignore ADDED
@@ -0,0 +1,35 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Specific to RubyMotion:
13
+ .dat*
14
+ .repl_history
15
+ build/
16
+
17
+ ## Documentation cache and generated files:
18
+ /.yardoc/
19
+ /_yardoc/
20
+ /doc/
21
+ /rdoc/
22
+
23
+ ## Environment normalisation:
24
+ /.bundle/
25
+ /vendor/bundle
26
+ /lib/bundler/man/
27
+
28
+ # for a library or gem, you might want to ignore these files since the code is
29
+ # intended to run in multiple environments; otherwise, check them in:
30
+ # Gemfile.lock
31
+ # .ruby-version
32
+ # .ruby-gemset
33
+
34
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
35
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ IcsParser
2
+ =========
3
+
4
+ Reads an ICalendar file and returns the events in an array.
5
+
6
+ Also supports recurring events with excluded dates (using EXDATE).
7
+
8
+ It is not complete and I have only tested it on my feed (exported from Apple iCloud)
9
+ so do not rely on it.
10
+
11
+ # Example
12
+
13
+ ```
14
+ require 'ics_parser'
15
+
16
+ parser = IcsParser.from_file('path/to/file')
17
+ # or parser = IcsParser.from_string(calendar)
18
+ events = parser.events
19
+
20
+ events.each do |event|
21
+ puts event.summary # string
22
+ puts event.starts_at # Time
23
+ puts event.ends_at # Time
24
+ end
25
+ ```
26
+
27
+ # Installation
28
+
29
+ ```
30
+ gem install ics_parser
31
+ ```
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "ics_parser"
7
+ spec.version = '0.0.1'
8
+ spec.authors = ["Matúš Tomlein"]
9
+ spec.email = ["matus@tomlein.org"]
10
+ spec.summary = %q{Reads iCalendar feeds and returns events in an array.}
11
+ spec.description = %q{}
12
+ spec.homepage = "https://github.com/matus-tomlein/ics_parser"
13
+ spec.license = "MIT"
14
+
15
+ spec.files = `git ls-files -z`.split("\x0")
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_development_dependency "bundler", "~> 1.6"
21
+ spec.add_development_dependency "rake"
22
+ spec.add_development_dependency "rspec", "~> 3.0"
23
+ end
@@ -0,0 +1,121 @@
1
+ require 'ostruct'
2
+ require 'time'
3
+
4
+ class EventParser
5
+ def initialize(calendar_content)
6
+ @calendar_content = calendar_content
7
+ end
8
+
9
+ def events
10
+ events = []
11
+
12
+ get_events_in_calendar.each do |event|
13
+ summary = parse_summary event
14
+ starts_at = parse_starts_at event
15
+ ends_at = parse_ends_at event
16
+
17
+ if recurring_event? event
18
+ recurring_dates = parse_recurring_dates(event)
19
+
20
+ recurring_dates.each do |recurring_starts_at, recurring_ends_at|
21
+ events << new_event(summary,
22
+ recurring_starts_at,
23
+ recurring_ends_at)
24
+ end
25
+ else
26
+ events << new_event(summary,
27
+ starts_at,
28
+ ends_at)
29
+ end
30
+ end
31
+
32
+ events
33
+ end
34
+
35
+ private
36
+
37
+ def new_event(summary, starts_at, ends_at)
38
+ OpenStruct.new(summary: summary,
39
+ starts_at: starts_at,
40
+ ends_at: ends_at)
41
+ end
42
+
43
+ def get_events_in_calendar
44
+ beginnings = @calendar_content.split('BEGIN:VEVENT')
45
+ events = []
46
+
47
+ beginnings.each do |event_beginning|
48
+ next unless event_beginning.include? 'END:VEVENT'
49
+
50
+ events << event_beginning.split('END:VEVENT').first
51
+ end
52
+
53
+ events
54
+ end
55
+
56
+ def parse_summary(event)
57
+ /SUMMARY:(.*)/.match(event)[1]
58
+ end
59
+
60
+ def parse_starts_at(event)
61
+ Time.parse(/DTSTART[\w\/=;]*:(\d+T\d+)/.match(event)[1])
62
+ end
63
+
64
+ def parse_ends_at(event)
65
+ Time.parse(/DTEND[\w\/=;]*:(\d+T\d+)/.match(event)[1])
66
+ end
67
+
68
+ def recurring_event?(event)
69
+ event.include? 'RRULE:FREQ=WEEKLY;'
70
+ end
71
+
72
+ def parse_recurring_dates(event)
73
+ recurring_dates = {}
74
+ starts_at = parse_starts_at event
75
+ ends_at = parse_ends_at event
76
+ summary = parse_summary event
77
+
78
+ if event.include? 'RRULE:FREQ=WEEKLY;COUNT='
79
+ count = /RRULE:FREQ=WEEKLY;COUNT=(\d+)/.match(event)[1].to_i
80
+
81
+ count.times do |i|
82
+ recurring_starts_at = add_days_to_time(starts_at, i * 7)
83
+ recurring_ends_at = add_days_to_time(ends_at, i * 7)
84
+
85
+ unless is_date_excluded?(recurring_starts_at, event)
86
+ recurring_dates[recurring_starts_at] = recurring_ends_at
87
+ end
88
+ end
89
+
90
+ elsif event.include? 'RRULE:FREQ=WEEKLY;UNTIL='
91
+ until_time = Time.parse(/RRULE:FREQ=WEEKLY;UNTIL=(\d+T\d+)/.match(event)[1])
92
+
93
+ last_starts_at = starts_at
94
+ last_ends_at = ends_at
95
+
96
+ while last_starts_at <= until_time
97
+ unless is_date_excluded?(last_starts_at, event)
98
+ recurring_dates[last_starts_at] = last_ends_at
99
+ end
100
+
101
+ last_starts_at = add_days_to_time(last_starts_at, 7)
102
+ last_ends_at = add_days_to_time(last_ends_at, 7)
103
+ end
104
+
105
+ else
106
+ raise "Unsupported recurring event type: #{summary}"
107
+ end
108
+
109
+ recurring_dates
110
+ end
111
+
112
+ def is_date_excluded?(date, event)
113
+ date_str = date.strftime '%Y%m%dT%I%M%S'
114
+ not_excluded = /EXDATE[\w\/=;]*:#{date_str}/.match(event).nil?
115
+ !not_excluded
116
+ end
117
+
118
+ def add_days_to_time(time, days)
119
+ time + (days * 24 * 60 * 60)
120
+ end
121
+ end
data/lib/ics_parser.rb ADDED
@@ -0,0 +1,19 @@
1
+ require_relative 'event_parser'
2
+
3
+ class IcsParser
4
+ def self.from_file(file)
5
+ from_string(File.read(file))
6
+ end
7
+
8
+ def self.from_string(text)
9
+ return IcsParser.new(text)
10
+ end
11
+
12
+ def initialize(calendar_content)
13
+ @calendar_content = calendar_content
14
+ end
15
+
16
+ def events
17
+ EventParser.new(@calendar_content).events
18
+ end
19
+ end
@@ -0,0 +1,100 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe EventParser do
4
+ let(:sample_data) { File.read 'spec/sample.ics' }
5
+ let(:parser) { EventParser.new(sample_data) }
6
+
7
+ context 'one time event' do
8
+ let(:event) do
9
+ parser.events.find {|event| event.summary == 'one time event' }
10
+ end
11
+
12
+ it 'has the right start time' do
13
+ expect(event.starts_at).to eq Time.new(2015, 2, 24, 12, 30)
14
+ end
15
+
16
+ it 'has the right end time' do
17
+ expect(event.ends_at).to eq Time.new(2015, 2, 24, 13, 15)
18
+ end
19
+ end
20
+
21
+ context 'recurring weekly event' do
22
+ let(:events) do
23
+ parser.events.find_all {|event| event.summary == 'recurring weekly event' }
24
+ end
25
+
26
+ it 'found 7 events' do
27
+ expect(events.size).to eq 7
28
+ end
29
+
30
+ it 'have the right start times' do
31
+ expected_times = [
32
+ Time.new(2015, 4, 15, 14, 0),
33
+ Time.new(2015, 4, 22, 14, 0),
34
+ Time.new(2015, 4, 29, 14, 0),
35
+ Time.new(2015, 5, 6, 14, 0),
36
+ Time.new(2015, 5, 13, 14, 0),
37
+ Time.new(2015, 5, 20, 14, 0),
38
+ Time.new(2015, 5, 27, 14, 0)
39
+ ]
40
+ times = events.map {|event| event.starts_at }
41
+ expect(times).to eq expected_times
42
+ end
43
+
44
+ it 'have the right end times' do
45
+ expected_times = [
46
+ Time.new(2015, 4, 15, 16, 0),
47
+ Time.new(2015, 4, 22, 16, 0),
48
+ Time.new(2015, 4, 29, 16, 0),
49
+ Time.new(2015, 5, 6, 16, 0),
50
+ Time.new(2015, 5, 13, 16, 0),
51
+ Time.new(2015, 5, 20, 16, 0),
52
+ Time.new(2015, 5, 27, 16, 0)
53
+ ]
54
+ times = events.map {|event| event.ends_at }
55
+ expect(times).to eq expected_times
56
+ end
57
+ end
58
+
59
+ context 'recurring event with exdates' do
60
+ let(:events) do
61
+ parser.events.find_all {|event| event.summary == 'recurring event with exdates' }
62
+ end
63
+
64
+ it 'found some events' do
65
+ expect(events.size).to be > 1
66
+ end
67
+
68
+ it 'contains the correct dates' do
69
+ times = events.map {|event| event.starts_at }
70
+
71
+ expect(times).to include Time.new(2014, 11, 19, 9, 0)
72
+ expect(times).to include Time.new(2014, 11, 26, 9, 0)
73
+ end
74
+
75
+ it 'didn\'t find the excluded events' do
76
+ times = events.map {|event| event.starts_at }
77
+
78
+ excluded_times = [
79
+ Time.new(2015, 4, 8, 9, 0),
80
+ Time.new(2015, 6, 10, 9, 0),
81
+ Time.new(2015, 4, 15, 9, 0),
82
+ Time.new(2015, 5, 20, 9, 0),
83
+ Time.new(2015, 3, 25, 9, 0),
84
+ Time.new(2015, 6, 3, 9, 0),
85
+ Time.new(2014, 12, 3, 9, 0),
86
+ Time.new(2015, 3, 11, 9, 0),
87
+ Time.new(2015, 7, 8, 9, 0),
88
+ Time.new(2014, 12, 24, 9, 0),
89
+ Time.new(2014, 12, 31, 9, 0),
90
+ Time.new(2015, 7, 1, 9, 0),
91
+ Time.new(2015, 6, 24, 9, 0),
92
+ Time.new(2015, 4, 1, 9, 0),
93
+ ]
94
+
95
+ excluded_times.each do |excluded_time|
96
+ expect(times).not_to include excluded_time
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,5 @@
1
+ require_relative 'spec_helper'
2
+
3
+ describe IcsParser do
4
+ let(:sample_data) { File.read 'spec/sample.ics' }
5
+ end
data/spec/sample.ics ADDED
@@ -0,0 +1,71 @@
1
+ BEGIN:VCALENDAR
2
+ CALSCALE:GREGORIAN
3
+ VERSION:2.0
4
+ METHOD:PUBLISH
5
+ X-WR-CALNAME:Supervision
6
+ X-WR-TIMEZONE:Europe/Copenhagen
7
+ X-APPLE-CALENDAR-COLOR:#FFCC00
8
+ BEGIN:VTIMEZONE
9
+ TZID:Europe/Copenhagen
10
+ BEGIN:DAYLIGHT
11
+ TZOFFSETFROM:+0100
12
+ RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
13
+ DTSTART:19810329T020000
14
+ TZNAME:CEST
15
+ TZOFFSETTO:+0200
16
+ END:DAYLIGHT
17
+ BEGIN:STANDARD
18
+ TZOFFSETFROM:+0200
19
+ RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
20
+ DTSTART:19961027T030000
21
+ TZNAME:CET
22
+ TZOFFSETTO:+0100
23
+ END:STANDARD
24
+ END:VTIMEZONE
25
+ BEGIN:VEVENT
26
+ CREATED:20150614T194602Z
27
+ UID:3DE997B6-EB52-4893-A922-6A1BCB35340F
28
+ RRULE:FREQ=WEEKLY;COUNT=7
29
+ DTEND;TZID=Europe/Copenhagen:20150415T160000
30
+ TRANSP:OPAQUE
31
+ SUMMARY:recurring weekly event
32
+ DTSTART;TZID=Europe/Copenhagen:20150415T140000
33
+ DTSTAMP:20150614T194602Z
34
+ SEQUENCE:0
35
+ END:VEVENT
36
+ BEGIN:VEVENT
37
+ CREATED:20150614T194154Z
38
+ UID:7B924445-C7BC-4F0A-BCFD-EFBF4394EAFA
39
+ DTEND;TZID=Europe/Copenhagen:20150224T131500
40
+ TRANSP:OPAQUE
41
+ SUMMARY:one time event
42
+ DTSTART;TZID=Europe/Copenhagen:20150224T123000
43
+ DTSTAMP:20150614T194154Z
44
+ SEQUENCE:0
45
+ END:VEVENT
46
+ BEGIN:VEVENT
47
+ CREATED:20150614T193544Z
48
+ UID:6371A3DF-17C9-47B7-9277-F7D2596CCD36
49
+ RRULE:FREQ=WEEKLY;UNTIL=20150714T235959Z
50
+ DTEND;TZID=Europe/Copenhagen:20141119T100000
51
+ EXDATE;TZID=Europe/Copenhagen:20150408T090000
52
+ EXDATE;TZID=Europe/Copenhagen:20150610T090000
53
+ EXDATE;TZID=Europe/Copenhagen:20150415T090000
54
+ EXDATE;TZID=Europe/Copenhagen:20150520T090000
55
+ EXDATE;TZID=Europe/Copenhagen:20150325T090000
56
+ EXDATE;TZID=Europe/Copenhagen:20150603T090000
57
+ EXDATE;TZID=Europe/Copenhagen:20141203T090000
58
+ EXDATE;TZID=Europe/Copenhagen:20150311T090000
59
+ EXDATE;TZID=Europe/Copenhagen:20150708T090000
60
+ EXDATE;TZID=Europe/Copenhagen:20141224T090000
61
+ EXDATE;TZID=Europe/Copenhagen:20141231T090000
62
+ EXDATE;TZID=Europe/Copenhagen:20150701T090000
63
+ EXDATE;TZID=Europe/Copenhagen:20150624T090000
64
+ EXDATE;TZID=Europe/Copenhagen:20150401T090000
65
+ TRANSP:OPAQUE
66
+ SUMMARY:recurring event with exdates
67
+ DTSTART;TZID=Europe/Copenhagen:20141119T090000
68
+ DTSTAMP:20150614T193544Z
69
+ SEQUENCE:0
70
+ END:VEVENT
71
+ END:VCALENDAR
@@ -0,0 +1,4 @@
1
+ require File.expand_path("../../lib/ics_parser", __FILE__)
2
+
3
+ RSpec.configure do |config|
4
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ics_parser
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Matúš Tomlein
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-07-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.6'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: ''
56
+ email:
57
+ - matus@tomlein.org
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - Gemfile
64
+ - README.md
65
+ - ics_parser.gemspec
66
+ - lib/event_parser.rb
67
+ - lib/ics_parser.rb
68
+ - spec/event_parser_spec.rb
69
+ - spec/ics_parser_spec.rb
70
+ - spec/sample.ics
71
+ - spec/spec_helper.rb
72
+ homepage: https://github.com/matus-tomlein/ics_parser
73
+ licenses:
74
+ - MIT
75
+ metadata: {}
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project:
92
+ rubygems_version: 2.2.2
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Reads iCalendar feeds and returns events in an array.
96
+ test_files:
97
+ - spec/event_parser_spec.rb
98
+ - spec/ics_parser_spec.rb
99
+ - spec/sample.ics
100
+ - spec/spec_helper.rb