tilia-vobject 4.0.0.pre.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rubocop.yml +32 -0
  4. data/.simplecov +4 -0
  5. data/.travis.yml +3 -0
  6. data/CHANGELOG.sabre.md +626 -0
  7. data/CONTRIBUTING.md +25 -0
  8. data/Gemfile +17 -0
  9. data/Gemfile.lock +68 -0
  10. data/LICENSE +27 -0
  11. data/LICENSE.sabre +27 -0
  12. data/README.md +63 -0
  13. data/Rakefile +17 -0
  14. data/bin/vobject +7 -0
  15. data/lib/tilia/v_object/birthday_calendar_generator.rb +142 -0
  16. data/lib/tilia/v_object/cli.rb +582 -0
  17. data/lib/tilia/v_object/component/available.rb +107 -0
  18. data/lib/tilia/v_object/component/v_alarm.rb +114 -0
  19. data/lib/tilia/v_object/component/v_availability.rb +128 -0
  20. data/lib/tilia/v_object/component/v_calendar.rb +468 -0
  21. data/lib/tilia/v_object/component/v_card.rb +457 -0
  22. data/lib/tilia/v_object/component/v_event.rb +127 -0
  23. data/lib/tilia/v_object/component/v_free_busy.rb +81 -0
  24. data/lib/tilia/v_object/component/v_journal.rb +75 -0
  25. data/lib/tilia/v_object/component/v_time_zone.rb +51 -0
  26. data/lib/tilia/v_object/component/v_todo.rb +147 -0
  27. data/lib/tilia/v_object/component.rb +591 -0
  28. data/lib/tilia/v_object/date_time_parser.rb +486 -0
  29. data/lib/tilia/v_object/document.rb +218 -0
  30. data/lib/tilia/v_object/element_list.rb +18 -0
  31. data/lib/tilia/v_object/eof_exception.rb +8 -0
  32. data/lib/tilia/v_object/free_busy_data.rb +149 -0
  33. data/lib/tilia/v_object/free_busy_generator.rb +465 -0
  34. data/lib/tilia/v_object/i_tip/broker.rb +909 -0
  35. data/lib/tilia/v_object/i_tip/i_tip_exception.rb +9 -0
  36. data/lib/tilia/v_object/i_tip/message.rb +109 -0
  37. data/lib/tilia/v_object/i_tip/same_organizer_for_all_components_exception.rb +13 -0
  38. data/lib/tilia/v_object/i_tip.rb +10 -0
  39. data/lib/tilia/v_object/node.rb +192 -0
  40. data/lib/tilia/v_object/parameter.rb +327 -0
  41. data/lib/tilia/v_object/parse_exception.rb +7 -0
  42. data/lib/tilia/v_object/parser/json.rb +149 -0
  43. data/lib/tilia/v_object/parser/mime_dir.rb +543 -0
  44. data/lib/tilia/v_object/parser/parser.rb +61 -0
  45. data/lib/tilia/v_object/parser/xml/element/key_value.rb +60 -0
  46. data/lib/tilia/v_object/parser/xml/element.rb +11 -0
  47. data/lib/tilia/v_object/parser/xml.rb +322 -0
  48. data/lib/tilia/v_object/parser.rb +10 -0
  49. data/lib/tilia/v_object/property/binary.rb +96 -0
  50. data/lib/tilia/v_object/property/boolean.rb +57 -0
  51. data/lib/tilia/v_object/property/flat_text.rb +52 -0
  52. data/lib/tilia/v_object/property/float_value.rb +107 -0
  53. data/lib/tilia/v_object/property/i_calendar/cal_address.rb +49 -0
  54. data/lib/tilia/v_object/property/i_calendar/date.rb +15 -0
  55. data/lib/tilia/v_object/property/i_calendar/date_time.rb +330 -0
  56. data/lib/tilia/v_object/property/i_calendar/duration.rb +65 -0
  57. data/lib/tilia/v_object/property/i_calendar/period.rb +124 -0
  58. data/lib/tilia/v_object/property/i_calendar/recur.rb +173 -0
  59. data/lib/tilia/v_object/property/i_calendar.rb +14 -0
  60. data/lib/tilia/v_object/property/integer_value.rb +60 -0
  61. data/lib/tilia/v_object/property/text.rb +352 -0
  62. data/lib/tilia/v_object/property/time.rb +85 -0
  63. data/lib/tilia/v_object/property/unknown.rb +30 -0
  64. data/lib/tilia/v_object/property/uri.rb +78 -0
  65. data/lib/tilia/v_object/property/utc_offset.rb +56 -0
  66. data/lib/tilia/v_object/property/v_card/date.rb +31 -0
  67. data/lib/tilia/v_object/property/v_card/date_and_or_time.rb +343 -0
  68. data/lib/tilia/v_object/property/v_card/date_time.rb +22 -0
  69. data/lib/tilia/v_object/property/v_card/language_tag.rb +41 -0
  70. data/lib/tilia/v_object/property/v_card/time_stamp.rb +74 -0
  71. data/lib/tilia/v_object/property/v_card.rb +13 -0
  72. data/lib/tilia/v_object/property.rb +532 -0
  73. data/lib/tilia/v_object/reader.rb +73 -0
  74. data/lib/tilia/v_object/recur/event_iterator.rb +417 -0
  75. data/lib/tilia/v_object/recur/no_instances_exception.rb +11 -0
  76. data/lib/tilia/v_object/recur/r_date_iterator.rb +138 -0
  77. data/lib/tilia/v_object/recur/r_rule_iterator.rb +717 -0
  78. data/lib/tilia/v_object/recur.rb +10 -0
  79. data/lib/tilia/v_object/settings.rb +32 -0
  80. data/lib/tilia/v_object/splitter/i_calendar.rb +95 -0
  81. data/lib/tilia/v_object/splitter/splitter_interface.rb +31 -0
  82. data/lib/tilia/v_object/splitter/v_card.rb +56 -0
  83. data/lib/tilia/v_object/splitter.rb +9 -0
  84. data/lib/tilia/v_object/string_util.rb +58 -0
  85. data/lib/tilia/v_object/time_zone_data/exchange_zones.rb +96 -0
  86. data/lib/tilia/v_object/time_zone_data/lotus_zones.rb +104 -0
  87. data/lib/tilia/v_object/time_zone_data/php_zones.rb +49 -0
  88. data/lib/tilia/v_object/time_zone_data/windows_zones.rb +121 -0
  89. data/lib/tilia/v_object/time_zone_data.rb +10 -0
  90. data/lib/tilia/v_object/time_zone_util.rb +213 -0
  91. data/lib/tilia/v_object/uuid_util.rb +51 -0
  92. data/lib/tilia/v_object/v_card_converter.rb +354 -0
  93. data/lib/tilia/v_object/version.rb +9 -0
  94. data/lib/tilia/v_object/writer.rb +56 -0
  95. data/lib/tilia/v_object.rb +45 -0
  96. data/lib/tilia/vobject.rb +1 -0
  97. data/resources/schema/xcal.rng +1192 -0
  98. data/resources/schema/xcard.rng +388 -0
  99. data/test/test_helper.rb +56 -0
  100. data/test/v_object/attach_issue_test.rb +19 -0
  101. data/test/v_object/birthday_calendar_generator_test.rb +463 -0
  102. data/test/v_object/cli_mock.rb +19 -0
  103. data/test/v_object/cli_test.rb +460 -0
  104. data/test/v_object/component/available_test.rb +59 -0
  105. data/test/v_object/component/v_alarm_test.rb +160 -0
  106. data/test/v_object/component/v_availability_test.rb +388 -0
  107. data/test/v_object/component/v_calendar_test.rb +646 -0
  108. data/test/v_object/component/v_card_test.rb +258 -0
  109. data/test/v_object/component/v_event_test.rb +85 -0
  110. data/test/v_object/component/v_free_busy_test.rb +59 -0
  111. data/test/v_object/component/v_journal_test.rb +85 -0
  112. data/test/v_object/component/v_time_zone_test.rb +47 -0
  113. data/test/v_object/component/v_todo_test.rb +172 -0
  114. data/test/v_object/component_test.rb +419 -0
  115. data/test/v_object/date_time_parser_test.rb +526 -0
  116. data/test/v_object/document_test.rb +71 -0
  117. data/test/v_object/element_list_test.rb +27 -0
  118. data/test/v_object/em_client_test.rb +53 -0
  119. data/test/v_object/empty_parameter_test.rb +65 -0
  120. data/test/v_object/empty_value_issue_test.rb +25 -0
  121. data/test/v_object/fake_component.rb +21 -0
  122. data/test/v_object/free_busy_data_test.rb +285 -0
  123. data/test/v_object/free_busy_generator_test.rb +637 -0
  124. data/test/v_object/google_colon_escaping_test.rb +27 -0
  125. data/test/v_object/i_calendar/attach_parse_test.rb +24 -0
  126. data/test/v_object/i_tip/broker_attendee_reply_test.rb +1042 -0
  127. data/test/v_object/i_tip/broker_delete_event_test.rb +175 -0
  128. data/test/v_object/i_tip/broker_new_event_test.rb +440 -0
  129. data/test/v_object/i_tip/broker_process_message_test.rb +153 -0
  130. data/test/v_object/i_tip/broker_process_reply_test.rb +402 -0
  131. data/test/v_object/i_tip/broker_tester.rb +71 -0
  132. data/test/v_object/i_tip/broker_update_event_test.rb +763 -0
  133. data/test/v_object/i_tip/evolution_test.rb +2644 -0
  134. data/test/v_object/i_tip/message_test.rb +25 -0
  135. data/test/v_object/issue153.vcf +352 -0
  136. data/test/v_object/issue153_test.rb +12 -0
  137. data/test/v_object/issue26_test.rb +25 -0
  138. data/test/v_object/issue36_work_around_test.rb +37 -0
  139. data/test/v_object/issue40_test.rb +26 -0
  140. data/test/v_object/issue64.vcf +351 -0
  141. data/test/v_object/issue64_test.rb +17 -0
  142. data/test/v_object/issue96_test.rb +22 -0
  143. data/test/v_object/issue_undefined_index_test.rb +24 -0
  144. data/test/v_object/j_cal_test.rb +150 -0
  145. data/test/v_object/j_card_test.rb +192 -0
  146. data/test/v_object/line_folding_issue_test.rb +19 -0
  147. data/test/v_object/mock_document.rb +6 -0
  148. data/test/v_object/parameter_test.rb +109 -0
  149. data/test/v_object/parser/json_test.rb +370 -0
  150. data/test/v_object/parser/mime_dir_test.rb +14 -0
  151. data/test/v_object/parser/quoted_printable_test.rb +78 -0
  152. data/test/v_object/parser/xml_test.rb +2563 -0
  153. data/test/v_object/property/binary_test.rb +12 -0
  154. data/test/v_object/property/boolean_test.rb +18 -0
  155. data/test/v_object/property/compound_test.rb +43 -0
  156. data/test/v_object/property/float_test.rb +20 -0
  157. data/test/v_object/property/i_calendar/cal_address_test.rb +26 -0
  158. data/test/v_object/property/i_calendar/date_time_test.rb +303 -0
  159. data/test/v_object/property/i_calendar/duration_test.rb +14 -0
  160. data/test/v_object/property/i_calendar/recur_test.rb +39 -0
  161. data/test/v_object/property/text_test.rb +81 -0
  162. data/test/v_object/property/v_card/date_and_or_time_test.rb +205 -0
  163. data/test/v_object/property/v_card/language_tag_test.rb +35 -0
  164. data/test/v_object/property_test.rb +338 -0
  165. data/test/v_object/reader_test.rb +403 -0
  166. data/test/v_object/recur/event_iterator/by_month_in_daily_test.rb +52 -0
  167. data/test/v_object/recur/event_iterator/by_set_pos_hang_test.rb +55 -0
  168. data/test/v_object/recur/event_iterator/expand_floating_times_test.rb +109 -0
  169. data/test/v_object/recur/event_iterator/fifth_tuesday_problem_test.rb +45 -0
  170. data/test/v_object/recur/event_iterator/incorrect_expand_test.rb +53 -0
  171. data/test/v_object/recur/event_iterator/infinite_loop_problem_test.rb +75 -0
  172. data/test/v_object/recur/event_iterator/issue48_test.rb +43 -0
  173. data/test/v_object/recur/event_iterator/issue50_test.rb +123 -0
  174. data/test/v_object/recur/event_iterator/main_test.rb +1222 -0
  175. data/test/v_object/recur/event_iterator/missing_overridden_test.rb +55 -0
  176. data/test/v_object/recur/event_iterator/no_instances_test.rb +32 -0
  177. data/test/v_object/recur/event_iterator/override_first_event_test.rb +106 -0
  178. data/test/v_object/recur/r_date_iterator_test.rb +44 -0
  179. data/test/v_object/recur/r_rule_iterator_test.rb +608 -0
  180. data/test/v_object/recurrence_iterator/UntilRespectsTimezoneTest.ics +39 -0
  181. data/test/v_object/slash_r_test.rb +15 -0
  182. data/test/v_object/splitter/i_calendar_test.rb +299 -0
  183. data/test/v_object/splitter/v_card_test.rb +173 -0
  184. data/test/v_object/string_util_test.rb +37 -0
  185. data/test/v_object/test_case.rb +42 -0
  186. data/test/v_object/time_zone_util_test.rb +271 -0
  187. data/test/v_object/uuid_util_test.rb +18 -0
  188. data/test/v_object/v_card21_test.rb +43 -0
  189. data/test/v_object/v_card_converter_test.rb +419 -0
  190. data/test/v_object/version_test.rb +15 -0
  191. data/test/v_object/writer_test.rb +33 -0
  192. data/tilia-vobject.gemspec +17 -0
  193. metadata +308 -0
@@ -0,0 +1,8 @@
1
+ module Tilia
2
+ module VObject
3
+ # Exception thrown by parser when the end of the stream has been reached,
4
+ # before this was expected.
5
+ class EofException < ParseException
6
+ end
7
+ end
8
+ end
@@ -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