ics_parser 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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