tilia-vobject 4.0.0.pre.alpha2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rubocop.yml +32 -0
- data/.simplecov +4 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.sabre.md +626 -0
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +68 -0
- data/LICENSE +27 -0
- data/LICENSE.sabre +27 -0
- data/README.md +63 -0
- data/Rakefile +17 -0
- data/bin/vobject +7 -0
- data/lib/tilia/v_object/birthday_calendar_generator.rb +142 -0
- data/lib/tilia/v_object/cli.rb +582 -0
- data/lib/tilia/v_object/component/available.rb +107 -0
- data/lib/tilia/v_object/component/v_alarm.rb +114 -0
- data/lib/tilia/v_object/component/v_availability.rb +128 -0
- data/lib/tilia/v_object/component/v_calendar.rb +468 -0
- data/lib/tilia/v_object/component/v_card.rb +457 -0
- data/lib/tilia/v_object/component/v_event.rb +127 -0
- data/lib/tilia/v_object/component/v_free_busy.rb +81 -0
- data/lib/tilia/v_object/component/v_journal.rb +75 -0
- data/lib/tilia/v_object/component/v_time_zone.rb +51 -0
- data/lib/tilia/v_object/component/v_todo.rb +147 -0
- data/lib/tilia/v_object/component.rb +591 -0
- data/lib/tilia/v_object/date_time_parser.rb +486 -0
- data/lib/tilia/v_object/document.rb +218 -0
- data/lib/tilia/v_object/element_list.rb +18 -0
- data/lib/tilia/v_object/eof_exception.rb +8 -0
- data/lib/tilia/v_object/free_busy_data.rb +149 -0
- data/lib/tilia/v_object/free_busy_generator.rb +465 -0
- data/lib/tilia/v_object/i_tip/broker.rb +909 -0
- data/lib/tilia/v_object/i_tip/i_tip_exception.rb +9 -0
- data/lib/tilia/v_object/i_tip/message.rb +109 -0
- data/lib/tilia/v_object/i_tip/same_organizer_for_all_components_exception.rb +13 -0
- data/lib/tilia/v_object/i_tip.rb +10 -0
- data/lib/tilia/v_object/node.rb +192 -0
- data/lib/tilia/v_object/parameter.rb +327 -0
- data/lib/tilia/v_object/parse_exception.rb +7 -0
- data/lib/tilia/v_object/parser/json.rb +149 -0
- data/lib/tilia/v_object/parser/mime_dir.rb +543 -0
- data/lib/tilia/v_object/parser/parser.rb +61 -0
- data/lib/tilia/v_object/parser/xml/element/key_value.rb +60 -0
- data/lib/tilia/v_object/parser/xml/element.rb +11 -0
- data/lib/tilia/v_object/parser/xml.rb +322 -0
- data/lib/tilia/v_object/parser.rb +10 -0
- data/lib/tilia/v_object/property/binary.rb +96 -0
- data/lib/tilia/v_object/property/boolean.rb +57 -0
- data/lib/tilia/v_object/property/flat_text.rb +52 -0
- data/lib/tilia/v_object/property/float_value.rb +107 -0
- data/lib/tilia/v_object/property/i_calendar/cal_address.rb +49 -0
- data/lib/tilia/v_object/property/i_calendar/date.rb +15 -0
- data/lib/tilia/v_object/property/i_calendar/date_time.rb +330 -0
- data/lib/tilia/v_object/property/i_calendar/duration.rb +65 -0
- data/lib/tilia/v_object/property/i_calendar/period.rb +124 -0
- data/lib/tilia/v_object/property/i_calendar/recur.rb +173 -0
- data/lib/tilia/v_object/property/i_calendar.rb +14 -0
- data/lib/tilia/v_object/property/integer_value.rb +60 -0
- data/lib/tilia/v_object/property/text.rb +352 -0
- data/lib/tilia/v_object/property/time.rb +85 -0
- data/lib/tilia/v_object/property/unknown.rb +30 -0
- data/lib/tilia/v_object/property/uri.rb +78 -0
- data/lib/tilia/v_object/property/utc_offset.rb +56 -0
- data/lib/tilia/v_object/property/v_card/date.rb +31 -0
- data/lib/tilia/v_object/property/v_card/date_and_or_time.rb +343 -0
- data/lib/tilia/v_object/property/v_card/date_time.rb +22 -0
- data/lib/tilia/v_object/property/v_card/language_tag.rb +41 -0
- data/lib/tilia/v_object/property/v_card/time_stamp.rb +74 -0
- data/lib/tilia/v_object/property/v_card.rb +13 -0
- data/lib/tilia/v_object/property.rb +532 -0
- data/lib/tilia/v_object/reader.rb +73 -0
- data/lib/tilia/v_object/recur/event_iterator.rb +417 -0
- data/lib/tilia/v_object/recur/no_instances_exception.rb +11 -0
- data/lib/tilia/v_object/recur/r_date_iterator.rb +138 -0
- data/lib/tilia/v_object/recur/r_rule_iterator.rb +717 -0
- data/lib/tilia/v_object/recur.rb +10 -0
- data/lib/tilia/v_object/settings.rb +32 -0
- data/lib/tilia/v_object/splitter/i_calendar.rb +95 -0
- data/lib/tilia/v_object/splitter/splitter_interface.rb +31 -0
- data/lib/tilia/v_object/splitter/v_card.rb +56 -0
- data/lib/tilia/v_object/splitter.rb +9 -0
- data/lib/tilia/v_object/string_util.rb +58 -0
- data/lib/tilia/v_object/time_zone_data/exchange_zones.rb +96 -0
- data/lib/tilia/v_object/time_zone_data/lotus_zones.rb +104 -0
- data/lib/tilia/v_object/time_zone_data/php_zones.rb +49 -0
- data/lib/tilia/v_object/time_zone_data/windows_zones.rb +121 -0
- data/lib/tilia/v_object/time_zone_data.rb +10 -0
- data/lib/tilia/v_object/time_zone_util.rb +213 -0
- data/lib/tilia/v_object/uuid_util.rb +51 -0
- data/lib/tilia/v_object/v_card_converter.rb +354 -0
- data/lib/tilia/v_object/version.rb +9 -0
- data/lib/tilia/v_object/writer.rb +56 -0
- data/lib/tilia/v_object.rb +45 -0
- data/lib/tilia/vobject.rb +1 -0
- data/resources/schema/xcal.rng +1192 -0
- data/resources/schema/xcard.rng +388 -0
- data/test/test_helper.rb +56 -0
- data/test/v_object/attach_issue_test.rb +19 -0
- data/test/v_object/birthday_calendar_generator_test.rb +463 -0
- data/test/v_object/cli_mock.rb +19 -0
- data/test/v_object/cli_test.rb +460 -0
- data/test/v_object/component/available_test.rb +59 -0
- data/test/v_object/component/v_alarm_test.rb +160 -0
- data/test/v_object/component/v_availability_test.rb +388 -0
- data/test/v_object/component/v_calendar_test.rb +646 -0
- data/test/v_object/component/v_card_test.rb +258 -0
- data/test/v_object/component/v_event_test.rb +85 -0
- data/test/v_object/component/v_free_busy_test.rb +59 -0
- data/test/v_object/component/v_journal_test.rb +85 -0
- data/test/v_object/component/v_time_zone_test.rb +47 -0
- data/test/v_object/component/v_todo_test.rb +172 -0
- data/test/v_object/component_test.rb +419 -0
- data/test/v_object/date_time_parser_test.rb +526 -0
- data/test/v_object/document_test.rb +71 -0
- data/test/v_object/element_list_test.rb +27 -0
- data/test/v_object/em_client_test.rb +53 -0
- data/test/v_object/empty_parameter_test.rb +65 -0
- data/test/v_object/empty_value_issue_test.rb +25 -0
- data/test/v_object/fake_component.rb +21 -0
- data/test/v_object/free_busy_data_test.rb +285 -0
- data/test/v_object/free_busy_generator_test.rb +637 -0
- data/test/v_object/google_colon_escaping_test.rb +27 -0
- data/test/v_object/i_calendar/attach_parse_test.rb +24 -0
- data/test/v_object/i_tip/broker_attendee_reply_test.rb +1042 -0
- data/test/v_object/i_tip/broker_delete_event_test.rb +175 -0
- data/test/v_object/i_tip/broker_new_event_test.rb +440 -0
- data/test/v_object/i_tip/broker_process_message_test.rb +153 -0
- data/test/v_object/i_tip/broker_process_reply_test.rb +402 -0
- data/test/v_object/i_tip/broker_tester.rb +71 -0
- data/test/v_object/i_tip/broker_update_event_test.rb +763 -0
- data/test/v_object/i_tip/evolution_test.rb +2644 -0
- data/test/v_object/i_tip/message_test.rb +25 -0
- data/test/v_object/issue153.vcf +352 -0
- data/test/v_object/issue153_test.rb +12 -0
- data/test/v_object/issue26_test.rb +25 -0
- data/test/v_object/issue36_work_around_test.rb +37 -0
- data/test/v_object/issue40_test.rb +26 -0
- data/test/v_object/issue64.vcf +351 -0
- data/test/v_object/issue64_test.rb +17 -0
- data/test/v_object/issue96_test.rb +22 -0
- data/test/v_object/issue_undefined_index_test.rb +24 -0
- data/test/v_object/j_cal_test.rb +150 -0
- data/test/v_object/j_card_test.rb +192 -0
- data/test/v_object/line_folding_issue_test.rb +19 -0
- data/test/v_object/mock_document.rb +6 -0
- data/test/v_object/parameter_test.rb +109 -0
- data/test/v_object/parser/json_test.rb +370 -0
- data/test/v_object/parser/mime_dir_test.rb +14 -0
- data/test/v_object/parser/quoted_printable_test.rb +78 -0
- data/test/v_object/parser/xml_test.rb +2563 -0
- data/test/v_object/property/binary_test.rb +12 -0
- data/test/v_object/property/boolean_test.rb +18 -0
- data/test/v_object/property/compound_test.rb +43 -0
- data/test/v_object/property/float_test.rb +20 -0
- data/test/v_object/property/i_calendar/cal_address_test.rb +26 -0
- data/test/v_object/property/i_calendar/date_time_test.rb +303 -0
- data/test/v_object/property/i_calendar/duration_test.rb +14 -0
- data/test/v_object/property/i_calendar/recur_test.rb +39 -0
- data/test/v_object/property/text_test.rb +81 -0
- data/test/v_object/property/v_card/date_and_or_time_test.rb +205 -0
- data/test/v_object/property/v_card/language_tag_test.rb +35 -0
- data/test/v_object/property_test.rb +338 -0
- data/test/v_object/reader_test.rb +403 -0
- data/test/v_object/recur/event_iterator/by_month_in_daily_test.rb +52 -0
- data/test/v_object/recur/event_iterator/by_set_pos_hang_test.rb +55 -0
- data/test/v_object/recur/event_iterator/expand_floating_times_test.rb +109 -0
- data/test/v_object/recur/event_iterator/fifth_tuesday_problem_test.rb +45 -0
- data/test/v_object/recur/event_iterator/incorrect_expand_test.rb +53 -0
- data/test/v_object/recur/event_iterator/infinite_loop_problem_test.rb +75 -0
- data/test/v_object/recur/event_iterator/issue48_test.rb +43 -0
- data/test/v_object/recur/event_iterator/issue50_test.rb +123 -0
- data/test/v_object/recur/event_iterator/main_test.rb +1222 -0
- data/test/v_object/recur/event_iterator/missing_overridden_test.rb +55 -0
- data/test/v_object/recur/event_iterator/no_instances_test.rb +32 -0
- data/test/v_object/recur/event_iterator/override_first_event_test.rb +106 -0
- data/test/v_object/recur/r_date_iterator_test.rb +44 -0
- data/test/v_object/recur/r_rule_iterator_test.rb +608 -0
- data/test/v_object/recurrence_iterator/UntilRespectsTimezoneTest.ics +39 -0
- data/test/v_object/slash_r_test.rb +15 -0
- data/test/v_object/splitter/i_calendar_test.rb +299 -0
- data/test/v_object/splitter/v_card_test.rb +173 -0
- data/test/v_object/string_util_test.rb +37 -0
- data/test/v_object/test_case.rb +42 -0
- data/test/v_object/time_zone_util_test.rb +271 -0
- data/test/v_object/uuid_util_test.rb +18 -0
- data/test/v_object/v_card21_test.rb +43 -0
- data/test/v_object/v_card_converter_test.rb +419 -0
- data/test/v_object/version_test.rb +15 -0
- data/test/v_object/writer_test.rb +33 -0
- data/tilia-vobject.gemspec +17 -0
- metadata +308 -0
@@ -0,0 +1,149 @@
|
|
1
|
+
module Tilia
|
2
|
+
module VObject
|
3
|
+
# FreeBusyData is a helper class that manages freebusy information.
|
4
|
+
class FreeBusyData
|
5
|
+
# Start timestamp
|
6
|
+
#
|
7
|
+
# @var int
|
8
|
+
# RUBY: attr_accessor :start
|
9
|
+
|
10
|
+
# End timestamp
|
11
|
+
#
|
12
|
+
# @var int
|
13
|
+
# RUBY: attr_accessor :end
|
14
|
+
|
15
|
+
# A list of free-busy times.
|
16
|
+
#
|
17
|
+
# @var array
|
18
|
+
# RUBY: attr_accessor :data
|
19
|
+
|
20
|
+
def initialize(start, ending)
|
21
|
+
@start = start
|
22
|
+
@end = ending
|
23
|
+
@data = []
|
24
|
+
|
25
|
+
@data << {
|
26
|
+
'start' => @start,
|
27
|
+
'end' => @end,
|
28
|
+
'type' => 'FREE'
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Adds free or busytime to the data.
|
33
|
+
#
|
34
|
+
# @param int start
|
35
|
+
# @param int end
|
36
|
+
# @param string type FREE, BUSY, BUSY-UNAVAILABLE or BUSY-TENTATIVE
|
37
|
+
# @return void
|
38
|
+
def add(start, ending, type)
|
39
|
+
if start > @end || ending < @start
|
40
|
+
# This new data is outside our timerange.
|
41
|
+
return nil
|
42
|
+
end
|
43
|
+
|
44
|
+
if start < @start
|
45
|
+
# The item starts before our requested time range
|
46
|
+
start = @start
|
47
|
+
end
|
48
|
+
if ending > @end
|
49
|
+
# The item ends after our requested time range
|
50
|
+
ending = @end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Finding out where we need to insert the new item.
|
54
|
+
current_index = 0
|
55
|
+
current_index += 1 while start > @data[current_index]['end']
|
56
|
+
|
57
|
+
# The standard insertion point will be one _after_ the first
|
58
|
+
# overlapping item.
|
59
|
+
insert_start_index = current_index + 1
|
60
|
+
|
61
|
+
new_item = {
|
62
|
+
'start' => start,
|
63
|
+
'end' => ending,
|
64
|
+
'type' => type
|
65
|
+
}
|
66
|
+
|
67
|
+
preceeding_item = @data[insert_start_index - 1]
|
68
|
+
if @data[insert_start_index - 1]['start'] == start
|
69
|
+
# The old item starts at the exact same point as the new item.
|
70
|
+
insert_start_index -= 1
|
71
|
+
end
|
72
|
+
|
73
|
+
# Now we know where to insert the item, we need to know where it
|
74
|
+
# starts overlapping with items on the tail end. We need to start
|
75
|
+
# looking one item before the insertStartIndex, because it's possible
|
76
|
+
# that the new item 'sits inside' the previous old item.
|
77
|
+
if insert_start_index > 0
|
78
|
+
current_index = insert_start_index - 1
|
79
|
+
else
|
80
|
+
current_index = 0
|
81
|
+
end
|
82
|
+
|
83
|
+
current_index += 1 while ending > @data[current_index]['end']
|
84
|
+
|
85
|
+
# What we are about to insert into the array
|
86
|
+
new_items = [new_item]
|
87
|
+
|
88
|
+
# This is the amount of items that are completely overwritten by the
|
89
|
+
# new item.
|
90
|
+
items_to_delete = current_index - insert_start_index
|
91
|
+
items_to_delete += 1 if @data[current_index]['end'] <= ending
|
92
|
+
|
93
|
+
# If itemsToDelete was -1, it means that the newly inserted item is
|
94
|
+
# actually sitting inside an existing one. This means we need to split
|
95
|
+
# the item at the current position in two and insert the new item in
|
96
|
+
# between.
|
97
|
+
if items_to_delete == -1
|
98
|
+
items_to_delete = 0
|
99
|
+
if new_item['end'] < preceeding_item['end']
|
100
|
+
new_items << {
|
101
|
+
'start' => new_item['end'] + 1,
|
102
|
+
'end' => preceeding_item['end'],
|
103
|
+
'type' => preceeding_item['type']
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
@data[insert_start_index, items_to_delete] = new_items
|
109
|
+
|
110
|
+
do_merge = false
|
111
|
+
merge_offset = insert_start_index
|
112
|
+
merge_item = new_item
|
113
|
+
merge_delete = 1
|
114
|
+
|
115
|
+
# Ruby knows negative indices as well!
|
116
|
+
if insert_start_index > 0 && @data.size > insert_start_index - 1
|
117
|
+
# Updating the start time of the previous item.
|
118
|
+
@data[insert_start_index - 1]['end'] = start
|
119
|
+
|
120
|
+
# If the previous and the current are of the same type, we can
|
121
|
+
# merge them into one item.
|
122
|
+
if @data[insert_start_index - 1]['type'] == @data[insert_start_index]['type']
|
123
|
+
do_merge = true
|
124
|
+
merge_offset -= 1
|
125
|
+
merge_delete += 1
|
126
|
+
merge_item['start'] = @data[insert_start_index - 1]['start']
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
if @data.size > insert_start_index + 1
|
131
|
+
# Updating the start time of the next item.
|
132
|
+
@data[insert_start_index + 1]['start'] = ending
|
133
|
+
|
134
|
+
# If the next and the current are of the same type, we can
|
135
|
+
# merge them into one item.
|
136
|
+
if @data[insert_start_index + 1]['type'] == @data[insert_start_index]['type']
|
137
|
+
do_merge = true
|
138
|
+
merge_delete += 1
|
139
|
+
merge_item['end'] = @data[insert_start_index + 1]['end']
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
@data[merge_offset, merge_delete] = merge_item if do_merge
|
144
|
+
end
|
145
|
+
|
146
|
+
attr_reader :data
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,465 @@
|
|
1
|
+
module Tilia
|
2
|
+
module VObject
|
3
|
+
# This class helps with generating FREEBUSY reports based on existing sets of
|
4
|
+
# objects.
|
5
|
+
#
|
6
|
+
# It only looks at VEVENT and VFREEBUSY objects from the sourcedata, and
|
7
|
+
# generates a single VFREEBUSY object.
|
8
|
+
#
|
9
|
+
# VFREEBUSY components are described in RFC5545, The rules for what should
|
10
|
+
# go in a single freebusy report is taken from RFC4791, section 7.10.
|
11
|
+
class FreeBusyGenerator
|
12
|
+
# Input objects.
|
13
|
+
#
|
14
|
+
# @var array
|
15
|
+
# RUBY: attr_accessor :objects
|
16
|
+
|
17
|
+
# Start of range.
|
18
|
+
#
|
19
|
+
# @var DateTimeInterface|null
|
20
|
+
# RUBY: attr_accessor :start
|
21
|
+
|
22
|
+
# End of range.
|
23
|
+
#
|
24
|
+
# @var DateTimeInterface|null
|
25
|
+
# RUBY: attr_accessor :end
|
26
|
+
|
27
|
+
# VCALENDAR object.
|
28
|
+
#
|
29
|
+
# @var Document
|
30
|
+
# RUBY: attr_accessor :base_object
|
31
|
+
|
32
|
+
# Reference timezone.
|
33
|
+
#
|
34
|
+
# When we are calculating busy times, and we come across so-called
|
35
|
+
# floating times (times without a timezone), we use the reference timezone
|
36
|
+
# instead.
|
37
|
+
#
|
38
|
+
# This is also used for all-day events.
|
39
|
+
#
|
40
|
+
# This defaults to UTC.
|
41
|
+
#
|
42
|
+
# @var DateTimeZone
|
43
|
+
# RUBY: attr_accessor :time_zone
|
44
|
+
|
45
|
+
# A VAVAILABILITY document.
|
46
|
+
#
|
47
|
+
# If this is set, it's information will be included when calculating
|
48
|
+
# freebusy time.
|
49
|
+
#
|
50
|
+
# @var Document
|
51
|
+
# RUBY: attr_accessor :vavailability
|
52
|
+
|
53
|
+
# Creates the generator.
|
54
|
+
#
|
55
|
+
# Check the setTimeRange and setObjects methods for details about the
|
56
|
+
# arguments.
|
57
|
+
#
|
58
|
+
# @param DateTimeInterface start
|
59
|
+
# @param DateTimeInterface end
|
60
|
+
# @param mixed objects
|
61
|
+
# @param DateTimeZone time_zone
|
62
|
+
def initialize(start = nil, ending = nil, objects = nil, time_zone = nil)
|
63
|
+
start = Time.zone.parse(Settings.min_date) unless start
|
64
|
+
ending = Time.zone.parse(Settings.max_date) unless ending
|
65
|
+
|
66
|
+
self.time_range = start..ending
|
67
|
+
@objects = []
|
68
|
+
|
69
|
+
self.objects = objects if objects
|
70
|
+
time_zone = ActiveSupport::TimeZone.new('UTC') unless time_zone
|
71
|
+
self.time_zone = time_zone
|
72
|
+
end
|
73
|
+
|
74
|
+
# Sets the VCALENDAR object.
|
75
|
+
#
|
76
|
+
# If this is set, it will not be generated for you. You are responsible
|
77
|
+
# for setting things like the METHOD, CALSCALE, VERSION, etc..
|
78
|
+
#
|
79
|
+
# The VFREEBUSY object will be automatically added though.
|
80
|
+
#
|
81
|
+
# @param Document vcalendar
|
82
|
+
# @return void
|
83
|
+
attr_writer :base_object
|
84
|
+
|
85
|
+
# Sets a VAVAILABILITY document.
|
86
|
+
#
|
87
|
+
# @param Document vcalendar
|
88
|
+
# @return void
|
89
|
+
def v_availability=(vcalendar)
|
90
|
+
@vavailability = vcalendar
|
91
|
+
end
|
92
|
+
|
93
|
+
# Sets the input objects.
|
94
|
+
#
|
95
|
+
# You must either specify a valendar object as a string, or as the parse
|
96
|
+
# Component.
|
97
|
+
# It's also possible to specify multiple objects as an array.
|
98
|
+
#
|
99
|
+
# @param mixed objects
|
100
|
+
#
|
101
|
+
# @return void
|
102
|
+
def objects=(objects)
|
103
|
+
objects = [objects] unless objects.is_a?(Array)
|
104
|
+
|
105
|
+
@objects = []
|
106
|
+
objects.each do |object|
|
107
|
+
if object.is_a?(String)
|
108
|
+
@objects << Reader.read(object)
|
109
|
+
elsif object.is_a?(Component)
|
110
|
+
@objects << object
|
111
|
+
else
|
112
|
+
fail ArgumentError, 'You can only pass strings or Component arguments to setObjects'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Sets the time range.
|
118
|
+
#
|
119
|
+
# Any freebusy object falling outside of this time range will be ignored.
|
120
|
+
#
|
121
|
+
# @param DateTimeInterface start
|
122
|
+
# @param DateTimeInterface end
|
123
|
+
#
|
124
|
+
# @return void
|
125
|
+
def time_range=(range)
|
126
|
+
@start = range.begin
|
127
|
+
@end = range.end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Sets the reference timezone for floating times.
|
131
|
+
#
|
132
|
+
# @param DateTimeZone time_zone
|
133
|
+
#
|
134
|
+
# @return void
|
135
|
+
def time_zone=(time_zone)
|
136
|
+
@time_zone = time_zone
|
137
|
+
end
|
138
|
+
|
139
|
+
# Parses the input data and returns a correct VFREEBUSY object, wrapped in
|
140
|
+
# a VCALENDAR.
|
141
|
+
#
|
142
|
+
# @return Component
|
143
|
+
def result
|
144
|
+
fb_data = FreeBusyData.new(@start.to_i, @end.to_i)
|
145
|
+
|
146
|
+
calculate_availability(fb_data, @vavailability) if @vavailability
|
147
|
+
|
148
|
+
calculate_busy(fb_data, @objects)
|
149
|
+
generate_free_busy_calendar(fb_data)
|
150
|
+
end
|
151
|
+
|
152
|
+
protected
|
153
|
+
|
154
|
+
# This method takes a VAVAILABILITY component and figures out all the
|
155
|
+
# available times.
|
156
|
+
#
|
157
|
+
# @param FreeBusyData fb_data
|
158
|
+
# @param VCalendar vavailability
|
159
|
+
# @return void
|
160
|
+
def calculate_availability(fb_data, vavailability)
|
161
|
+
vavail_comps = vavailability['VAVAILABILITY'].to_a
|
162
|
+
vavail_comps.sort! do |a, b|
|
163
|
+
# We need to order the components by priority. Priority 1
|
164
|
+
# comes first, up until priority 9. Priority 0 comes after
|
165
|
+
# priority 9. No priority implies priority 0.
|
166
|
+
#
|
167
|
+
# Yes, I'm serious.
|
168
|
+
priority_a = a.key?('PRIORITY') ? a['PRIORITY'].value.to_i : 0
|
169
|
+
priority_b = b.key?('PRIORITY') ? b['PRIORITY'].value.to_i : 0
|
170
|
+
|
171
|
+
priority_a = 10 if priority_a == 0
|
172
|
+
priority_b = 10 if priority_b == 0
|
173
|
+
|
174
|
+
priority_a <=> priority_b
|
175
|
+
end
|
176
|
+
|
177
|
+
# Now we go over all the VAVAILABILITY components and figure if
|
178
|
+
# there's any we don't need to consider.
|
179
|
+
#
|
180
|
+
# This is can be because of one of two reasons: either the
|
181
|
+
# VAVAILABILITY component falls outside the time we are interested in,
|
182
|
+
# or a different VAVAILABILITY component with a higher priority has
|
183
|
+
# already completely covered the time-range.
|
184
|
+
old = vavail_comps
|
185
|
+
new = []
|
186
|
+
|
187
|
+
old.each do |vavail|
|
188
|
+
(comp_start, comp_end) = vavail.effective_start_end
|
189
|
+
|
190
|
+
# We don't care about datetimes that are earlier or later than the
|
191
|
+
# start and end of the freebusy report, so this gets normalized
|
192
|
+
# first.
|
193
|
+
comp_start = @start if comp_start.nil? || comp_start < @start
|
194
|
+
comp_end = @end if comp_end.nil? || comp_end > @end
|
195
|
+
|
196
|
+
# If the item fell out of the timerange, we can just skip it.
|
197
|
+
next if comp_start > @end || comp_end < @start
|
198
|
+
|
199
|
+
# Going through our existing list of components to see if there's
|
200
|
+
# a higher priority component that already fully covers this one.
|
201
|
+
skip = false
|
202
|
+
new.each do |higher_vavail|
|
203
|
+
(higher_start, higher_end) = higher_vavail.effective_start_end
|
204
|
+
if (higher_start.nil? || higher_start < comp_start) &&
|
205
|
+
(higher_end.nil? || higher_end > comp_end)
|
206
|
+
# Component is fully covered by a higher priority
|
207
|
+
# component. We can skip this component.
|
208
|
+
skip = true
|
209
|
+
break
|
210
|
+
end
|
211
|
+
end
|
212
|
+
next if skip
|
213
|
+
|
214
|
+
# We're keeping it!
|
215
|
+
new << vavail
|
216
|
+
end
|
217
|
+
|
218
|
+
# Lastly, we need to traverse the remaining components and fill in the
|
219
|
+
# freebusydata slots.
|
220
|
+
#
|
221
|
+
# We traverse the components in reverse, because we want the higher
|
222
|
+
# priority components to override the lower ones.
|
223
|
+
new.reverse_each do |vavail|
|
224
|
+
busy_type = vavail.key?('BUSYTYPE') ? vavail['BUSYTYPE'].to_s.upcase : 'BUSY-UNAVAILABLE'
|
225
|
+
(vavail_start, vavail_end) = vavail.effective_start_end
|
226
|
+
|
227
|
+
# Making the component size no larger than the requested free-busy
|
228
|
+
# report range.
|
229
|
+
vavail_start = @start if !vavail_start || vavail_start < @start
|
230
|
+
vavail_end = @end if !vavail_end || vavail_end > @end
|
231
|
+
|
232
|
+
# Marking the entire time range of the VAVAILABILITY component as
|
233
|
+
# busy.
|
234
|
+
fb_data.add(
|
235
|
+
vavail_start.to_i,
|
236
|
+
vavail_end.to_i,
|
237
|
+
busy_type
|
238
|
+
)
|
239
|
+
|
240
|
+
# Looping over the AVAILABLE components.
|
241
|
+
if vavail.key?('AVAILABLE')
|
242
|
+
vavail['AVAILABLE'].each do |available|
|
243
|
+
(avail_start, avail_end) = available.effective_start_end
|
244
|
+
fb_data.add(
|
245
|
+
avail_start.to_i,
|
246
|
+
avail_end.to_i,
|
247
|
+
'FREE'
|
248
|
+
)
|
249
|
+
|
250
|
+
if available['RRULE']
|
251
|
+
# Our favourite thing: recurrence!!
|
252
|
+
rrule_iterator = Recur::RRuleIterator.new(
|
253
|
+
available['RRULE'].value,
|
254
|
+
avail_start
|
255
|
+
)
|
256
|
+
|
257
|
+
rrule_iterator.fast_forward(vavail_start)
|
258
|
+
|
259
|
+
start_end_diff = avail_end - avail_start
|
260
|
+
|
261
|
+
while rrule_iterator.valid
|
262
|
+
recur_start = rrule_iterator.current
|
263
|
+
recur_end = recur_start + start_end_diff
|
264
|
+
|
265
|
+
if recur_start > vavail_end
|
266
|
+
# We're beyond the legal timerange.
|
267
|
+
break
|
268
|
+
end
|
269
|
+
|
270
|
+
if recur_end > vavail_end
|
271
|
+
# Truncating the end if it exceeds the
|
272
|
+
# VAVAILABILITY end.
|
273
|
+
recur_end = vavail_end
|
274
|
+
end
|
275
|
+
|
276
|
+
fb_data.add(
|
277
|
+
recur_start.to_i,
|
278
|
+
recur_end.to_i,
|
279
|
+
'FREE'
|
280
|
+
)
|
281
|
+
|
282
|
+
rrule_iterator.next
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# This method takes an array of iCalendar objects and applies its busy
|
291
|
+
# times on fbData.
|
292
|
+
#
|
293
|
+
# @param FreeBusyData fb_data
|
294
|
+
# @param VCalendar[] objects
|
295
|
+
def calculate_busy(fb_data, objects)
|
296
|
+
objects.each_with_index do |object, key|
|
297
|
+
object.base_components.each do |component|
|
298
|
+
case component.name
|
299
|
+
when 'VEVENT'
|
300
|
+
skip = false
|
301
|
+
fb_type = 'BUSY'
|
302
|
+
if component.key?('TRANSP') && component['TRANSP'].to_s.upcase == 'TRANSPARENT'
|
303
|
+
skip = true
|
304
|
+
end
|
305
|
+
if component.key?('STATUS')
|
306
|
+
status = component['STATUS'].to_s.upcase
|
307
|
+
if status == 'CANCELLED'
|
308
|
+
skip = true
|
309
|
+
elsif status == 'TENTATIVE'
|
310
|
+
fb_type = 'BUSY-TENTATIVE'
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
unless skip
|
315
|
+
times = []
|
316
|
+
|
317
|
+
if component.key?('RRULE')
|
318
|
+
begin
|
319
|
+
iterator = Recur::EventIterator.new(object, component['UID'].to_s, @time_zone)
|
320
|
+
rescue Recur::NoInstancesException => e
|
321
|
+
# This event is recurring, but it doesn't have a single
|
322
|
+
# instance. We are skipping this event from the output
|
323
|
+
# entirely.
|
324
|
+
@objects.delete_at(key)
|
325
|
+
next
|
326
|
+
end
|
327
|
+
|
328
|
+
iterator.fast_forward(@start) if @start
|
329
|
+
|
330
|
+
max_recurrences = 200
|
331
|
+
|
332
|
+
while iterator.valid && max_recurrences > 0
|
333
|
+
max_recurrences -= 1
|
334
|
+
|
335
|
+
start_time = iterator.dt_start
|
336
|
+
break if @end && start_time > @end
|
337
|
+
times << [
|
338
|
+
iterator.dt_start,
|
339
|
+
iterator.dt_end
|
340
|
+
]
|
341
|
+
|
342
|
+
iterator.next
|
343
|
+
end
|
344
|
+
else
|
345
|
+
start_time = component['DTSTART'].date_time(@time_zone)
|
346
|
+
skip = true if @end && start_time > @end
|
347
|
+
|
348
|
+
end_time = nil
|
349
|
+
if component.key?('DTEND')
|
350
|
+
end_time = component['DTEND'].date_time(@time_zone)
|
351
|
+
elsif component.key?('DURATION')
|
352
|
+
duration = DateTimeParser.parse_duration(component['DURATION'].to_s)
|
353
|
+
end_time = start_time + duration
|
354
|
+
elsif !component['DTSTART'].time?
|
355
|
+
end_time = start_time + 1.day
|
356
|
+
else
|
357
|
+
# The event had no duration (0 seconds)
|
358
|
+
skip = true
|
359
|
+
end
|
360
|
+
|
361
|
+
times << [start_time, end_time] unless skip
|
362
|
+
end
|
363
|
+
|
364
|
+
times.each do |time|
|
365
|
+
break if @end && time[0] > @end
|
366
|
+
break if @start && time[1] < @start
|
367
|
+
|
368
|
+
fb_data.add(
|
369
|
+
time[0].to_i,
|
370
|
+
time[1].to_i,
|
371
|
+
fb_type
|
372
|
+
)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
when 'VFREEBUSY'
|
376
|
+
component['FREEBUSY'].each do |freebusy|
|
377
|
+
fb_type = freebusy.key?('FBTYPE') ? freebusy['FBTYPE'].to_s.upcase : 'BUSY'
|
378
|
+
|
379
|
+
# Skipping intervals marked as 'free'
|
380
|
+
next if fb_type == 'FREE'
|
381
|
+
|
382
|
+
values = freebusy.to_s.split(',')
|
383
|
+
values.each do |value|
|
384
|
+
(start_time, end_time) = value.split('/')
|
385
|
+
start_time = DateTimeParser.parse_date_time(start_time)
|
386
|
+
|
387
|
+
if end_time[0] == 'P' || end_time[0..1] == '-P'
|
388
|
+
duration = DateTimeParser.parse_duration(end_time)
|
389
|
+
end_time = start_time + duration
|
390
|
+
else
|
391
|
+
end_time = DateTimeParser.parse_date_time(end_time)
|
392
|
+
end
|
393
|
+
|
394
|
+
next if @start && @start > end_time
|
395
|
+
next if @end && @end < start_time
|
396
|
+
|
397
|
+
fb_data.add(
|
398
|
+
start_time.to_i,
|
399
|
+
end_time.to_i,
|
400
|
+
fb_type
|
401
|
+
)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
# This method takes a FreeBusyData object and generates the VCALENDAR
|
410
|
+
# object associated with it.
|
411
|
+
#
|
412
|
+
# @return VCalendar
|
413
|
+
def generate_free_busy_calendar(fb_data)
|
414
|
+
if @base_object
|
415
|
+
calendar = @base_object
|
416
|
+
else
|
417
|
+
calendar = Component::VCalendar.new
|
418
|
+
end
|
419
|
+
|
420
|
+
vfreebusy = calendar.create_component('VFREEBUSY')
|
421
|
+
calendar.add(vfreebusy)
|
422
|
+
|
423
|
+
if @start
|
424
|
+
dtstart = calendar.create_property('DTSTART')
|
425
|
+
dtstart.date_time = @start
|
426
|
+
vfreebusy.add(dtstart)
|
427
|
+
end
|
428
|
+
if @end
|
429
|
+
dtend = calendar.create_property('DTEND')
|
430
|
+
dtend.date_time = @end
|
431
|
+
vfreebusy.add(dtend)
|
432
|
+
end
|
433
|
+
|
434
|
+
tz = ActiveSupport::TimeZone.new('UTC')
|
435
|
+
dtstamp = calendar.create_property('DTSTAMP')
|
436
|
+
dtstamp.date_time = tz.now
|
437
|
+
vfreebusy.add(dtstamp)
|
438
|
+
|
439
|
+
fb_data.data.each do |busy_time|
|
440
|
+
busy_type = busy_time['type'].upcase
|
441
|
+
|
442
|
+
# Ignoring all the FREE parts, because those are already assumed.
|
443
|
+
next if busy_type == 'FREE'
|
444
|
+
|
445
|
+
tmp = []
|
446
|
+
tmp << tz.at(busy_time['start'])
|
447
|
+
tmp << tz.at(busy_time['end'])
|
448
|
+
|
449
|
+
prop = calendar.create_property(
|
450
|
+
'FREEBUSY',
|
451
|
+
tmp[0].strftime('%Y%m%dT%H%M%SZ') + '/' + tmp[1].strftime('%Y%m%dT%H%M%SZ')
|
452
|
+
)
|
453
|
+
|
454
|
+
# Only setting FBTYPE if it's not BUSY, because BUSY is the
|
455
|
+
# default anyway.
|
456
|
+
prop['FBTYPE'] = busy_type unless busy_type == 'BUSY'
|
457
|
+
|
458
|
+
vfreebusy.add(prop)
|
459
|
+
end
|
460
|
+
|
461
|
+
calendar
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|