when_exe 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -33
  3. data/bin/locales.rb +1 -1
  4. data/bin/make_ttl.rb.config +1 -1
  5. data/lib/when_exe.rb +27 -16
  6. data/lib/when_exe/basictypes.rb +772 -771
  7. data/lib/when_exe/calendartypes.rb +1485 -1453
  8. data/lib/when_exe/coordinates.rb +5 -0
  9. data/lib/when_exe/core/compatibility.rb +1 -1
  10. data/lib/when_exe/core/duration.rb +147 -116
  11. data/lib/when_exe/core/extension.rb +499 -497
  12. data/lib/when_exe/ephemeris.rb +1952 -1951
  13. data/lib/when_exe/ephemeris/eclipse.rb +5 -4
  14. data/lib/when_exe/ephemeris/notes.rb +457 -421
  15. data/lib/when_exe/ephemeris/planets.rb +585 -585
  16. data/lib/when_exe/ephemeris/sun.rb +214 -214
  17. data/lib/when_exe/google_api.rb +153 -0
  18. data/lib/when_exe/icalendar.rb +1640 -1632
  19. data/lib/when_exe/inspect.rb +42 -20
  20. data/lib/when_exe/linkeddata.rb +28 -7
  21. data/lib/when_exe/locales/autoload.rb +2 -1
  22. data/lib/when_exe/locales/locale.rb +35 -15
  23. data/lib/when_exe/locales/zh.rb +77 -0
  24. data/lib/when_exe/mini_application.rb +3 -1
  25. data/lib/when_exe/{googlecalendar.rb → obsolete/googlecalendar.rb} +144 -144
  26. data/lib/when_exe/parts/enumerator.rb +498 -486
  27. data/lib/when_exe/parts/geometric_complex.rb +397 -397
  28. data/lib/when_exe/parts/timezone.rb +246 -241
  29. data/lib/when_exe/region/armenian.rb +55 -56
  30. data/lib/when_exe/region/babylonian.rb +406 -405
  31. data/lib/when_exe/region/bahai.rb +107 -106
  32. data/lib/when_exe/region/balinese.rb +624 -622
  33. data/lib/when_exe/region/chinese.rb +1071 -1026
  34. data/lib/when_exe/region/chinese/epochs.rb +28 -28
  35. data/lib/when_exe/region/chinese/notes.rb +219 -0
  36. data/lib/when_exe/region/chinese/twins.rb +803 -803
  37. data/lib/when_exe/region/christian.rb +21 -15
  38. data/lib/when_exe/region/coptic.rb +107 -106
  39. data/lib/when_exe/region/discordian.rb +218 -218
  40. data/lib/when_exe/region/east_asian.rb +1 -1
  41. data/lib/when_exe/region/french.rb +126 -56
  42. data/lib/when_exe/region/geologicalage.rb +639 -639
  43. data/lib/when_exe/region/goddess.rb +60 -58
  44. data/lib/when_exe/region/hanke_henry.rb +2 -2
  45. data/lib/when_exe/region/indian.rb +1225 -1222
  46. data/lib/when_exe/region/international_fixed.rb +96 -97
  47. data/lib/when_exe/region/iranian.rb +206 -203
  48. data/lib/when_exe/region/islamic.rb +102 -102
  49. data/lib/when_exe/region/japanese.rb +126 -71
  50. data/lib/when_exe/region/japanese/epochs.rb +426 -426
  51. data/lib/when_exe/region/japanese/notes.rb +101 -81
  52. data/lib/when_exe/region/japanese/residues.rb +1345 -1311
  53. data/lib/when_exe/region/japanese/twins.rb +225 -225
  54. data/lib/when_exe/region/japanese/weeks.rb +112 -112
  55. data/lib/when_exe/region/javanese.rb +230 -230
  56. data/lib/when_exe/region/jewish.rb +130 -131
  57. data/lib/when_exe/region/m17n.rb +114 -114
  58. data/lib/when_exe/region/martian.rb +258 -258
  59. data/lib/when_exe/region/mayan.rb +11 -8
  60. data/lib/when_exe/region/pax.rb +4 -5
  61. data/lib/when_exe/region/pope.rb +1 -1
  62. data/lib/when_exe/region/positivist.rb +100 -100
  63. data/lib/when_exe/region/residue.rb +162 -162
  64. data/lib/when_exe/region/roman.rb +333 -333
  65. data/lib/when_exe/region/{soviet.rb → russian.rb} +221 -209
  66. data/lib/when_exe/region/shire.rb +222 -223
  67. data/lib/when_exe/region/symmetry.rb +50 -50
  68. data/lib/when_exe/region/thai.rb +336 -336
  69. data/lib/when_exe/region/tibetan.rb +315 -316
  70. data/lib/when_exe/region/tranquility.rb +207 -208
  71. data/lib/when_exe/region/vanishing_leprechaun.rb +3 -1
  72. data/lib/when_exe/region/vietnamese.rb +449 -440
  73. data/lib/when_exe/region/weekdate.rb +80 -80
  74. data/lib/when_exe/region/world.rb +170 -171
  75. data/lib/when_exe/region/world_season.rb +89 -89
  76. data/lib/when_exe/region/yerm.rb +3 -3
  77. data/lib/when_exe/region/zoroastrian.rb +205 -205
  78. data/lib/when_exe/timestandard.rb +708 -707
  79. data/lib/when_exe/tmduration.rb +338 -338
  80. data/lib/when_exe/tmobjects.rb +1356 -1356
  81. data/lib/when_exe/tmposition.rb +66 -31
  82. data/lib/when_exe/version.rb +16 -2
  83. data/test/examples/Residue.m17n +83 -83
  84. data/test/examples/Terms.m17n +2 -2
  85. data/test/test.rb +2 -2
  86. data/test/test/google_api.rb +65 -0
  87. data/test/test/linkeddata.rb +1 -1
  88. data/test/test/{googlecalendar.rb → obsolete/googlecalendar.rb} +194 -194
  89. data/test/test/region/indian.rb +90 -85
  90. data/test/test/region/m17n.rb +7 -7
  91. data/test/test/region/mayan.rb +195 -195
  92. data/test/test/region/residue.rb +153 -153
  93. data/test/test/tmposition.rb +11 -1
  94. data/when_exe.gemspec +2 -2
  95. metadata +95 -8
  96. data/test/test.rb.config +0 -1
@@ -1,1632 +1,1640 @@
1
- # -*- coding: utf-8 -*-
2
- =begin
3
- Copyright (C) 2011-2014 Takashi SUGA
4
-
5
- You may use and/or modify this file according to the license described in the LICENSE.txt file included in this archive.
6
- =end
7
-
8
- #
9
- # An implementation of RFC 5545 - iCalendar ( http://tools.ietf.org/html/rfc5545 )
10
- #
11
- # == Extensions of RFC 5545
12
- # === Multi Calendar
13
- # * CALSCALE Property can specify calendars other than GREGORIAN
14
- # The value of CALSCALE Property is capitalized.
15
- # * Date and Time Representation of DTSTART Property
16
- # When.exe Standard Representation is available except ','. (',' cannot be used as a decimal mark)
17
- # Calendar can be specified by a notation '^^calnedar' or '^calnedar'.
18
- # (same as epoch of When::TM::CalendarEra)
19
- # === The extension of RRULE property
20
- # * FREQ=duration
21
- # When.exe Standard Representation により duration を指定できる。
22
- # ただし、この指定は BYxxx とは共存できない。
23
- # * BYHOUR=h(,..)
24
- # h に When.exe Standard Representation を使用できる
25
- # example: 'BYHOUR=1,2'
26
- # 夏時間から標準時間への切り替え時に 夏時間の1時,標準時間の2時 を生成
27
- # example: 'BYHOUR=1,1=,2'
28
- # 夏時間から標準時間への切り替え時に 夏時間の1時,標準時間の1時,標準時間の2時 を生成
29
- # 他のソフトウェアとの互換性を損なう可能性があるが、本ライブラリは両方の動作の違いを
30
- # 記述できる必要があると判断。
31
- # * BYDAY/c=n*e±s(,..)
32
- # c で指定したWhen::CalendarNoteオブジェクトのイベントeのうち、
33
- # n番目のものについて、その±s日目
34
- # デフォルトは n=all, s=0, cとeは省略不可(eはcのメソッドとして定義されている必要がある)
35
- # example: 'BYDAY/Christian=easter-2' は、キリスト教の聖金曜日
36
- # example: 'BYDAY/SolarTerms=term180' は、秋分日(太陽黄経が180度になる日)
37
- # * BYDAY/d=n*m±s(,..);DAYST=b
38
- # ユリウス日をdで除した余りがmになる日のうち、n番目のものについて、その±s日目
39
- # m は b を基点として計算する
40
- # デフォルトは n=all, s=0, b=0, dとmは省略不可
41
- # /d がない場合、次項のBYWEEKDAYとして扱い、RFC 5545に対する互換性を確保する
42
- # * BYWEEKDAY=n*m±s(,..);WKST=b
43
- # ユリウス日を7で除した余りがmになる日のうち、n番目のものについて、その±s日目
44
- # m は b を基点として計算する(mとbはともに文字列'MO','TU','WE','TH','FR','SA','SU'で指定)
45
- # デフォルトは n=all, s=0, b=MO, mは省略不可
46
- # * BYYEAR/d=n*m±s(,..);YEARST=b
47
- # 通年をdで除した余りがmになる年のうち、n番目のものについて、その±s年目
48
- # m は b を基点として計算する
49
- # デフォルトは n=all, s=0, b=4, dとmは省略不可
50
- # 通年の意味は暦法に依存する
51
- # === icd 形式の多言語対応
52
- # * NAMESPACE Property を追加
53
- # * LOCALE Property を追加
54
- # * SUMMARY Property で[...]表現を多言語対応文字列として解釈する
55
- # * RFC6350 ( http://tools.ietf.org/html/rfc6350 ) 方式にも対応
56
- # === Content Lines
57
- # RFC 5545 3.1 Content Lines では、記述が75文字を超える場合、改行してインデントする。
58
- # 本ライブラリでは文字数によらず、BEGIN:の次の行を基準にして、より深いインデントがある場合は、
59
- # 論理的1行を物理的に分割したものとみなして解釈している。実質的には上位互換と判断している
60
- #
61
- # == Limitations from RFC 5545
62
- # * VALARM
63
- # VENENT や VTODO に包含されている VALARM は無視される。
64
- # 包含元のイベントからの差分時刻での計算に対応していないため。
65
- # * RRULE
66
- # BYYEARDAY, BYMONTHDAY, BYDAY が存在する場合 BYWEEKDAY の n と s は指定できない。
67
- # BYYEARDAY, BYMONTHDAY, BYWEEKDAY が存在する場合 BYDAY の n と s は指定できない。
68
- module When::V
69
-
70
- # iCalendar を構成するクラス群の共通抽象クラス
71
- #
72
- # RFC 5545 のクラスは、有無のチェックを除いてProperty の扱いが共通なので、
73
- # Property の扱いを、本クラスにまとめて記述している。
74
- #
75
- class Root < When::BasicTypes::Object
76
-
77
- Properties = [[],[],[],[],[]]
78
- Classes = nil
79
- DefaultUnique = ['calscale', 'namespace', 'locale']
80
- DefaultOptional = ['x_prop', 'iana_prop']
81
- AwareProperties = DefaultUnique +
82
- ['tzoffsetfrom', 'tzoffsetto', 'tzname',
83
- 'dtstart', 'dtend', 'due',
84
- 'repeat', 'duration',
85
- 'rrule', 'rdate', 'exdate', 'exevent',
86
- 'summary', 'freebusy']
87
-
88
- #
89
- # iCalendar クラス群の属性
90
- # @return [Hash] { String => When::Parts::Resource::ContentLine }
91
- #
92
- attr_reader :property
93
-
94
- #
95
- # デフォルトのWhen::TM::Calendar
96
- # @return [When::TM::Calendar]
97
- #
98
- # @note
99
- # RFC 5545 では、'GREGORIAN' のみ指定可能としている。
100
- # CALSCALE Property 文字列を capitalize したものに、
101
- # prefix _c:(=http://hosi.org/When/CalendarTypes/)を補い
102
- # When::TM::Calendar オブジェクトの定義を取得する。
103
- #
104
- attr_reader :calscale
105
-
106
- # 指定の日時を含むか?
107
- #
108
- # @param [When::TM::TemporalPosition] date
109
- #
110
- # @return [Boolean]
111
- # [ true - 含む ]
112
- # [ false - 含まない ]
113
- #
114
- def include?(date)
115
- first = enum_for(date).next
116
- return first.include?(date) if first.kind_of?(When::Parts::GeometricComplex)
117
- return first == date if first.precision <= date.precision
118
- return false
119
- end
120
-
121
- # イテレータの生成
122
- # @private
123
- def _enumerator(*args)
124
- options = When::Parts::Enumerator._options(args)
125
- args << options
126
- exdate = options[:exdate]
127
-
128
- enumerators = _enumerator_list(args)
129
- raise ArgumentError, "No enumerator exists" if (enumerators.length==0)
130
-
131
- # Enumerator の生成
132
- enumerator =
133
- if (enumerators.length==1 && exdate.node.size==0)
134
- enumerators[0]
135
- else
136
- options[:exdate] = exdate
137
- When::Parts::Enumerator::Integrated.new(self, enumerators, *args)
138
- end
139
- if ::Object.const_defined?(:Date) && ::Date.method_defined?(:+) && (args[0].kind_of?(Range) ? args[0].first : args[0]).kind_of?(::Date)
140
- enumerator.instance_eval %Q{
141
- alias :_succ_of_super :succ
142
- def succ
143
- result = _succ_of_super
144
- result.kind_of?(When::TimeValue) ? result.to_date_or_datetime : result
145
- end
146
- }
147
- end
148
- enumerator
149
- end
150
- alias :to_enum :_enumerator
151
- alias :enum_for :_enumerator
152
-
153
- private
154
-
155
- # オブジェクトの生成
156
- def initialize(*args)
157
- # option の取得
158
- options = args[-1].kind_of?(Hash) ? args.pop.dup : {}
159
-
160
- # 包含関係
161
- @_pool = {}
162
- @_pool['..'] = options['..']
163
-
164
- # parsed 部の属性化
165
- @property = {}
166
- @namespace = @_pool['..'].respond_to?(:namespace) ? @_pool['..'].namespace : {}
167
- if options['.']
168
- _parse_from_file(options)
169
- else
170
- _parse_from_code(options)
171
- end
172
- _set_variables
173
-
174
- # 属性の存在チェック & 設定
175
- _initialize_attributes(_attribute_appearance(self.class::Properties)) # .const_get(:Properties)))
176
-
177
- # 包含オブジェクトの生成
178
- _child(options, self.class::Classes) #.const_get(:Classes))
179
- end
180
-
181
- # ファイルからの属性読み込み
182
- def _parse_from_file(options)
183
- options['.'].each do |v|
184
- v = When::Parts::Resource._parse(v)
185
- _parse_altid(@property, v) if v.kind_of?(ContentLine)
186
- end
187
-
188
- keys = @property.keys
189
- if keys.delete('namespace')
190
- content = @property['namespace'][0]
191
- @property['namespace'] = content if @property['namespace'].size == 1
192
- if content.attribute['prefix']
193
- begin
194
- @namespace[content.attribute['prefix'].object] = content.object
195
- end while (content = content.same_altid)
196
- else
197
- @namespace.update(When::Locale._namespace(content.object))
198
- end
199
- end
200
-
201
- keys.each do |key|
202
- @property[key].each do |content|
203
- content.object = When::BasicTypes::M17n.new(content, @namespace, []) if content.same_altid
204
- end
205
- @property[key] = @property[key][0] if @property[key].size == 1
206
- end
207
- end
208
-
209
- # コードからの属性読み込み
210
- def _parse_from_code(options)
211
- options.each_pair do |key, value|
212
- @property[key] = ContentLine.new(key, value) if key.kind_of?(String)
213
- end
214
- @property['dtstamp'] ||= ContentLine.new('dtstamp',
215
- When.now.to_s.gsub(/[-:]/,'')) if self.class::Properties[0].index('dtstamp')
216
- @property['uid'] ||= ContentLine.new('uid',
217
- @property['dtstamp'].object + '-auto') if self.class::Properties[0].index('uid')
218
- end
219
-
220
- # @propertyの個別属性化
221
- def _set_variables
222
- @property.each_key do |key|
223
- next if respond_to?(key)
224
- instance_eval %Q{
225
- def #{key}
226
- @property['#{key}'].object
227
- end
228
- }
229
- end
230
- end
231
-
232
- # 包含オブジェクトの生成
233
- def _child(options, classes)
234
- @child = []
235
- opt = options.dup
236
- opt['..'] = self
237
- if options['.']
238
- options['.'].each do |v|
239
- next unless (v.kind_of?(Array) && v[0].kind_of?(Class))
240
- raise ArgumentError, "The #{self.class} cannot include #{v[0]}" if (classes && !classes.index(v[0]))
241
- opt['.'] = v
242
- obj = v[0].new(opt)
243
- @child << obj
244
- @_pool[obj.label.to_s] = obj
245
- end
246
- else
247
- options.each_pair do |key, value|
248
- next unless key.kind_of?(Class)
249
- obj = key.new(opt.merge(value))
250
- @child << obj
251
- @_pool[obj.label.to_s] = obj
252
- end
253
- end
254
- end
255
-
256
- # attribute の出現数のチェック
257
- def _attribute_appearance(attributes)
258
-
259
- require_unique, require, unique, almost_unique, optional = attributes
260
-
261
- # REQUIRED but MUST NOT occur more than once
262
- require_unique.each do |key|
263
- unless @property[key].kind_of?(When::Parts::Resource::ContentLine)
264
- raise ArgumentError, "The #{key.upcase.gsub(/_/,'-')} is REQUIRED but MUST NOT occur more than once"
265
- end
266
- end
267
-
268
- # REQUIRED and MAY occur more than once
269
- require.each do |key|
270
- unless @property[key]
271
- raise ArgumentError, "The #{key.upcase.gsub(/_/,'-')} is REQUIRED and MAY occur more than once"
272
- end
273
- @property[key] = [@property[key]] if (@property[key].kind_of?(When::Parts::Resource::ContentLine))
274
- end
275
-
276
- # OPTIONAL but MUST NOT occur more than once
277
- (unique + DefaultUnique).each do |key|
278
- if (@property[key].kind_of?(Array))
279
- raise ArgumentError, "The #{key.upcase.gsub(/_/,'-')} is OPTIONAL but MUST NOT occur more than once"
280
- end
281
- end
282
-
283
- # OPTIONAL but SHOULD NOT occur more than once
284
- almost_unique.each do |key|
285
- if (@property[key].kind_of?(Array))
286
- raise ArgumentError, "The #{key.upcase.gsub(/_/,'-')} is OPTIONAL but SHOULD NOT occur more than once"
287
- end
288
- end
289
-
290
- # OPTIONAL and MAY occur more than once
291
- (optional + DefaultOptional).each do |key|
292
- @property[key] = [@property[key]] if (@property[key].kind_of?(When::Parts::Resource::ContentLine))
293
- end
294
-
295
- # Other Properties
296
- allowed_attributes = require_unique + require + unique + almost_unique + optional + DefaultUnique + DefaultOptional
297
- #@property.each_key do |key|
298
- # raise ArgumentError, "The #{key.upcase.gsub(/_/,'-')} is not allowed" unless allowed_attributes.index(key)
299
- #end
300
-
301
- return (AwareProperties & allowed_attributes)
302
- end
303
-
304
- # attribute の設定
305
- def _initialize_attributes(aware)
306
-
307
- # calscale の登録
308
- @calscale = @_pool['..'].calscale if @_pool['..'].respond_to?(:calscale)
309
- @calscale = @property['calscale'].object if @property['calscale']
310
- @calscale = When.Resource((@calscale||'GREGORIAN').capitalize, '_c:') unless (@calscale.kind_of?(When::TM::Calendar))
311
-
312
- # locale の登録
313
- @locale = @_pool['..'].respond_to?(:locale) ? @_pool['..'].locale : []
314
- @locale = When::Locale._locale(@property['locale'].object) if @property['locale']
315
-
316
- # tzoffsetfrom の登録
317
- if @property['tzoffsetfrom']
318
- object = @property['tzoffsetfrom'].object
319
- new_zone = object.to_s # See RFC5545 page.66
320
- @tzoffsetfrom = When.Clock(object).dup
321
- @tzoffsetfrom.tz_prop = self
322
- else
323
- new_zone = ''
324
- end
325
-
326
- # tzoffsetto, tzname の登録
327
- if @property['tzoffsetto']
328
- clock = When.Clock(@property['tzoffsetto'].object).dup
329
- clock.tz_prop = self
330
- @tzname = []
331
- (@property['tzname']||[]).each do |tzname|
332
- tz = tzname.object # ↓このチェックは不十分?
333
- When::TM::Clock.synchronize do
334
- raise ArgumentError, "Conflict tzname: #{tz}" if When::TM::Clock[tz] &&
335
- When::TM::Clock[tz].universal_time != clock.universal_time
336
- When::TM::Clock[tz] = clock
337
- end
338
- @tzname << tz
339
- end
340
- @tzoffsetto = clock
341
- @tzname = @tzname[0] if (@tzname.length <= 1)
342
- end
343
-
344
- # due の登録
345
- date_options = {:frame=>@calscale}
346
- if @property['due']
347
- @due = When.when?(@property['due'].attribute['.'] ? @property['due'].attribute['.'] + new_zone :
348
- @property['due'].object, date_options)
349
- end
350
-
351
- # dtstart の登録
352
- if @property['dtstart']
353
- @dtstart = When.when?(@property['dtstart'].attribute['.'] ? @property['dtstart'].attribute['.'] + new_zone :
354
- @property['dtstart'].object, date_options)
355
- @dtstart.clk_time.frame = @tzoffsetfrom unless (new_zone == '') # See RFC5545 page.66
356
-
357
- @first_occurrence = "Include"
358
- else
359
- @dtstart = When.now(@due||{})
360
- @first_occurrence = "Don't care"
361
- end
362
-
363
- # dtend の登録
364
- if @property['dtend']
365
- @dtend = When.when?(@property['dtend'].attribute['.'] ? @property['dtend'].attribute['.'] + new_zone :
366
- @property['dtend'].object, date_options)
367
- end
368
-
369
- # repeat の登録
370
- if aware.index('repeat')
371
- raise ArgumentError, "The DURATION MUST occur" if ( @property['repeat'] && !@property['duration'])
372
- raise ArgumentError, "The REPEAT MUST occur" if (!@property['repeat'] && @property['duration'])
373
- if @property['repeat']
374
- @repeat = @property['repeat'].object.to_i
375
- end
376
- end
377
-
378
- # duration の登録
379
- if aware.index('duration')
380
- if @property['duration']
381
- raise ArgumentError, "The DURATION should appear with DTSTART" unless (@dtstart||@repeat)
382
- raise ArgumentError, "The DTEND and DURATION are exclusive" if (@dtend)
383
- raise ArgumentError, "The DUE and DURATION are exclusive" if (@due)
384
- duration = @property['duration'].object
385
- duration = When.Duration(duration) unless duration.kind_of?(Numeric)
386
- elsif @dtend
387
- duration = When::TM::IntervalLength.difference(@dtend, @dtstart)
388
- end
389
- duration_precision = When::Coordinates::PERIOD[duration.to_s]
390
- @duration = duration unless (duration_precision && @dtstart.precision <= duration_precision)
391
- end
392
-
393
- # rrule の登録
394
- if aware.index('rrule')
395
- @rrule = []
396
- @rrule = @property['rrule'].map {|v| Event::Enumerator._decode_rule(v.object, @dtstart, new_zone)} if @property['rrule']
397
- end
398
-
399
- # rdate の登録
400
- if aware.index('rdate')
401
- @rdate = []
402
- @exevent = []
403
- @exdate = When::Parts::GeometricComplex.new()
404
- if (@property['rdate'])
405
- @rdate = @property['rdate'].inject([]) do |sum, v|
406
- if v.kind_of?(When::Parts::Resource::ContentLine)
407
- if new_zone == ''
408
- sum += When.when?(v.attribute['.'].split(/,/), date_options)
409
- else
410
- sum += When.when?((v.attribute['.'].split(/,/).map {|d| d + new_zone}), date_options)
411
- end
412
- else
413
- sum << v
414
- end
415
- end
416
- end
417
- unless new_zone == ''
418
- @rdate.each do |date|
419
- date.clk_time.frame = @tzoffsetto
420
- end
421
- end
422
- end
423
-
424
- # exdate の登録
425
- if @property['exdate']
426
- dates = @property['exdate'].inject([]) do |sum, v|
427
- if v.kind_of?(When::Parts::Resource::ContentLine)
428
- sum += When.when?(v.attribute['.'].split(/,/), date_options)
429
- else
430
- sum << v
431
- end
432
- end
433
- dates.each do |date|
434
- @exdate |= date
435
- end
436
- end
437
-
438
- # exevent の登録
439
- if @property['exevent']
440
- @exevent += @property['exevent'].object.split(/,/)
441
- end
442
-
443
- # summary の登録
444
- term_options = {'namespace'=>@namespace, 'locale'=>@locale}
445
- if @property['summary']
446
- text = @property['summary'].object
447
- @summary = (text =~ /\A\[/) ? m17n(text, nil, nil, term_options) : text
448
- end
449
-
450
- # freebusy の登録
451
- if aware.index('freebusy')
452
- @freebusy = []
453
- if (@property['freebusy'])
454
- @freebusy = @property['freebusy'].inject([]) do |sum, v|
455
- if v.kind_of?(When::Parts::Resource::ContentLine)
456
- sum += When.when?((v.attribute['.']||v.object).split(/,/), date_options)
457
- else
458
- sum << v
459
- end
460
- end
461
- end
462
- @rdate = @freebusy
463
- @rrule = []
464
- end
465
- end
466
- end
467
-
468
- # ひとつの ics 形式ファイルをまとめて保持する
469
- #
470
- # BEGIN:VCALENADR...END:VCALENDAR のブロックに対応
471
- #
472
- class Calendar < Root
473
-
474
- Properties= [['prodid', 'version'], [],
475
- ['method'], [], []]
476
- Classes = nil
477
-
478
- attr_accessor :child
479
- protected :child=
480
-
481
- # When::V::Event の検索
482
- #
483
- # @param [Hash] keys { key => value }
484
- #
485
- # @return [When::V::Calendar] key で指定する Property の値が value に一致する When::V::Event のみを持つ self の複製を作る
486
- # (一致の判断は演算子 === による)
487
- #
488
- def intersection(keys={})
489
- copy = self.dup
490
- copy.child = @child.select {|ev|
491
- if ev.kind_of?(Event)
492
- keys.each_pair do |key, value|
493
- case value
494
- when String ; break unless ev.property[key].object.index(value)
495
- when Regexp ; break unless (ev.property[key].object =~ value)
496
- end
497
- end
498
- else
499
- true
500
- end
501
- }
502
- return copy
503
- end
504
- alias :subset :intersection
505
-
506
- # @private
507
- def _enumerator_list(args)
508
- (@child.reject {|el| !el.kind_of?(Event)}).inject([]) { |sum, ev|
509
- sum += ev._enumerator_list(args)
510
- }
511
- end
512
- end
513
-
514
- # Eventを定義する
515
- #
516
- # BEGIN:VEVENT...END:VEVENT のブロックに対応
517
- #
518
- class Event < Root
519
-
520
- Properties= [['dtstamp', 'uid', 'dtstart'], [],
521
- ['class', 'created', 'description', 'geo',
522
- 'last_modified', 'location', 'organizer', 'priority',
523
- 'seq', 'status', 'summary', 'transp',
524
- 'url', 'recurid', 'dtend', 'duration'], [],
525
- ['rrule',
526
- 'attach', 'attendee', 'categories', 'comment', 'contact',
527
- 'exdate', 'exevent', 'rstatus', 'related',
528
- 'resources', 'rdate']]
529
- # Classes = [V::Root, V::Alarm]
530
-
531
- class << self
532
- include When::Parts::Resource::Pool
533
-
534
- # When::V::Event Class のグローバルな設定を行う
535
- #
536
- # @param [When::TM::IntervalLength] default_until
537
- #
538
- # @note
539
- # RRULE の条件が成立しない場合に無限ループにおちいることを避けるため
540
- # 他に指定がなくとも、計算を打ち切るようにしている。その打ち切り時間
541
- # (When.now + default_until)を本メソッドで指定している。
542
- # default_until の指定がない場合、default_until は 1000年と解釈する。
543
- #
544
- # @note
545
- # 本メソッドでマルチスレッド対応の管理変数の初期化を行っている。
546
- # このため、本メソッド自体はスレッドセーフでない。
547
- #
548
- def _setup_(default_until=nil)
549
- @_lock_ = Mutex.new if When.multi_thread
550
- @_pool = {}
551
- @default_until = default_until
552
- end
553
-
554
- # 設定情報を取得する
555
- #
556
- # @return [Hash] 設定情報
557
- #
558
- def _setup_info
559
- {:until => default_until}
560
- end
561
-
562
- # 最大打ち切り時間
563
- #
564
- # @return [When::TM::IntervalLength]
565
- def default_until
566
- @default_until ||= 1000*When::TM::Duration::YEAR
567
- end
568
- end
569
-
570
- # SUMMARY Property
571
- #
572
- # @return [String, When::BasicTypes::M17n]
573
- #
574
- attr_reader :summary
575
-
576
- # RRULE Property
577
- #
578
- # @return [Hash]
579
- #
580
- # @note
581
- # iCalendar の RRULE を Hash に展開したものを保持している。
582
- # RRULE は、年のサイクルや7日以外の日のサイクルおよび夏時間の切り替えを
583
- # 扱えるように RFC 5545 から拡張されている。
584
- #
585
- attr_reader :rrule
586
-
587
- # DTSTART Property
588
- #
589
- # @return [When::TM::TemporalPosition, When::Parts::GeometricComplex]
590
- #
591
- attr_reader :dtstart
592
-
593
- # DTEND Property
594
- #
595
- # @return [When::TM::TemporalPosition, When::Parts::GeometricComplex]
596
- #
597
- attr_reader :dtend
598
-
599
- # DURATION Property
600
- #
601
- # @return [When::TM::Duration]
602
- #
603
- # @note
604
- # DTSTART Property が保持する When::TM::TemporalPosition の分解能で識別できない
605
- # 時間差はイベント継続中とみなすので、例えば分解能が DAY の場合、DURATION Porperty
606
- # に When.Duration('P1D')と指定する必要はない。
607
- # DTEND Property が指定された場合、DURATION Property に変換して保持する。
608
- #
609
- attr_reader :duration
610
-
611
- # EXDATE Property
612
- #
613
- # @return [When::Parts::GeometricComplex]
614
- #
615
- attr_reader :exdate
616
-
617
- # RDATE Property
618
- #
619
- # @return [Array<When::TM::TemporalPosition, When::Parts::GeometricComplex>]
620
- #
621
- # @note
622
- # RRULE の COUNT が指定されている場合、後で途中の系列を抜き出すような指定をされても
623
- # よいように、Enumerator 生成時にCOUNT分の計算をして RDATE Property に登録する。
624
- # このため、COUNTに大きな値を指定すると、Enumerator 生成に予想外の時間がかかることが
625
- # ある。
626
- #
627
- attr_reader :rdate
628
-
629
- # DTSTART Property を first occurrence とするか
630
- #
631
- # @return [String]
632
- # [ 'Include' - first occurrence とする ]
633
- # [ 'Exclude' - first occurrence しない ]
634
- # [ それ以外 - RRULE により該当する場合に first occurrence とする ]
635
- #
636
- # @note
637
- # RFC 5545 では 'Include' となっているが、それ以外の振る舞いが可能なように拡張。
638
- #
639
- attr_reader :first_occurrence # See RFC 5545 [Page 41] 3rd paragraph
640
-
641
- # ユニーク識別名 - UID Property をユニーク識別名とする。
642
- #
643
- # @return [String]
644
- #
645
- def label
646
- @label ||= @property['uid'].object
647
- end
648
-
649
- # 最後のイベント
650
- #
651
- # @return [When::TM::TemporalPosition, When::Parts::GeometricComplex]
652
- #
653
- # @note
654
- # 無限に続く可能性がある場合、When::TimeValue::Max(+Infinity)
655
- #
656
- def dtstop
657
- return @dtstop if (@dtstop)
658
-
659
- @dtstop = @dtstart
660
- @rdate.each do |date|
661
- @dtstop = date if (date > @dtstop)
662
- end
663
- @rrule.each do |rrule|
664
- unless (rrule['UNTIL'])
665
- @dtstop = When::TimeValue::Max
666
- break
667
- end
668
- @dtstop = rrule['UNTIL'] if (rrule['UNTIL'] > @dtstop)
669
- end
670
- return @dtstop
671
- end
672
-
673
- # 順次実行
674
- #
675
- # @overload each()
676
- #
677
- # @overload each(range, count_limit=nil)
678
- # @param [Range, When::Parts::GeometricComplex] range 始点-range.first, 終点-range.last
679
- # @param [Integer] count_limit 繰り返し回数(デフォルトは指定なし)
680
- #
681
- # @overload each(first, direction, count_limit)
682
- # @param [When::TM::TemporalPosition] first 始点
683
- # @param [Symbol] direction :forward - 昇順, :reverse - 降順 (options[:direction]で渡してもよい)
684
- # @param [Integer] count_limit 繰り返し回数(デフォルトは指定なし, options[:count_limit]で渡してもよい)
685
- #
686
- # @return [Enumerator]
687
- #
688
- # @note block が与えられている場合、yield する。
689
- #
690
- def each(*args, &block)
691
- if args.length > 0
692
- super
693
- else
694
- super(@dtstart, &block)
695
- end
696
- end
697
-
698
- # @private
699
- def _enumerator_list(args)
700
- options = args[-1]
701
- rdate = @rdate.dup
702
- options[:exdate] = @exdate.dup if @exdate
703
- options[:exevent] = @exevent.map {|v| self[v]} if @exevent
704
- case (options['1st'] || @first_occurrence).capitalize
705
- when 'Include' ; rdate.unshift(@duration ? When::Parts::GeometricComplex.new(@dtstart, @duration) : @dtstart)
706
- when 'Exclude' ; options[:exdate] |= @dtstart
707
- end
708
-
709
- # 配下の Enumerator の初期化
710
- enumerators = []
711
- @rrule.each do |rrule|
712
- if @due && !(rrule['UNTIL'] && rrule['UNTIL'] <= @due)
713
- rrule = rrule.dup
714
- rrule['UNTIL'] = @due
715
- end
716
- if rrule['COUNT']
717
- Event::Enumerator.new(self, rrule, @dtstart, @duration, @dtstart, options).each {|date| rdate << date }
718
- else
719
- enumerators << Event::Enumerator.new(self, rrule, @dtstart, @duration, *args)
720
- end
721
- end
722
-
723
- enumerators.unshift(When::Parts::Enumerator::Array.new(self, rdate, *args[1..-1])) if (rdate.length > 0)
724
-
725
- return enumerators
726
- end
727
- end
728
-
729
- # Alarm を定義する
730
- #
731
- # BEGIN:VALARM...END:VALARM のブロックに対応
732
- #
733
- class Alarm < Event
734
-
735
- # ユニーク識別名 - ACTION Property をユニーク識別名とする。
736
- # @return [String]
737
- def label
738
- @label ||= @property['action'].object # TODO
739
- end
740
-
741
- # @private
742
- def initialize(options)
743
- # 包含関係
744
- @_pool = {}
745
- @_pool['..'] = options['..']
746
-
747
- # parsed 部の属性化
748
- _parsed(options)
749
-
750
- # 属性の存在チェック
751
- case (@property['action'].kind_of?(When::Parts::Resource::ContentLine) && @property['action'].object)
752
- when 'AUDIO'
753
- aware = _attribute_appearance([
754
- ['action', 'trigger'], [],
755
- ['duration', 'repeat', 'attach'], [], []])
756
- when 'DISPLAY'
757
- aware = _attribute_appearance([
758
- ['action', 'description', 'trigger'], [],
759
- ['duration', 'repeat'], [], []])
760
- when 'EMAIL'
761
- aware = _attribute_appearance([
762
- ['action', 'description', 'trigger', 'summary'], ['attendee'],
763
- ['duration', 'repeat'], [], ['attach']])
764
- else
765
- raise ArgumentError, "The ACTION is invalid"
766
- end
767
-
768
- # 属性の設定
769
- _initialize_attributes(aware)
770
-
771
- # 包含オブジェクトがないことの確認
772
- _child(options, [])
773
- end
774
- end
775
-
776
- # Todo を定義する
777
- #
778
- # BEGIN:VTODO...END:VTODO のブロックに対応
779
- #
780
- class Todo < Event
781
- Properties= [['dtstamp', 'uid'], [],
782
- ['class', 'completed', 'created', 'description',
783
- 'dtstart', 'geo', 'last_modified', 'location', 'organizer',
784
- 'percent', 'priority', 'recurid', 'seq', 'status',
785
- 'summary', 'url'], ['due', 'duration'],
786
- ['rrule',
787
- 'attach', 'attendee', 'categories', 'comment', 'contact',
788
- 'exdate', 'exevent', 'rstatus', 'related', 'resources',
789
- 'rdate']]
790
- Classes = [Alarm]
791
- end
792
-
793
- # Journal を定義する
794
- #
795
- # BEGIN:VJOURNAL...END:VJOURNAL のブロックに対応
796
- #
797
- class Journal < Event
798
-
799
- Properties= [['dtstamp', 'uid'], [],
800
- ['class', 'created', 'dtstart',
801
- 'last_modified', 'organizer', 'recurid', 'seq',
802
- 'status', 'summary', 'url'], [],
803
- ['rrule',
804
- 'attach', 'attendee', 'categories', 'comment', 'contact',
805
- 'description', 'exdate', 'exevent', 'related', 'rdate',
806
- 'rstatus']]
807
- Classes = []
808
- end
809
-
810
- # Freebusy を定義する
811
- #
812
- # BEGIN:VFREEBUSY...END:VFREEBUSY のブロックに対応
813
- #
814
- class Freebusy < Event
815
-
816
- Properties= [['dtstamp', 'uid'], [],
817
- ['contact', 'dtstart', 'dtend',
818
- 'organizer', 'url'], [],
819
- ['attendee', 'comment', 'freebusy', 'rstatus']]
820
- Classes = []
821
-
822
- attr_reader :freebusy
823
- end
824
-
825
- #
826
- # Timezone Property の共通抽象クラス
827
- #
828
- class TimezoneProperty < Event
829
-
830
- Properties= [['dtstart', 'tzoffsetto', 'tzoffsetfrom'], [],
831
- [], [], ['rrule',
832
- 'comment', 'rdate', 'tzname']]
833
- Classes = []
834
-
835
- # TZNAME Property
836
- #
837
- # @return [Array<String(サブクラスであるWhen::BasicTypes::M17nでもよい)>]
838
- #
839
- # @note
840
- # RFC 5545 は複数個の指定を許しているため、Array としている。
841
- #
842
- attr_reader :tzname
843
-
844
- # TZOFFSETFROM Property
845
- #
846
- # @return [When::TM::Clock]
847
- #
848
- # @note
849
- # イベント時刻前の時間帯
850
- # 保持する When::TM::Clock の tz_prop は、本オブジェクトを参照している。
851
- #
852
- attr_reader :tzoffsetfrom
853
-
854
- # TZOFFSETTO Property
855
- #
856
- # @return [When::TM::Clock]
857
- #
858
- # @note
859
- # イベント時刻後の時間帯
860
- # 保持する When::TM::Clock の tz_prop は、本オブジェクトを参照している。
861
- #
862
- attr_reader :tzoffsetto
863
-
864
- # ユニーク識別名 - DTSTART Property をユニーク識別名とする
865
- # @return [String]
866
- def label
867
- @label ||= @property['dtstart'].attribute['.']
868
- end
869
- end
870
-
871
- # 標準時間帯を定義する
872
- #
873
- # BEGIN:STANDARD...END:STANDARD のブロックに対応
874
- #
875
- class Standard < TimezoneProperty
876
- end
877
-
878
- # 夏時間帯を定義する
879
- #
880
- # BEGIN:DAYLIGHT...END:DAYLIGHT のブロックに対応
881
- #
882
- class Daylight < TimezoneProperty
883
- end
884
-
885
- # Timezone を定義する
886
- #
887
- # BEGIN:VTIMEZONE...END:VTIMEZONE のブロックに対応
888
- #
889
- class Timezone < Root
890
-
891
- Properties= [['tzid'], [],
892
- ['last_modified', 'tzurl', 'x_lic_location'], [], []]
893
- Classes = [Standard, Daylight]
894
-
895
- extend When::Parts::Resource::Pool
896
-
897
- include When::Parts::Timezone::Base
898
-
899
- # ユニーク識別名 - TZID Property をユニーク識別名とする
900
- # @return [String]
901
- def label
902
- @label ||= @property['tzid'].object
903
- end
904
-
905
- # 同一の時間帯を用いた期間
906
- #
907
- # @param [When::TM::TemporalPositionに変換可能な型] current_date 期間に含まれる日時
908
- #
909
- # @return [Range]
910
- # [ 期間が過去または未来に対して無限に続く場合は、GeometricComplex ]
911
- # [ 期間が有界の場合は、start...end という形式の Range ]
912
- #
913
- def current_period(current_date=Time.now)
914
- current_date = When.when?(current_date) unless current_date.kind_of?(When::TM::TemporalPosition)
915
- period = _tz_period(current_date.universal_time)
916
- range = period[1]
917
- return range if range.kind_of?(Range)
918
- GeometricComplex.new([period], !range)
919
- end
920
-
921
- # @private
922
- def _daylight(time)
923
- raise ArgumentError, "Needless daylight saving time evaluation" unless _need_validate
924
- frame, cal_date, clk_time = time
925
- time = frame.to_universal_time(cal_date, clk_time, @standard) if clk_time
926
- ndate = _neighbor_event_date(time)
927
- nprop = ndate.clock.tz_prop
928
- time = frame.to_universal_time(cal_date, clk_time, nprop.tzoffsetfrom) if clk_time && !@standard.equal?(nprop.tzoffsetfrom)
929
- (time >= ndate.universal_time) ? nprop.tzoffsetto : nprop.tzoffsetfrom
930
- end
931
-
932
- # @private
933
- def _need_validate
934
- @tz_difference != 0
935
- end
936
-
937
- private
938
-
939
- # オブジェクトの生成
940
- def initialize(options)
941
- super
942
-
943
- @child.each do |prop|
944
- @dtstart = prop.dtstart if (@dtstart==nil || prop.dtstart < @dtstart)
945
- @dtstop = prop.dtstop unless (prop.dtstop.kind_of?(When::TimeValue) &&
946
- @dtstop.kind_of?(When::TimeValue) &&
947
- prop.dtstop <= @dtstop)
948
- [prop.tzoffsetfrom, prop.tzoffsetto].each do |tz|
949
- @daylight = tz if (@daylight==nil || tz.universal_time < @daylight.universal_time)
950
- @standard = tz if (@standard==nil || tz.universal_time > @standard.universal_time)
951
- end
952
- end
953
- @tz_difference = @standard.universal_time - @daylight.universal_time
954
-
955
- name = @property['tzid'].object
956
- case self.class[name]
957
- when nil ; self.class[name] = self
958
- when Array ; self.class[name] << self
959
- else ; self.class[name] = [self.class[name], self]
960
- end
961
-
962
- @_lock_ = Mutex.new if When.multi_thread
963
- @range = []
964
- end
965
-
966
- # 指定の日時に最も近い、時間帯変更イベントの日時
967
- #
968
- # @param [Numeric] current_time 捜索の基点の日時の universal time
969
- #
970
- # @return [When::TM::TemporalPosition] 捜索の基点の日時に最も近い、時間帯変更イベントの日時
971
- #
972
- def _neighbor_event_date(current_time)
973
- _tz_period(current_time)[0]
974
- end
975
-
976
- def _tz_period(current_time)
977
- return [@dtstart, false] if (current_time <= @dtstart.universal_time)
978
- return [@dtstop, true ] if (@dtstop.kind_of?(When::TimeValue) &&
979
- current_time >= @dtstop.universal_time)
980
-
981
- # Thread 要注意 - @range は生成後に更新
982
- synchronize do
983
- @range.each do |range|
984
- from = current_time - range.first.universal_time
985
- to = range.last.universal_time - current_time
986
- return [(from < to) ? range.first : range.last, range] if (from >= 0 && to > 0)
987
- end
988
- early = _neighbor(current_time, :reverse)
989
- late = _neighbor(current_time, :forward)
990
- from = current_time - early.universal_time
991
- to = late.universal_time - current_time
992
- @range << (early...late)
993
- return [(from < to) ? early : late, @range[-1]]
994
- end
995
- end
996
-
997
- def _neighbor(current_time, direction)
998
- event = nil
999
- minimum = nil
1000
- @child.each do |prop|
1001
- date = prop.enum_for(current_time, direction, 1).succ
1002
- if (date)
1003
- diff = (date.universal_time - current_time).abs
1004
- if (minimum == nil || minimum > diff)
1005
- event = date
1006
- minimum = diff
1007
- end
1008
- end
1009
- end
1010
- return event
1011
- end
1012
- end
1013
-
1014
- class Event < Root
1015
-
1016
- Classes = [Root, Alarm]
1017
-
1018
- #
1019
- # When::V::Event が使用する Enumerator
1020
- #
1021
- class Enumerator < When::Parts::Enumerator
1022
-
1023
- include When
1024
- include Coordinates
1025
-
1026
- FreqIndex = {'YEARLY' =>YEAR, 'MONTHLY' =>MONTH,
1027
- 'WEEKLY' =>WEEK, 'DAILY' =>DAY,
1028
- 'HOURLY' =>HOUR, 'MINUTELY' =>MINUTE, 'SECONDLY' =>SECOND}
1029
- PostFreqIndex = {'BYYEAR' =>YEAR, 'BYMONTH' =>MONTH, 'BYWEEKNO' =>WEEK,
1030
- 'BYYEARDAY'=>DAY, 'BYMONTHDAY'=>DAY,
1031
- 'BYWEEKDAY'=>DAY, 'BYDAY' =>DAY,
1032
- 'BYHOUR' =>HOUR, 'BYMINUTE' =>MINUTE, 'BYSECOND' =>SECOND}
1033
-
1034
- # RRULE Property - iCalendar の RRULE を Hash に展開したものを保持している
1035
- # @return [Hash]
1036
- attr_reader :rule
1037
-
1038
- # 繰り返しの始点
1039
- # @return [When::TM::TemporalPosition, When::Parts::GeometricComplex]
1040
- attr_reader :dtstart
1041
-
1042
- # 多重繰り返しロジック
1043
- # @return [Array<When::V::Event::Enumerator::Logic>]
1044
- attr_reader :logics
1045
-
1046
- # 多重繰り返しの現在状態
1047
- # @return [Array<When::V::Event::Enumerator::Step>]
1048
- attr_accessor :steps
1049
- protected :steps=
1050
-
1051
- #
1052
- # 巻き戻す
1053
- #
1054
- # @return [rewind された self]
1055
- #
1056
- def _rewind
1057
- @steps = [Step.new(_first_seed(@first, @dtstart))]
1058
- super
1059
- end
1060
-
1061
- private
1062
-
1063
- # オブジェクトの生成
1064
- def initialize(*args)
1065
- @options = When::Parts::Enumerator._options(args)
1066
- @exdate = @options.delete(:exdate)
1067
- @exevent = @options.delete(:exevent)
1068
- @parent, @rule, @dtstart, @duration, *rest = args
1069
- @dtstart = When.when?(@dtstart)
1070
- @rule = self.class._decode_rule(@rule, @dtstart) if (@rule.kind_of?(String))
1071
- @logics = @rule[:logics]
1072
- @tz_prop = nil
1073
- if (@dtstart.kind_of?(When::TM::DateAndTime))
1074
- clock = @dtstart.clock
1075
- if (clock.kind_of?(When::TM::Clock) &&
1076
- clock.tz_prop.kind_of?(TimezoneProperty) &&
1077
- (clock.tz_prop.dtstart == @dtstart))
1078
- @tz_prop = @dtstart.clock.tz_prop
1079
- @dtstart = @dtstart.dup._copy({:tz_prop=>nil, :validate=>:done})
1080
- end
1081
- end
1082
- _range(rest)
1083
- _rewind
1084
- end
1085
-
1086
- def _copy
1087
- copy = super
1088
- copy.steps = @steps.dup
1089
- return copy
1090
- end
1091
-
1092
- def _first_seed(target, dtstart)
1093
- if (@rule['FREQ'].kind_of?(When::TM::Duration))
1094
- @interval = @rule['FREQ']
1095
- @interval *= @rule['INTERVAL'] unless (@rule['INTERVAL'] == 1)
1096
- else
1097
- @interval = When::TM::PeriodDuration.new(@rule['INTERVAL'], FreqIndex[@rule['FREQ']])
1098
- end
1099
- return dtstart if (dtstart == target)
1100
- interval_time = (dtstart + @interval) - dtstart
1101
- return dtstart if (interval_time == 0)
1102
- duration = target.kind_of?(Numeric) ? target - dtstart.universal_time : (target - dtstart).duration
1103
- div, mod = duration.divmod(interval_time.duration)
1104
- seed = dtstart + (@interval * div)
1105
- case @direction
1106
- when :reverse ; seed += @interval while (seed <= target)
1107
- else ; seed -= @interval while (seed >= target)
1108
- end
1109
- return seed
1110
- end
1111
-
1112
- def _next_seed(seed)
1113
- case @direction
1114
- when :reverse ; return seed - @interval
1115
- else ; return seed + @interval
1116
- end
1117
- end
1118
-
1119
- def _succ(depth=0)
1120
- return nil if (@rule['COUNT'] && (@count >= @rule['COUNT']))
1121
- step = @steps[depth]
1122
- unless (step)
1123
- step = Step.new(_candidates(depth-1))
1124
- @steps[depth..-1] = [step]
1125
- end
1126
-
1127
- case depth
1128
- when 0
1129
- if (@logics.length == 0)
1130
- result = @steps[0]._current_date
1131
- @steps[0..-1] = [Step.new(_next_seed(result))]
1132
- else
1133
- loop do
1134
- result = _succ(depth+1)
1135
- break if (result)
1136
- @steps[0..-1] = [Step.new(_next_seed(@steps[0]._current_date))]
1137
- end
1138
- end
1139
- return (@direction==:reverse) ? nil : :next if (@dtstart > result)
1140
- return (@direction==:reverse) ? :next : nil if (@rule['UNTIL'] && (result > @rule['UNTIL']))
1141
- if (@tz_prop)
1142
- result = result.dup._copy({:tz_prop=>nil, :validate=>:done})
1143
- result.clock.tz_prop = @tz_prop
1144
- end
1145
- return result
1146
-
1147
- when logics.length
1148
- step._inc
1149
- return step._previuos_date
1150
-
1151
- else
1152
- while (step._current_date) do
1153
- result = _succ(depth+1)
1154
- return result if (result)
1155
- @steps[depth..-1] = [step._inc]
1156
- end
1157
- return nil
1158
- end
1159
- end
1160
-
1161
- def _candidates(depth)
1162
- # logics と seed
1163
- logics = @logics[depth]
1164
- seed = @steps[depth]._current_date
1165
- logics.cash ||= {}
1166
- return logics.cash[seed.universal_time] if logics.cash[seed.universal_time]
1167
-
1168
- # 上下限の決定
1169
- lower_bound, higher_bound = logics._bound(seed, @rule['WKST'])
1170
-
1171
- # 候補
1172
- list = logics._candidates(lower_bound, higher_bound).reject { |date|
1173
- # See RFC 5545 [Page 43] invalid date (e.g., February 30)
1174
- !(lower_bound.universal_time <= date.universal_time && date.universal_time < higher_bound.universal_time)
1175
- }
1176
- list = Array._sort(list, @direction)
1177
- list = Array._sort((@rule['BYSETPOS'].map {|pos| list[pos]}).compact, @direction) if @rule['BYSETPOS']
1178
-
1179
- # 結果をCash
1180
- logics.cash[seed.universal_time] = list
1181
- end
1182
-
1183
- # @private
1184
- def self._decode_rule(description, dtstart, new_zone='')
1185
- # 指定の読み込み
1186
- if description.kind_of?(Hash)
1187
- rule = description
1188
- else
1189
- rule = {}
1190
- description.split(/;/).each do |pair|
1191
- raise ArgumentError, "Invalid RRULE format" unless pair =~ /\A([^\/=]+)(\/(.+))?=(.*)\z/
1192
- k, f, c, v = $~[1..4]
1193
- case k
1194
- when 'BYYEAR', 'BYDAY', 'YEARPOS', 'DAYPOS', 'YEARST', 'DAYST'
1195
- rule[k] ||= {}
1196
- c ||= ""
1197
- raise ArgumentError, "The #{k}#{f} part MUST NOT occur more than once" if (rule[k][c])
1198
- rule[k][c] = v
1199
- else
1200
- raise ArgumentError, "Invalid RRULE format" if (f)
1201
- raise ArgumentError, "The #{k} part MUST NOT occur more than once" if (rule[k])
1202
- rule[k] = v
1203
- end
1204
- end
1205
- end
1206
-
1207
- # 整合性確認と省略された指定の設定
1208
- raise ArgumentError, "The UNTIL and COUNT are exclusive" if (rule['UNTIL'] && rule['COUNT'])
1209
- rule['FREQ'] = When.Duration(rule['FREQ']) unless rule['FREQ'].kind_of?(When::TM::Duration) ||
1210
- FreqIndex.key?(rule['FREQ'])
1211
- if (rule['UNTIL'].kind_of?(String))
1212
- rule['UNTIL'] += new_zone unless rule['UNTIL'] =~ /Z\z/
1213
- rule['UNTIL'] = When.when?(rule['UNTIL'])
1214
- end
1215
- unless rule['UNTIL']
1216
- rule['UNTIL'] = When.now + Event.default_until
1217
- end
1218
- rule['COUNT'] = rule['COUNT'].to_i if (rule['COUNT'])
1219
- rule['INTERVAL'] = (rule['INTERVAL'] || 1).to_i
1220
- rule['WKST'] = Residue.day_of_week(rule['WKST'] || 'MO')
1221
-
1222
- if (rule['BYSETPOS'])
1223
- rule['BYSETPOS'] = rule['BYSETPOS'].split(/,/).map {|v|
1224
- pos = v.to_i
1225
- pos -= 1 if (pos>0)
1226
- pos
1227
- }
1228
- end
1229
- ['YEARPOS', 'DAYPOS', 'YEARST', 'DAYST'].each do |by_part|
1230
- if (rule[by_part])
1231
- rule[by_part].each_pair do |c,v|
1232
- rule[by_part][c] = v.to_i
1233
- end
1234
- else
1235
- rule[by_part] = {}
1236
- end
1237
- end
1238
-
1239
- if (rule['BYDAY'])
1240
- v = rule['BYDAY'].delete("")
1241
- if (v)
1242
- raise ArgumentError, "The BYWEEKDAY and BYDAY are exclusive" if (rule['BYWEEKDAY'])
1243
- rule['BYWEEKDAY'] = v
1244
- end
1245
- rule.delete('BYDAY') if rule['BYDAY'].size == 0
1246
- end
1247
-
1248
- count = 0
1249
- if (rule['BYWEEKNO'])
1250
- raise ArgumentError, "The 'BYWEEKNO' is not allowed" unless (rule['FREQ'] == 'YEARLY')
1251
- count += 1
1252
- end
1253
- count += 1 if (rule['BYYEARDAY'])
1254
- raise ArgumentError, "The 'BYMONTHDAY' is not allowed" if (rule['BYMONTHDAY'] && (count>0))
1255
- count += 1 if (rule['BYMONTH'])
1256
- raise ArgumentError, "The BYMONTH, BYWEEKNO and BYYEARDAY are exclusive" if (count > 1)
1257
-
1258
- # ロジックオブジェクトの生成
1259
- if (rule['BYWEEKDAY'])
1260
- rule['BYWEEKDAY'] = Logic::Weekday.new('BYWEEKDAY', rule['BYWEEKDAY'])
1261
- end
1262
-
1263
- ['YEAR', 'DAY'].each do |part|
1264
- by_part = 'BY' + part
1265
- if (rule[by_part])
1266
- rule[by_part].each_pair do |ref,list|
1267
- position = rule[(by_part == 'BYYEAR') ? 'YEARPOS' : 'DAYPOS'][ref]
1268
- start = rule[(by_part == 'BYYEAR') ? 'YEARST' : 'DAYST' ][ref]
1269
- rule[by_part][ref] = ((ref.to_i==0) ? Logic::Enumerator :
1270
- Logic::Residue).new(by_part, list, position, ref, start)
1271
- end
1272
- end
1273
- end
1274
-
1275
- ['MONTH', 'WEEKNO', 'YEARDAY', 'MONTHDAY'].each do |part|
1276
- by_part = 'BY' + part
1277
- rule[by_part] = Logic.const_get(part.capitalize).new(by_part, rule[by_part]) if rule[by_part]
1278
- end
1279
-
1280
- ['HOUR', 'MINUTE', 'SECOND'].each do |part|
1281
- by_part = 'BY' + part
1282
- clock = dtstart.clock
1283
- leap = dtstart.time_standard.has_leap?
1284
- if rule[by_part]
1285
- if clock
1286
- index = When::Coordinates::PRECISION[part]
1287
- lower = clock.base[index]
1288
- upper = clock.unit[index] + lower
1289
- end
1290
- rule[by_part] = Logic.const_get(part.capitalize).new(by_part, rule[by_part], lower, upper, leap)
1291
- end
1292
- end
1293
-
1294
- freq_index = FreqIndex[rule['FREQ']]
1295
- if (dtstart && dtstart.has_time?)
1296
- ['HOUR', 'MINUTE', 'SECOND'].each do |part|
1297
- by_part = 'BY' + part
1298
- if (rule[by_part]==nil && freq_index && freq_index < PostFreqIndex[by_part])
1299
- base = dtstart[PostFreqIndex[by_part]]
1300
- rule[by_part] = Logic.const_get(part.capitalize).new(by_part, base) unless (base == 0)
1301
- end
1302
- end
1303
- end
1304
-
1305
- # 配列に登録
1306
- logics = []
1307
- ['BYYEAR', 'BYMONTH', 'BYWEEKNO', 'BYYEARDAY', 'BYMONTHDAY', 'BYDAY', 'BYWEEKDAY',
1308
- 'BYHOUR', 'BYMINUTE', 'BYSECOND'].each do |by_part|
1309
- if (rule[by_part])
1310
- if (rule[by_part].kind_of?(Hash))
1311
- rule[by_part].each_pair do |ref,logic|
1312
- raise ArgumentError, "The #{by_part} must have class specification" if (ref == "")
1313
- logic.freq_index = freq_index
1314
- logics << logic
1315
- end
1316
- else
1317
- rule[by_part].freq_index = freq_index
1318
- logics << rule[by_part]
1319
- end
1320
- rule.delete(by_part)
1321
- freq_index = [PostFreqIndex[by_part], freq_index].max if freq_index
1322
- end
1323
- end
1324
- raise ArgumentError, "BY part must not be specified" if rule['FREQ'].kind_of?(When::TM::Duration) &&
1325
- logics.length > 0
1326
- rule[:logics] = logics
1327
- rule[:parsed] = description
1328
- return rule
1329
- end
1330
-
1331
- #
1332
- # 多重繰り返しの現在状態
1333
- #
1334
- class Step
1335
-
1336
- attr_reader :index
1337
- attr_reader :date
1338
-
1339
- # @private
1340
- def _current_date
1341
- return @date[@index]
1342
- end
1343
-
1344
- # @private
1345
- def _previuos_date
1346
- return @date[@index-1]
1347
- end
1348
-
1349
- # @private
1350
- def _inc
1351
- @index += 1
1352
- return self
1353
- end
1354
-
1355
- # @private
1356
- def initialize(date)
1357
- @index = 0
1358
- @date = date.kind_of?(Array) ? date : [date]
1359
- end
1360
- end
1361
-
1362
- #
1363
- # 多重繰り返しロジック
1364
- #
1365
- class Logic
1366
- attr_accessor :freq_index
1367
- attr_reader :by_part
1368
- attr_reader :list
1369
- attr_accessor :cash
1370
- #protected :cash=
1371
-
1372
- # @private
1373
- def _bound(seed, week_start)
1374
- if (@freq_index == When::WEEK)
1375
- # 週の初め
1376
- bound = seed & week_start
1377
- if (bound == seed)
1378
- lower_bound = bound.floor(When::DAY, nil)
1379
- higher_bound = lower_bound + week_start.duration
1380
- else
1381
- higher_bound = bound.floor(When::DAY, nil)
1382
- lower_bound = higher_bound - week_start.duration
1383
- end
1384
- else
1385
- # 指定桁で切り捨て/切り上げ
1386
- lower_bound = seed.floor(@freq_index, nil)
1387
- higher_bound = seed.ceil(@freq_index, nil)
1388
- end
1389
- return [lower_bound, higher_bound]
1390
- end
1391
-
1392
- # 候補日時の生成
1393
- # @private
1394
- def _candidates(lower_bound, higher_bound)
1395
- # TODO unitBaseの扱い
1396
- return @list.map { |ord|
1397
- if (ord >= 0)
1398
- period = ord - 1
1399
- bound = lower_bound
1400
- else
1401
- period = ord
1402
- bound = higher_bound
1403
- end
1404
- index = PostFreqIndex[@by_part]
1405
- period = ord-bound[index] if (index>0)
1406
- result = bound + When::TM::PeriodDuration.new(period, index)
1407
- if period > 0 && result.universal_time < lower_bound.universal_time
1408
- clock = result.clock
1409
- case clock.tz_prop
1410
- when When::V::TimezoneProperty ; clock = clock.tz_prop.tzoffsetto
1411
- when When::Parts::Timezone ; clock = clock.tz_prop.standard
1412
- end
1413
- result = result._copy({:date=>result.cal_date, :validate=>:done, :events=>nil,
1414
- :time=>result.clk_time._copy({:clock=>clock})})
1415
- end
1416
- result
1417
- }
1418
- end
1419
-
1420
- # @private
1421
- def initialize(by_part, list)
1422
- @by_part = by_part
1423
- @list =
1424
- case list
1425
- when String ; When::Coordinates::Pair::_en_pair_array(list)
1426
- when Array ;
1427
- else ; [list]
1428
- end
1429
- end
1430
-
1431
- #
1432
- # 剰余類を用いた繰り返し
1433
- # 曜日の指定などに用いる
1434
- #
1435
- class Residue < Logic
1436
- # @private
1437
- def _candidates(lower_bound, higher_bound)
1438
- candidate = []
1439
- @list.each do |ord|
1440
- nth, residue, period = ord
1441
- shift = (period[PostFreqIndex[@by_part]] != 0)
1442
- raise ArgumentError, "n*m+/-s format not permitted" if (nth || shift) && @freq_index >= When::DAY
1443
- if (nth)
1444
- date = ((nth >= 0) ? lower_bound & residue :
1445
- higher_bound & residue)
1446
- date += period if shift
1447
- candidate << date
1448
- else
1449
- (lower_bound ^ residue).each do |date|
1450
- date += period if shift
1451
- break unless date < higher_bound
1452
- candidate << date
1453
- end
1454
- end
1455
- end
1456
- return candidate
1457
- end
1458
-
1459
- # @private
1460
- def initialize(by_part, list, position, ref, start)
1461
- @by_part = by_part
1462
- divisor = When::Coordinates::Pair._en_number(ref)
1463
- @list = list.split(/,/).map {|w|
1464
- raise ArgumentError, "The #{by_part} rule format error" unless w =~ /\A(([-+]?\d+)\*)?(.+?)([-+]\d+)?\z/
1465
- nth, spec, period = $~[2..4]
1466
- nth = (nth) ? nth.to_i : position
1467
- period = When::TM::PeriodDuration.new((period||0).to_i, PostFreqIndex[@by_part])
1468
- args = [spec.to_i, divisor, (nth) ? ((nth>0) ? nth-1 : nth) : 0]
1469
- case by_part
1470
- when 'BYYEAR'
1471
- start ||= 4
1472
- start = start % divisor
1473
- args << {'year'=>start}
1474
- when 'BYDAY'
1475
- start ||= 11
1476
- start = start % divisor
1477
- args << {'day'=>start} unless (start == 0)
1478
- end
1479
- residue = When::Coordinates::Residue.new(*args)
1480
- [nth, residue, period]
1481
- }
1482
- end
1483
- end
1484
-
1485
- #
1486
- # 外部 Enumerator 用いた繰り返し
1487
- # 春分日、秋分日、復活祭などの指定に用いる
1488
- #
1489
- class Enumerator < Logic
1490
- attr_reader :ref
1491
- attr_reader :start
1492
-
1493
- # @private
1494
- def _candidates(lower_bound, higher_bound)
1495
- candidate = []
1496
- @list.each do |ord|
1497
- nth, spec, period = ord
1498
- shift = (period[PostFreqIndex[@by_part]] != 0)
1499
- raise ArgumentError, "n*e+/-s format not permitted" if (nth || shift) && @freq_index >= When::DAY
1500
- if (nth==nil || nth>0)
1501
- enum = @ref.enum_for(lower_bound, :forward, {:event=>spec})
1502
- else
1503
- enum = @ref.enum_for(higher_bound, :reverse, {:event=>spec})
1504
- end
1505
- if (nth)
1506
- date = nil
1507
- nth.abs.times {date = enum.succ}
1508
- date += period if shift
1509
- candidate << date
1510
- else
1511
- enum.each do |date|
1512
- date += period if shift
1513
- break unless date < higher_bound
1514
- candidate << date
1515
- end
1516
- end
1517
- end
1518
- return candidate
1519
- end
1520
-
1521
- # @private
1522
- def initialize(by_part, list, position, ref, start)
1523
- @by_part = by_part
1524
- @ref = When.Resource(ref, '_n:')
1525
- @start = start
1526
- @list = list.split(/,/).map {|w|
1527
- raise ArgumentError, "The #{by_part} rule format error" unless w =~ /\A(([-+]?\d+)\*)?(.+?)([-+]\d+)?\z/
1528
- nth, spec, period = $~[2..4]
1529
- nth = (nth) ? nth.to_i : position
1530
- period = When::TM::PeriodDuration.new((period||0).to_i, PostFreqIndex[@by_part])
1531
- [nth, spec, period]
1532
- }
1533
- end
1534
- end
1535
-
1536
- #
1537
- # BYMONTHを実装
1538
- # @private
1539
- class Month < Logic
1540
- end
1541
-
1542
- #
1543
- # BYWEEKNOを実装
1544
- # @private
1545
- class Weekno < Logic
1546
- # @private
1547
- def _bound(seed, week_start)
1548
- duration = week_start.duration
1549
- center = duration / 2
1550
- # 1月4日を含む週
1551
- return [seed.floor(When::YEAR, nil), seed.ceil(When::YEAR, nil)].map { |s|
1552
- s += center
1553
- bound = s & week_start
1554
- bound -= duration if (bound > s)
1555
- bound
1556
- }
1557
- end
1558
- end
1559
-
1560
- #
1561
- # BYYEARDAYを実装
1562
- # @private
1563
- class Yearday < Logic
1564
- end
1565
-
1566
- #
1567
- # BYMONTHDAYを実装
1568
- # @private
1569
- class Monthday < Logic
1570
- end
1571
-
1572
- #
1573
- # BYWEEKDAY(=BYDAY)を実装
1574
- # @private
1575
- class Weekday < Residue
1576
- # @private
1577
- def initialize(by_part, list)
1578
- @by_part = by_part
1579
- @list = list.split(/,/).map {|w|
1580
- raise ArgumentError, "The BYDAY rule format error" unless w =~ /\A([-+]?\d+)?(MO|TU|WE|TH|FR|SA|SU)([-+]\d+)?\z/
1581
- nth, spec, period = $~[1..3]
1582
- if nth
1583
- nth = nth.to_i
1584
- nth = (nth>0) ? nth-1 : nth
1585
- end
1586
- residue = When::Coordinates::Residue.day_of_week(spec) >> (nth||0)
1587
- [nth, residue, When::TM::PeriodDuration.new([0,0,(period||0).to_i])]
1588
- }
1589
- end
1590
- end
1591
-
1592
- #
1593
- # BYHOURを実装
1594
- # @private
1595
- class Hour < Logic
1596
- # @private
1597
- def initialize(by_part, list, lower=nil, upper=nil, leap=false)
1598
- super(by_part, list)
1599
- if lower
1600
- @list.each do |v|
1601
- raise ArgumentError, "#{by_part} out of range: #{v}" unless lower <= v && v < upper
1602
- end
1603
- end
1604
- end
1605
- end
1606
-
1607
- #
1608
- # BYMINUTEを実装
1609
- # @private
1610
- class Minute < Hour
1611
- end
1612
-
1613
- #
1614
- # BYSECONDを実装
1615
- # @private
1616
- class Second < Logic
1617
- # @private
1618
- def initialize(by_part, list, lower=nil, upper=nil, leap=false)
1619
- super(by_part, list)
1620
- if lower
1621
- @list = @list.map {|v|
1622
- v -= 1 if leap && upper <= v && v < upper+1
1623
- raise ArgumentError, "#{by_part} out of range: #{v}" unless lower <= v && v < upper
1624
- v
1625
- }
1626
- end
1627
- end
1628
- end
1629
- end
1630
- end
1631
- end
1632
- end
1
+ # -*- coding: utf-8 -*-
2
+ =begin
3
+ Copyright (C) 2011-2015 Takashi SUGA
4
+
5
+ You may use and/or modify this file according to the license described in the LICENSE.txt file included in this archive.
6
+ =end
7
+
8
+ #
9
+ # An implementation of RFC 5545 - iCalendar ( http://tools.ietf.org/html/rfc5545 )
10
+ #
11
+ # == Extensions of RFC 5545
12
+ # === Multi Calendar
13
+ # * CALSCALE Property can specify calendars other than GREGORIAN
14
+ # The value of CALSCALE Property is capitalized.
15
+ # * Date and Time Representation of DTSTART Property
16
+ # When.exe Standard Representation is available except ','. (',' cannot be used as a decimal mark)
17
+ # Calendar can be specified by a notation '^^calnedar' or '^calnedar'.
18
+ # (same as epoch of When::TM::CalendarEra)
19
+ # === The extension of RRULE property
20
+ # * FREQ=duration
21
+ # When.exe Standard Representation により duration を指定できる。
22
+ # ただし、この指定は BYxxx とは共存できない。
23
+ # * BYHOUR=h(,..)
24
+ # h に When.exe Standard Representation を使用できる
25
+ # example: 'BYHOUR=1,2'
26
+ # 夏時間から標準時間への切り替え時に 夏時間の1時,標準時間の2時 を生成
27
+ # example: 'BYHOUR=1,1=,2'
28
+ # 夏時間から標準時間への切り替え時に 夏時間の1時,標準時間の1時,標準時間の2時 を生成
29
+ # 他のソフトウェアとの互換性を損なう可能性があるが、本ライブラリは両方の動作の違いを
30
+ # 記述できる必要があると判断。
31
+ # * BYDAY/c=n*e±s(,..)
32
+ # c で指定したWhen::CalendarNoteオブジェクトのイベントeのうち、
33
+ # n番目のものについて、その±s日目
34
+ # デフォルトは n=all, s=0, cとeは省略不可(eはcのメソッドとして定義されている必要がある)
35
+ # example: 'BYDAY/Christian=easter-2' は、キリスト教の聖金曜日
36
+ # example: 'BYDAY/SolarTerms=term180' は、秋分日(太陽黄経が180度になる日)
37
+ # * BYDAY/d=n*m±s(,..);DAYST=b
38
+ # ユリウス日をdで除した余りがmになる日のうち、n番目のものについて、その±s日目
39
+ # m は b を基点として計算する
40
+ # デフォルトは n=all, s=0, b=0, dとmは省略不可
41
+ # /d がない場合、次項のBYWEEKDAYとして扱い、RFC 5545に対する互換性を確保する
42
+ # * BYWEEKDAY=n*m±s(,..);WKST=b
43
+ # ユリウス日を7で除した余りがmになる日のうち、n番目のものについて、その±s日目
44
+ # m は b を基点として計算する(mとbはともに文字列'MO','TU','WE','TH','FR','SA','SU'で指定)
45
+ # デフォルトは n=all, s=0, b=MO, mは省略不可
46
+ # * BYYEAR/d=n*m±s(,..);YEARST=b
47
+ # 通年をdで除した余りがmになる年のうち、n番目のものについて、その±s年目
48
+ # m は b を基点として計算する
49
+ # デフォルトは n=all, s=0, b=4, dとmは省略不可
50
+ # 通年の意味は暦法に依存する
51
+ # === icd 形式の多言語対応
52
+ # * NAMESPACE Property を追加
53
+ # * LOCALE Property を追加
54
+ # * SUMMARY Property で[...]表現を多言語対応文字列として解釈する
55
+ # * RFC6350 ( http://tools.ietf.org/html/rfc6350 ) 方式にも対応
56
+ # === Content Lines
57
+ # RFC 5545 3.1 Content Lines では、記述が75文字を超える場合、改行してインデントする。
58
+ # 本ライブラリでは文字数によらず、BEGIN:の次の行を基準にして、より深いインデントがある場合は、
59
+ # 論理的1行を物理的に分割したものとみなして解釈している。実質的には上位互換と判断している
60
+ #
61
+ # == Limitations from RFC 5545
62
+ # * VALARM
63
+ # VENENT や VTODO に包含されている VALARM は無視される。
64
+ # 包含元のイベントからの差分時刻での計算に対応していないため。
65
+ # * RRULE
66
+ # BYYEARDAY, BYMONTHDAY, BYDAY が存在する場合 BYWEEKDAY の n と s は指定できない。
67
+ # BYYEARDAY, BYMONTHDAY, BYWEEKDAY が存在する場合 BYDAY の n と s は指定できない。
68
+ module When::V
69
+
70
+ # iCalendar を構成するクラス群の共通抽象クラス
71
+ #
72
+ # RFC 5545 のクラスは、有無のチェックを除いてProperty の扱いが共通なので、
73
+ # Property の扱いを、本クラスにまとめて記述している。
74
+ #
75
+ class Root < When::BasicTypes::Object
76
+
77
+ Properties = [[],[],[],[],[]]
78
+ Classes = nil
79
+ DefaultUnique = ['calscale', 'namespace', 'locale']
80
+ DefaultOptional = ['x_prop', 'iana_prop']
81
+ AwareProperties = DefaultUnique +
82
+ ['tzoffsetfrom', 'tzoffsetto', 'tzname',
83
+ 'dtstart', 'dtend', 'due',
84
+ 'repeat', 'duration',
85
+ 'rrule', 'rdate', 'exdate', 'exevent',
86
+ 'summary', 'freebusy']
87
+
88
+ #
89
+ # iCalendar クラス群の属性
90
+ # @return [Hash] { String => When::Parts::Resource::ContentLine }
91
+ #
92
+ attr_reader :property
93
+
94
+ #
95
+ # デフォルトのWhen::TM::Calendar
96
+ # @return [When::TM::Calendar]
97
+ #
98
+ # @note
99
+ # RFC 5545 では、'GREGORIAN' のみ指定可能としている。
100
+ # CALSCALE Property 文字列を capitalize したものに、
101
+ # prefix _c:(=http://hosi.org/When/CalendarTypes/)を補い
102
+ # When::TM::Calendar オブジェクトの定義を取得する。
103
+ #
104
+ attr_reader :calscale
105
+
106
+ # 指定の日時を含むか?
107
+ #
108
+ # @param [When::TM::TemporalPosition] date
109
+ #
110
+ # @return [Boolean]
111
+ # [ true - 含む ]
112
+ # [ false - 含まない ]
113
+ #
114
+ def include?(date)
115
+ first = enum_for(date).next
116
+ return first.include?(date) if first.kind_of?(When::Parts::GeometricComplex)
117
+ return first == date if first.precision <= date.precision
118
+ return false
119
+ end
120
+
121
+ # イテレータの生成
122
+ # @private
123
+ def _enumerator(*args)
124
+ options = When::Parts::Enumerator._options(args)
125
+ args << options
126
+ exdate = options[:exdate]
127
+
128
+ enumerators = _enumerator_list(args)
129
+ raise ArgumentError, "No enumerator exists" if (enumerators.length==0)
130
+
131
+ # Enumerator の生成
132
+ enumerator =
133
+ if (enumerators.length==1 && exdate.node.size==0)
134
+ enumerators[0]
135
+ else
136
+ options[:exdate] = exdate
137
+ When::Parts::Enumerator::Integrated.new(self, enumerators, *args)
138
+ end
139
+ if ::Object.const_defined?(:Date) && ::Date.method_defined?(:+) && (args[0].kind_of?(Range) ? args[0].first : args[0]).kind_of?(::Date)
140
+ enumerator.instance_eval %Q{
141
+ alias :_succ_of_super :succ
142
+ def succ
143
+ result = _succ_of_super
144
+ result.kind_of?(When::TimeValue) ? result.to_date_or_datetime : result
145
+ end
146
+ }
147
+ end
148
+ enumerator
149
+ end
150
+ alias :to_enum :_enumerator
151
+ alias :enum_for :_enumerator
152
+
153
+ private
154
+
155
+ # オブジェクトの生成
156
+ def initialize(*args)
157
+ # option の取得
158
+ options = args[-1].kind_of?(Hash) ? args.pop.dup : {}
159
+
160
+ # 包含関係
161
+ @_pool = {}
162
+ @_pool['..'] = options['..']
163
+
164
+ # parsed 部の属性化
165
+ @property = {}
166
+ @namespace = @_pool['..'].respond_to?(:namespace) ? @_pool['..'].namespace : {}
167
+ if options['.']
168
+ _parse_from_file(options)
169
+ else
170
+ _parse_from_code(options)
171
+ end
172
+ _set_variables
173
+
174
+ # 属性の存在チェック & 設定
175
+ _initialize_attributes(_attribute_appearance(self.class::Properties)) # .const_get(:Properties)))
176
+
177
+ # 包含オブジェクトの生成
178
+ _child(options, self.class::Classes) #.const_get(:Classes))
179
+ end
180
+
181
+ # ファイルからの属性読み込み
182
+ def _parse_from_file(options)
183
+ options['.'].each do |v|
184
+ v = When::Parts::Resource._parse(v)
185
+ _parse_altid(@property, v) if v.kind_of?(ContentLine)
186
+ end
187
+
188
+ keys = @property.keys
189
+ if keys.delete('namespace')
190
+ content = @property['namespace'][0]
191
+ @property['namespace'] = content if @property['namespace'].size == 1
192
+ if content.attribute['prefix']
193
+ begin
194
+ @namespace[content.attribute['prefix'].object] = content.object
195
+ end while (content = content.same_altid)
196
+ else
197
+ @namespace.update(When::Locale._namespace(content.object))
198
+ end
199
+ end
200
+
201
+ keys.each do |key|
202
+ @property[key].each do |content|
203
+ content.object = When::BasicTypes::M17n.new(content, @namespace, []) if content.same_altid
204
+ end
205
+ @property[key] = @property[key][0] if @property[key].size == 1
206
+ end
207
+ end
208
+
209
+ # コードからの属性読み込み
210
+ def _parse_from_code(options)
211
+ options.each_pair do |key, value|
212
+ @property[key] = ContentLine.new(key, value) if key.kind_of?(String)
213
+ end
214
+ @property['dtstamp'] ||= ContentLine.new('dtstamp',
215
+ When.now.to_s.gsub(/[-:]/,'')) if self.class::Properties[0].index('dtstamp')
216
+ @property['uid'] ||= ContentLine.new('uid',
217
+ @property['dtstamp'].object + '-auto') if self.class::Properties[0].index('uid')
218
+ end
219
+
220
+ # @propertyの個別属性化
221
+ def _set_variables
222
+ @property.each_key do |key|
223
+ next if respond_to?(key)
224
+ instance_eval %Q{
225
+ def #{key}
226
+ @property['#{key}'].object
227
+ end
228
+ }
229
+ end
230
+ end
231
+
232
+ # 包含オブジェクトの生成
233
+ def _child(options, classes)
234
+ @child = []
235
+ opt = options.dup
236
+ opt['..'] = self
237
+ if options['.']
238
+ options['.'].each do |v|
239
+ next unless (v.kind_of?(Array) && v[0].kind_of?(Class))
240
+ raise ArgumentError, "The #{self.class} cannot include #{v[0]}" if (classes && !classes.index(v[0]))
241
+ opt['.'] = v
242
+ obj = v[0].new(opt)
243
+ @child << obj
244
+ @_pool[obj.label.to_s] = obj
245
+ end
246
+ else
247
+ options.each_pair do |key, value|
248
+ next unless key.kind_of?(Class)
249
+ obj = key.new(opt.merge(value))
250
+ @child << obj
251
+ @_pool[obj.label.to_s] = obj
252
+ end
253
+ end
254
+ end
255
+
256
+ # attribute の出現数のチェック
257
+ def _attribute_appearance(attributes)
258
+
259
+ require_unique, require, unique, almost_unique, optional = attributes
260
+
261
+ # REQUIRED but MUST NOT occur more than once
262
+ require_unique.each do |key|
263
+ unless @property[key].kind_of?(When::Parts::Resource::ContentLine)
264
+ raise ArgumentError, "The #{key.upcase.gsub(/_/,'-')} is REQUIRED but MUST NOT occur more than once"
265
+ end
266
+ end
267
+
268
+ # REQUIRED and MAY occur more than once
269
+ require.each do |key|
270
+ unless @property[key]
271
+ raise ArgumentError, "The #{key.upcase.gsub(/_/,'-')} is REQUIRED and MAY occur more than once"
272
+ end
273
+ @property[key] = [@property[key]] if (@property[key].kind_of?(When::Parts::Resource::ContentLine))
274
+ end
275
+
276
+ # OPTIONAL but MUST NOT occur more than once
277
+ (unique + DefaultUnique).each do |key|
278
+ if (@property[key].kind_of?(Array))
279
+ raise ArgumentError, "The #{key.upcase.gsub(/_/,'-')} is OPTIONAL but MUST NOT occur more than once"
280
+ end
281
+ end
282
+
283
+ # OPTIONAL but SHOULD NOT occur more than once
284
+ almost_unique.each do |key|
285
+ if (@property[key].kind_of?(Array))
286
+ raise ArgumentError, "The #{key.upcase.gsub(/_/,'-')} is OPTIONAL but SHOULD NOT occur more than once"
287
+ end
288
+ end
289
+
290
+ # OPTIONAL and MAY occur more than once
291
+ (optional + DefaultOptional).each do |key|
292
+ @property[key] = [@property[key]] if (@property[key].kind_of?(When::Parts::Resource::ContentLine))
293
+ end
294
+
295
+ # Other Properties
296
+ allowed_attributes = require_unique + require + unique + almost_unique + optional + DefaultUnique + DefaultOptional
297
+ #@property.each_key do |key|
298
+ # raise ArgumentError, "The #{key.upcase.gsub(/_/,'-')} is not allowed" unless allowed_attributes.index(key)
299
+ #end
300
+
301
+ return (AwareProperties & allowed_attributes)
302
+ end
303
+
304
+ # attribute の設定
305
+ def _initialize_attributes(aware)
306
+
307
+ # calscale の登録
308
+ @calscale = @_pool['..'].calscale if @_pool['..'].respond_to?(:calscale)
309
+ @calscale = @property['calscale'].object if @property['calscale']
310
+ @calscale = When.Resource((@calscale||'GREGORIAN').capitalize, '_c:') unless (@calscale.kind_of?(When::TM::Calendar))
311
+
312
+ # locale の登録
313
+ @locale = @_pool['..'].respond_to?(:locale) ? @_pool['..'].locale : []
314
+ @locale = When::Locale._locale(@property['locale'].object) if @property['locale']
315
+
316
+ # tzoffsetfrom の登録
317
+ if @property['tzoffsetfrom']
318
+ object = @property['tzoffsetfrom'].object
319
+ new_zone = object.to_s # See RFC5545 page.66
320
+ @tzoffsetfrom = When.Clock(object).dup
321
+ @tzoffsetfrom.tz_prop = self
322
+ else
323
+ new_zone = ''
324
+ end
325
+
326
+ # tzoffsetto, tzname の登録
327
+ if @property['tzoffsetto']
328
+ clock = When.Clock(@property['tzoffsetto'].object).dup
329
+ clock.tz_prop = self
330
+ @tzname = []
331
+ (@property['tzname']||[]).each do |tzname|
332
+ tz = tzname.object # ↓このチェックは不十分?
333
+ When::TM::Clock.synchronize do
334
+ raise ArgumentError, "Conflict tzname: #{tz}" if When::TM::Clock[tz] &&
335
+ When::TM::Clock[tz].universal_time != clock.universal_time
336
+ When::TM::Clock[tz] = clock
337
+ end
338
+ @tzname << tz
339
+ end
340
+ @tzoffsetto = clock
341
+ @tzname = @tzname[0] if (@tzname.length <= 1)
342
+ end
343
+
344
+ # due の登録
345
+ date_options = {:frame=>@calscale}
346
+ if @property['due']
347
+ @due = When.when?(@property['due'].attribute['.'] ? @property['due'].attribute['.'] + new_zone :
348
+ @property['due'].object, date_options)
349
+ end
350
+
351
+ # dtstart の登録
352
+ if @property['dtstart']
353
+ @dtstart = When.when?(@property['dtstart'].attribute['.'] ? @property['dtstart'].attribute['.'] + new_zone :
354
+ @property['dtstart'].object, date_options)
355
+ @dtstart.clk_time.frame = @tzoffsetfrom unless (new_zone == '') # See RFC5545 page.66
356
+
357
+ @first_occurrence = "Include"
358
+ else
359
+ @dtstart = When.now(@due||{})
360
+ @first_occurrence = "Don't care"
361
+ end
362
+
363
+ # dtend の登録
364
+ if @property['dtend']
365
+ @dtend = When.when?(@property['dtend'].attribute['.'] ? @property['dtend'].attribute['.'] + new_zone :
366
+ @property['dtend'].object, date_options)
367
+ end
368
+
369
+ # repeat の登録
370
+ if aware.index('repeat')
371
+ raise ArgumentError, "The DURATION MUST occur" if ( @property['repeat'] && !@property['duration'])
372
+ raise ArgumentError, "The REPEAT MUST occur" if (!@property['repeat'] && @property['duration'])
373
+ if @property['repeat']
374
+ @repeat = @property['repeat'].object.to_i
375
+ end
376
+ end
377
+
378
+ # duration の登録
379
+ if aware.index('duration')
380
+ if @property['duration']
381
+ raise ArgumentError, "The DURATION should appear with DTSTART" unless (@dtstart||@repeat)
382
+ raise ArgumentError, "The DTEND and DURATION are exclusive" if (@dtend)
383
+ raise ArgumentError, "The DUE and DURATION are exclusive" if (@due)
384
+ duration = @property['duration'].object
385
+ duration = When.Duration(duration) unless duration.kind_of?(Numeric)
386
+ elsif @dtend
387
+ duration = When::TM::IntervalLength.difference(@dtend, @dtstart)
388
+ end
389
+ duration_precision = When::Coordinates::PERIOD[duration.to_s]
390
+ @duration = duration unless (duration_precision && @dtstart.precision <= duration_precision)
391
+ end
392
+
393
+ # rrule の登録
394
+ if aware.index('rrule')
395
+ @rrule = []
396
+ @rrule = @property['rrule'].map {|v| Event::Enumerator._decode_rule(v.object, @dtstart, new_zone)} if @property['rrule']
397
+ end
398
+
399
+ # rdate の登録
400
+ if aware.index('rdate')
401
+ @rdate = []
402
+ @exevent = []
403
+ @exdate = When::Parts::GeometricComplex.new()
404
+ if (@property['rdate'])
405
+ @rdate = @property['rdate'].inject([]) do |sum, v|
406
+ if v.kind_of?(When::Parts::Resource::ContentLine)
407
+ if new_zone == ''
408
+ sum += When.when?(v.attribute['.'].split(/,/), date_options)
409
+ else
410
+ sum += When.when?((v.attribute['.'].split(/,/).map {|d| d + new_zone}), date_options)
411
+ end
412
+ else
413
+ sum << v
414
+ end
415
+ end
416
+ end
417
+ unless new_zone == ''
418
+ @rdate.each do |date|
419
+ date.clk_time.frame = @tzoffsetto
420
+ end
421
+ end
422
+ end
423
+
424
+ # exdate の登録
425
+ if @property['exdate']
426
+ dates = @property['exdate'].inject([]) do |sum, v|
427
+ if v.kind_of?(When::Parts::Resource::ContentLine)
428
+ sum += When.when?(v.attribute['.'].split(/,/), date_options)
429
+ else
430
+ sum << v
431
+ end
432
+ end
433
+ dates.each do |date|
434
+ @exdate |= date
435
+ end
436
+ end
437
+
438
+ # exevent の登録
439
+ if @property['exevent']
440
+ @exevent += @property['exevent'].object.split(/,/)
441
+ end
442
+
443
+ # summary の登録
444
+ term_options = {'namespace'=>@namespace, 'locale'=>@locale}
445
+ if @property['summary']
446
+ text = @property['summary'].object
447
+ @summary = (text =~ /\A\[/) ? m17n(text, nil, nil, term_options) : text
448
+ end
449
+
450
+ # freebusy の登録
451
+ if aware.index('freebusy')
452
+ @freebusy = []
453
+ if (@property['freebusy'])
454
+ @freebusy = @property['freebusy'].inject([]) do |sum, v|
455
+ if v.kind_of?(When::Parts::Resource::ContentLine)
456
+ sum += When.when?((v.attribute['.']||v.object).split(/,/), date_options)
457
+ else
458
+ sum << v
459
+ end
460
+ end
461
+ end
462
+ @rdate = @freebusy
463
+ @rrule = []
464
+ end
465
+ end
466
+ end
467
+
468
+ # ひとつの ics 形式ファイルをまとめて保持する
469
+ #
470
+ # BEGIN:VCALENADR...END:VCALENDAR のブロックに対応
471
+ #
472
+ class Calendar < Root
473
+
474
+ Properties= [['prodid', 'version'], [],
475
+ ['method'], [], []]
476
+ Classes = nil
477
+
478
+ attr_accessor :child
479
+ protected :child=
480
+
481
+ # When::V::Event の検索
482
+ #
483
+ # @param [Hash] keys { key => value }
484
+ #
485
+ # @return [When::V::Calendar] key で指定する Property の値が value に一致する When::V::Event のみを持つ self の複製を作る
486
+ # (一致の判断は演算子 === による)
487
+ #
488
+ def intersection(keys={})
489
+ copy = self.dup
490
+ copy.child = @child.select {|ev|
491
+ if ev.kind_of?(Event)
492
+ keys.each_pair do |key, value|
493
+ case value
494
+ when String ; break unless ev.property[key].object.index(value)
495
+ when Regexp ; break unless (ev.property[key].object =~ value)
496
+ end
497
+ end
498
+ else
499
+ true
500
+ end
501
+ }
502
+ return copy
503
+ end
504
+ alias :subset :intersection
505
+
506
+ # @private
507
+ def _enumerator_list(args)
508
+ (@child.reject {|el| !el.kind_of?(Event)}).inject([]) { |sum, ev|
509
+ sum += ev._enumerator_list(args)
510
+ }
511
+ end
512
+ end
513
+
514
+ # Eventを定義する
515
+ #
516
+ # BEGIN:VEVENT...END:VEVENT のブロックに対応
517
+ #
518
+ class Event < Root
519
+
520
+ Properties= [['dtstamp', 'uid', 'dtstart'], [],
521
+ ['class', 'created', 'description', 'geo',
522
+ 'last_modified', 'location', 'organizer', 'priority',
523
+ 'seq', 'status', 'summary', 'transp',
524
+ 'url', 'recurid', 'dtend', 'duration'], [],
525
+ ['rrule',
526
+ 'attach', 'attendee', 'categories', 'comment', 'contact',
527
+ 'exdate', 'exevent', 'rstatus', 'related',
528
+ 'resources', 'rdate']]
529
+ # Classes = [V::Root, V::Alarm]
530
+
531
+ class << self
532
+ include When::Parts::Resource::Pool
533
+
534
+ # When::V::Event Class のグローバルな設定を行う
535
+ #
536
+ # @param [When::TM::IntervalLength] default_until
537
+ #
538
+ # @note
539
+ # RRULE の条件が成立しない場合に無限ループにおちいることを避けるため
540
+ # 他に指定がなくとも、計算を打ち切るようにしている。その打ち切り時間
541
+ # (When.now + default_until)を本メソッドで指定している。
542
+ # default_until の指定がない場合、default_until は 1000年と解釈する。
543
+ #
544
+ # @note
545
+ # 本メソッドでマルチスレッド対応の管理変数の初期化を行っている。
546
+ # このため、本メソッド自体はスレッドセーフでない。
547
+ #
548
+ def _setup_(default_until=nil)
549
+ @_lock_ = Mutex.new if When.multi_thread
550
+ @_pool = {}
551
+ @default_until = default_until
552
+ end
553
+
554
+ # 設定情報を取得する
555
+ #
556
+ # @return [Hash] 設定情報
557
+ #
558
+ def _setup_info
559
+ {:until => default_until}
560
+ end
561
+
562
+ # 最大打ち切り時間
563
+ #
564
+ # @return [When::TM::IntervalLength]
565
+ def default_until
566
+ @default_until ||= 1000*When::TM::Duration::YEAR
567
+ end
568
+ end
569
+
570
+ # SUMMARY Property
571
+ #
572
+ # @return [String, When::BasicTypes::M17n]
573
+ #
574
+ attr_reader :summary
575
+
576
+ # RRULE Property
577
+ #
578
+ # @return [Hash]
579
+ #
580
+ # @note
581
+ # iCalendar の RRULE を Hash に展開したものを保持している。
582
+ # RRULE は、年のサイクルや7日以外の日のサイクルおよび夏時間の切り替えを
583
+ # 扱えるように RFC 5545 から拡張されている。
584
+ #
585
+ attr_reader :rrule
586
+
587
+ # DTSTART Property
588
+ #
589
+ # @return [When::TM::TemporalPosition, When::Parts::GeometricComplex]
590
+ #
591
+ attr_reader :dtstart
592
+
593
+ # DTEND Property
594
+ #
595
+ # @return [When::TM::TemporalPosition, When::Parts::GeometricComplex]
596
+ #
597
+ attr_reader :dtend
598
+
599
+ # DURATION Property
600
+ #
601
+ # @return [When::TM::Duration]
602
+ #
603
+ # @note
604
+ # DTSTART Property が保持する When::TM::TemporalPosition の分解能で識別できない
605
+ # 時間差はイベント継続中とみなすので、例えば分解能が DAY の場合、DURATION Porperty
606
+ # に When.Duration('P1D')と指定する必要はない。
607
+ # DTEND Property が指定された場合、DURATION Property に変換して保持する。
608
+ #
609
+ attr_reader :duration
610
+
611
+ # EXDATE Property
612
+ #
613
+ # @return [When::Parts::GeometricComplex]
614
+ #
615
+ attr_reader :exdate
616
+
617
+ # RDATE Property
618
+ #
619
+ # @return [Array<When::TM::TemporalPosition, When::Parts::GeometricComplex>]
620
+ #
621
+ # @note
622
+ # RRULE の COUNT が指定されている場合、後で途中の系列を抜き出すような指定をされても
623
+ # よいように、Enumerator 生成時にCOUNT分の計算をして RDATE Property に登録する。
624
+ # このため、COUNTに大きな値を指定すると、Enumerator 生成に予想外の時間がかかることが
625
+ # ある。
626
+ #
627
+ attr_reader :rdate
628
+
629
+ # DTSTART Property を first occurrence とするか
630
+ #
631
+ # @return [String]
632
+ # [ 'Include' - first occurrence とする ]
633
+ # [ 'Exclude' - first occurrence しない ]
634
+ # [ それ以外 - RRULE により該当する場合に first occurrence とする ]
635
+ #
636
+ # @note
637
+ # RFC 5545 では 'Include' となっているが、それ以外の振る舞いが可能なように拡張。
638
+ #
639
+ attr_reader :first_occurrence # See RFC 5545 [Page 41] 3rd paragraph
640
+
641
+ # ユニーク識別名 - UID Property をユニーク識別名とする。
642
+ #
643
+ # @return [String]
644
+ #
645
+ def label
646
+ @label ||= @property['uid'].object
647
+ end
648
+
649
+ # 最後のイベント
650
+ #
651
+ # @return [When::TM::TemporalPosition, When::Parts::GeometricComplex]
652
+ #
653
+ # @note
654
+ # 無限に続く可能性がある場合、When::TimeValue::Max(+Infinity)
655
+ #
656
+ def dtstop
657
+ return @dtstop if (@dtstop)
658
+
659
+ @dtstop = @dtstart
660
+ @rdate.each do |date|
661
+ @dtstop = date if (date > @dtstop)
662
+ end
663
+ @rrule.each do |rrule|
664
+ unless (rrule['UNTIL'])
665
+ @dtstop = When::TimeValue::Max
666
+ break
667
+ end
668
+ @dtstop = rrule['UNTIL'] if (rrule['UNTIL'] > @dtstop)
669
+ end
670
+ return @dtstop
671
+ end
672
+
673
+ # 順次実行
674
+ #
675
+ # @overload each()
676
+ #
677
+ # @overload each(range, count_limit=nil)
678
+ # @param [Range, When::Parts::GeometricComplex] range 始点-range.first, 終点-range.last
679
+ # @param [Integer] count_limit 繰り返し回数(デフォルトは指定なし)
680
+ #
681
+ # @overload each(first, direction, count_limit)
682
+ # @param [When::TM::TemporalPosition] first 始点
683
+ # @param [Symbol] direction :forward - 昇順, :reverse - 降順 (options[:direction]で渡してもよい)
684
+ # @param [Integer] count_limit 繰り返し回数(デフォルトは指定なし, options[:count_limit]で渡してもよい)
685
+ #
686
+ # @return [Enumerator]
687
+ #
688
+ # @note block が与えられている場合、yield する。
689
+ #
690
+ def each(*args, &block)
691
+ if args.length > 0
692
+ super
693
+ else
694
+ super(@dtstart, &block)
695
+ end
696
+ end
697
+
698
+ # @private
699
+ def _enumerator_list(args)
700
+ options = args[-1]
701
+ rdate = @rdate.dup
702
+ options[:exdate] = @exdate.dup if @exdate
703
+ options[:exevent] = @exevent.map {|v| self[v]} if @exevent
704
+ case (options['1st'] || @first_occurrence).capitalize
705
+ when 'Include' ; rdate.unshift(@duration ? When::Parts::GeometricComplex.new(@dtstart, @duration) : @dtstart)
706
+ when 'Exclude' ; options[:exdate] |= @dtstart
707
+ end
708
+
709
+ # 配下の Enumerator の初期化
710
+ enumerators = []
711
+ @rrule.each do |rrule|
712
+ if @due && !(rrule['UNTIL'] && rrule['UNTIL'] <= @due)
713
+ rrule = rrule.dup
714
+ rrule['UNTIL'] = @due
715
+ end
716
+ if rrule['COUNT']
717
+ Event::Enumerator.new(self, rrule, @dtstart, @duration, @dtstart, options).each {|date| rdate << date }
718
+ else
719
+ enumerators << Event::Enumerator.new(self, rrule, @dtstart, @duration, *args)
720
+ end
721
+ end
722
+
723
+ enumerators.unshift(When::Parts::Enumerator::Array.new(self, rdate, *args[1..-1])) if (rdate.length > 0)
724
+
725
+ return enumerators
726
+ end
727
+ end
728
+
729
+ # Alarm を定義する
730
+ #
731
+ # BEGIN:VALARM...END:VALARM のブロックに対応
732
+ #
733
+ class Alarm < Event
734
+
735
+ # ユニーク識別名 - ACTION Property をユニーク識別名とする。
736
+ # @return [String]
737
+ def label
738
+ @label ||= @property['action'].object # TODO
739
+ end
740
+
741
+ # @private
742
+ def initialize(options)
743
+ # 包含関係
744
+ @_pool = {}
745
+ @_pool['..'] = options['..']
746
+
747
+ # parsed 部の属性化
748
+ _parsed(options)
749
+
750
+ # 属性の存在チェック
751
+ case (@property['action'].kind_of?(When::Parts::Resource::ContentLine) && @property['action'].object)
752
+ when 'AUDIO'
753
+ aware = _attribute_appearance([
754
+ ['action', 'trigger'], [],
755
+ ['duration', 'repeat', 'attach'], [], []])
756
+ when 'DISPLAY'
757
+ aware = _attribute_appearance([
758
+ ['action', 'description', 'trigger'], [],
759
+ ['duration', 'repeat'], [], []])
760
+ when 'EMAIL'
761
+ aware = _attribute_appearance([
762
+ ['action', 'description', 'trigger', 'summary'], ['attendee'],
763
+ ['duration', 'repeat'], [], ['attach']])
764
+ else
765
+ raise ArgumentError, "The ACTION is invalid"
766
+ end
767
+
768
+ # 属性の設定
769
+ _initialize_attributes(aware)
770
+
771
+ # 包含オブジェクトがないことの確認
772
+ _child(options, [])
773
+ end
774
+ end
775
+
776
+ # Todo を定義する
777
+ #
778
+ # BEGIN:VTODO...END:VTODO のブロックに対応
779
+ #
780
+ class Todo < Event
781
+ Properties= [['dtstamp', 'uid'], [],
782
+ ['class', 'completed', 'created', 'description',
783
+ 'dtstart', 'geo', 'last_modified', 'location', 'organizer',
784
+ 'percent', 'priority', 'recurid', 'seq', 'status',
785
+ 'summary', 'url'], ['due', 'duration'],
786
+ ['rrule',
787
+ 'attach', 'attendee', 'categories', 'comment', 'contact',
788
+ 'exdate', 'exevent', 'rstatus', 'related', 'resources',
789
+ 'rdate']]
790
+ Classes = [Alarm]
791
+ end
792
+
793
+ # Journal を定義する
794
+ #
795
+ # BEGIN:VJOURNAL...END:VJOURNAL のブロックに対応
796
+ #
797
+ class Journal < Event
798
+
799
+ Properties= [['dtstamp', 'uid'], [],
800
+ ['class', 'created', 'dtstart',
801
+ 'last_modified', 'organizer', 'recurid', 'seq',
802
+ 'status', 'summary', 'url'], [],
803
+ ['rrule',
804
+ 'attach', 'attendee', 'categories', 'comment', 'contact',
805
+ 'description', 'exdate', 'exevent', 'related', 'rdate',
806
+ 'rstatus']]
807
+ Classes = []
808
+ end
809
+
810
+ # Freebusy を定義する
811
+ #
812
+ # BEGIN:VFREEBUSY...END:VFREEBUSY のブロックに対応
813
+ #
814
+ class Freebusy < Event
815
+
816
+ Properties= [['dtstamp', 'uid'], [],
817
+ ['contact', 'dtstart', 'dtend',
818
+ 'organizer', 'url'], [],
819
+ ['attendee', 'comment', 'freebusy', 'rstatus']]
820
+ Classes = []
821
+
822
+ attr_reader :freebusy
823
+ end
824
+
825
+ #
826
+ # Timezone Property の共通抽象クラス
827
+ #
828
+ class TimezoneProperty < Event
829
+
830
+ Properties= [['dtstart', 'tzoffsetto', 'tzoffsetfrom'], [],
831
+ [], [], ['rrule',
832
+ 'comment', 'rdate', 'tzname']]
833
+ Classes = []
834
+
835
+ # TZNAME Property
836
+ #
837
+ # @return [Array<String(サブクラスであるWhen::BasicTypes::M17nでもよい)>]
838
+ #
839
+ # @note
840
+ # RFC 5545 は複数個の指定を許しているため、Array としている。
841
+ #
842
+ attr_reader :tzname
843
+
844
+ # TZOFFSETFROM Property
845
+ #
846
+ # @return [When::TM::Clock]
847
+ #
848
+ # @note
849
+ # イベント時刻前の時間帯
850
+ # 保持する When::TM::Clock の tz_prop は、本オブジェクトを参照している。
851
+ #
852
+ attr_reader :tzoffsetfrom
853
+
854
+ # TZOFFSETTO Property
855
+ #
856
+ # @return [When::TM::Clock]
857
+ #
858
+ # @note
859
+ # イベント時刻後の時間帯
860
+ # 保持する When::TM::Clock の tz_prop は、本オブジェクトを参照している。
861
+ #
862
+ attr_reader :tzoffsetto
863
+
864
+ # ユニーク識別名 - DTSTART Property をユニーク識別名とする
865
+ # @return [String]
866
+ def label
867
+ @label ||= @property['dtstart'].attribute['.']
868
+ end
869
+ end
870
+
871
+ # 標準時間帯を定義する
872
+ #
873
+ # BEGIN:STANDARD...END:STANDARD のブロックに対応
874
+ #
875
+ class Standard < TimezoneProperty
876
+ end
877
+
878
+ # 夏時間帯を定義する
879
+ #
880
+ # BEGIN:DAYLIGHT...END:DAYLIGHT のブロックに対応
881
+ #
882
+ class Daylight < TimezoneProperty
883
+ end
884
+
885
+ # Timezone を定義する
886
+ #
887
+ # BEGIN:VTIMEZONE...END:VTIMEZONE のブロックに対応
888
+ #
889
+ class Timezone < Root
890
+
891
+ Properties= [['tzid'], [],
892
+ ['last_modified', 'tzurl', 'x_lic_location'], [], []]
893
+ Classes = [Standard, Daylight]
894
+
895
+ extend When::Parts::Resource::Pool
896
+
897
+ include When::Parts::Timezone::Base
898
+
899
+ # ユニーク識別名 - TZID Property をユニーク識別名とする
900
+ # @return [String]
901
+ def label
902
+ @label ||= @property['tzid'].object
903
+ end
904
+
905
+ # 同一の時間帯を用いた期間
906
+ #
907
+ # @param [When::TM::TemporalPositionに変換可能な型] current_date 期間に含まれる日時
908
+ #
909
+ # @return [Range]
910
+ # [ 期間が過去または未来に対して無限に続く場合は、GeometricComplex ]
911
+ # [ 期間が有界の場合は、start...end という形式の Range ]
912
+ #
913
+ def current_period(current_date=Time.now)
914
+ current_date = When.when?(current_date) unless current_date.kind_of?(When::TM::TemporalPosition)
915
+ period = _tz_period(current_date.universal_time)
916
+ range = period[1]
917
+ return range if range.kind_of?(Range)
918
+ GeometricComplex.new([period], !range)
919
+ end
920
+
921
+ # @private
922
+ def _daylight(time)
923
+ raise ArgumentError, "Needless daylight saving time evaluation" unless _need_validate
924
+ frame, cal_date, clk_time = time
925
+ time = frame.to_universal_time(cal_date, clk_time, @standard) if clk_time
926
+ ndate = _neighbor_event_date(time)
927
+ nprop = ndate.clock.tz_prop
928
+ time = frame.to_universal_time(cal_date, clk_time, nprop.tzoffsetfrom) if clk_time && !@standard.equal?(nprop.tzoffsetfrom)
929
+ (time >= ndate.universal_time) ? nprop.tzoffsetto : nprop.tzoffsetfrom
930
+ end
931
+
932
+ # @private
933
+ def _need_validate
934
+ @tz_difference != 0
935
+ end
936
+
937
+ private
938
+
939
+ # オブジェクトの生成
940
+ def initialize(options)
941
+ super
942
+
943
+ @child.each do |prop|
944
+ @dtstart = prop.dtstart if (@dtstart==nil || prop.dtstart < @dtstart)
945
+ @dtstop = prop.dtstop unless (prop.dtstop.kind_of?(When::TimeValue) &&
946
+ @dtstop.kind_of?(When::TimeValue) &&
947
+ prop.dtstop <= @dtstop)
948
+ [prop.tzoffsetfrom, prop.tzoffsetto].each do |tz|
949
+ @daylight = tz if (@daylight==nil || tz.universal_time < @daylight.universal_time)
950
+ @standard = tz if (@standard==nil || tz.universal_time > @standard.universal_time)
951
+ end
952
+ end
953
+ @tz_difference = @standard.universal_time - @daylight.universal_time
954
+
955
+ name = @property['tzid'].object
956
+ case self.class[name]
957
+ when nil ; self.class[name] = self
958
+ when Array ; self.class[name] << self
959
+ else ; self.class[name] = [self.class[name], self]
960
+ end
961
+
962
+ @_lock_ = Mutex.new if When.multi_thread
963
+ @range = []
964
+ end
965
+
966
+ # 指定の日時に最も近い、時間帯変更イベントの日時
967
+ #
968
+ # @param [Numeric] current_time 捜索の基点の日時の universal time
969
+ #
970
+ # @return [When::TM::TemporalPosition] 捜索の基点の日時に最も近い、時間帯変更イベントの日時
971
+ #
972
+ def _neighbor_event_date(current_time)
973
+ _tz_period(current_time)[0]
974
+ end
975
+
976
+ def _tz_period(current_time)
977
+ return [@dtstart, false] if (current_time <= @dtstart.universal_time)
978
+ return [@dtstop, true ] if (@dtstop.kind_of?(When::TimeValue) &&
979
+ current_time >= @dtstop.universal_time)
980
+
981
+ # Thread 要注意 - @range は生成後に更新
982
+ synchronize do
983
+ @range.each do |range|
984
+ from = current_time - range.first.universal_time
985
+ to = range.last.universal_time - current_time
986
+ return [(from < to) ? range.first : range.last, range] if (from >= 0 && to > 0)
987
+ end
988
+ early = _neighbor(current_time, :reverse)
989
+ late = _neighbor(current_time, :forward)
990
+ from = current_time - early.universal_time
991
+ to = late.universal_time - current_time
992
+ @range << (early...late)
993
+ return [(from < to) ? early : late, @range[-1]]
994
+ end
995
+ end
996
+
997
+ def _neighbor(current_time, direction)
998
+ event = nil
999
+ minimum = nil
1000
+ @child.each do |prop|
1001
+ date = prop.enum_for(current_time, direction, 1).succ
1002
+ if (date)
1003
+ diff = (date.universal_time - current_time).abs
1004
+ if (minimum == nil || minimum > diff)
1005
+ event = date
1006
+ minimum = diff
1007
+ end
1008
+ end
1009
+ end
1010
+ return event
1011
+ end
1012
+ end
1013
+
1014
+ class Event < Root
1015
+
1016
+ Classes = [Root, Alarm]
1017
+
1018
+ #
1019
+ # When::V::Event が使用する Enumerator
1020
+ #
1021
+ class Enumerator < When::Parts::Enumerator
1022
+
1023
+ include When
1024
+ include Coordinates
1025
+
1026
+ FreqIndex = {'YEARLY' =>YEAR, 'MONTHLY' =>MONTH,
1027
+ 'WEEKLY' =>WEEK, 'DAILY' =>DAY,
1028
+ 'HOURLY' =>HOUR, 'MINUTELY' =>MINUTE, 'SECONDLY' =>SECOND}
1029
+ PostFreqIndex = {'BYYEAR' =>YEAR, 'BYMONTH' =>MONTH, 'BYWEEKNO' =>WEEK,
1030
+ 'BYYEARDAY'=>DAY, 'BYMONTHDAY'=>DAY,
1031
+ 'BYWEEKDAY'=>DAY, 'BYDAY' =>DAY,
1032
+ 'BYHOUR' =>HOUR, 'BYMINUTE' =>MINUTE, 'BYSECOND' =>SECOND}
1033
+
1034
+ # RRULE Property - iCalendar の RRULE を Hash に展開したものを保持している
1035
+ # @return [Hash]
1036
+ attr_reader :rule
1037
+
1038
+ # 繰り返しの始点
1039
+ # @return [When::TM::TemporalPosition, When::Parts::GeometricComplex]
1040
+ attr_reader :dtstart
1041
+
1042
+ # 多重繰り返しロジック
1043
+ # @return [Array<When::V::Event::Enumerator::Logic>]
1044
+ attr_reader :logics
1045
+
1046
+ # 多重繰り返しの現在状態
1047
+ # @return [Array<When::V::Event::Enumerator::Step>]
1048
+ attr_accessor :steps
1049
+ protected :steps=
1050
+
1051
+ #
1052
+ # 巻き戻す
1053
+ #
1054
+ # @return [rewind された self]
1055
+ #
1056
+ def _rewind
1057
+ @steps = [Step.new(_first_seed(@first, @dtstart))]
1058
+ super
1059
+ end
1060
+
1061
+ private
1062
+
1063
+ # オブジェクトの生成
1064
+ def initialize(*args)
1065
+ @options = When::Parts::Enumerator._options(args)
1066
+ @exdate = @options.delete(:exdate)
1067
+ @exevent = @options.delete(:exevent)
1068
+ @parent, @rule, @dtstart, @duration, *rest = args
1069
+ @dtstart = When.when?(@dtstart)
1070
+ @rule = self.class._decode_rule(@rule, @dtstart) if (@rule.kind_of?(String))
1071
+ @logics = @rule[:logics]
1072
+ @tz_prop = nil
1073
+ if (@dtstart.kind_of?(When::TM::DateAndTime))
1074
+ clock = @dtstart.clock
1075
+ if (clock.kind_of?(When::TM::Clock) &&
1076
+ clock.tz_prop.kind_of?(TimezoneProperty) &&
1077
+ (clock.tz_prop.dtstart == @dtstart))
1078
+ @tz_prop = @dtstart.clock.tz_prop
1079
+ @dtstart = @dtstart.dup._copy({:tz_prop=>nil, :validate=>:done})
1080
+ end
1081
+ end
1082
+ _range(rest)
1083
+ _rewind
1084
+ end
1085
+
1086
+ def _copy
1087
+ copy = super
1088
+ copy.steps = @steps.dup
1089
+ return copy
1090
+ end
1091
+
1092
+ def _first_seed(target, dtstart)
1093
+ if @rule['FREQ'].kind_of?(When::TM::Duration)
1094
+ @interval = @rule['FREQ']
1095
+ @interval *= @rule['INTERVAL'] unless @rule['INTERVAL'] == 1
1096
+ else
1097
+ @interval = When::TM::PeriodDuration.new(@rule['INTERVAL'], FreqIndex[@rule['FREQ']])
1098
+ end
1099
+ return dtstart if dtstart == target
1100
+ interval_time = (dtstart + @interval) - dtstart
1101
+ return dtstart if interval_time == 0
1102
+ duration = target.kind_of?(Numeric) ? target - dtstart.universal_time : (target - dtstart).duration
1103
+ div, mod = duration.divmod(interval_time.duration)
1104
+ seed = dtstart + (@interval * div)
1105
+ case @direction
1106
+ when :reverse ; seed += @interval while (seed <= target)
1107
+ else ; seed -= @interval while (seed >= target)
1108
+ end
1109
+ return seed
1110
+ end
1111
+
1112
+ def _next_seed(seed)
1113
+ case @direction
1114
+ when :reverse ; return seed - @interval
1115
+ else ; return seed + @interval
1116
+ end
1117
+ end
1118
+
1119
+ def _succ(depth=0)
1120
+ return nil if @rule['COUNT'] && (@count >= @rule['COUNT'])
1121
+ step = @steps[depth]
1122
+ unless step
1123
+ step = Step.new(_candidates(depth-1))
1124
+ @steps[depth..-1] = [step]
1125
+ end
1126
+
1127
+ case depth
1128
+ when 0
1129
+ if @logics.length == 0
1130
+ result = @steps[0]._current_date
1131
+ @steps[0..-1] = [Step.new(_next_seed(result))]
1132
+ else
1133
+ loop do
1134
+ result = _succ(depth+1)
1135
+ break if result
1136
+ @steps[0..-1] = [Step.new(_next_seed(@steps[0]._current_date))]
1137
+ end
1138
+ end
1139
+ return (@direction==:reverse) ? nil : :next if @dtstart > result
1140
+ return (@direction==:reverse) ? :next : nil if @rule['UNTIL'] && (result > @rule['UNTIL'])
1141
+ if @tz_prop
1142
+ result = result.dup._copy({:tz_prop=>nil, :validate=>:done})
1143
+ result.clock.tz_prop = @tz_prop
1144
+ end
1145
+ return result
1146
+
1147
+ when logics.length
1148
+ step._inc
1149
+ return step._previuos_date
1150
+
1151
+ else
1152
+ while step._current_date do
1153
+ result = _succ(depth+1)
1154
+ return result if result
1155
+ @steps[depth..-1] = [step._inc]
1156
+ end
1157
+ return nil
1158
+ end
1159
+ end
1160
+
1161
+ def _candidates(depth)
1162
+ # logics と seed
1163
+ logics = @logics[depth]
1164
+ seed = @steps[depth]._current_date
1165
+ logics.cash ||= {}
1166
+ return logics.cash[seed.universal_time] if logics.cash[seed.universal_time]
1167
+
1168
+ # 上下限の決定
1169
+ lower_bound, higher_bound = logics._bound(seed, @rule['WKST'])
1170
+
1171
+ # 候補
1172
+ list = logics._candidates(lower_bound, higher_bound).reject { |date|
1173
+ # See RFC 5545 [Page 43] invalid date (e.g., February 30)
1174
+ !(lower_bound.universal_time <= date.universal_time && date.universal_time < higher_bound.universal_time)
1175
+ }
1176
+ list = Array._sort(list, @direction)
1177
+ list = Array._sort((@rule['BYSETPOS'].map {|pos| list[pos]}).compact, @direction) if @rule['BYSETPOS']
1178
+
1179
+ # 結果をCash
1180
+ logics.cash[seed.universal_time] = list
1181
+ end
1182
+
1183
+ # @private
1184
+ def self._decode_rule(description, dtstart, new_zone='')
1185
+ # 指定の読み込み
1186
+ if description.kind_of?(Hash)
1187
+ rule = description
1188
+ else
1189
+ rule = {}
1190
+ description.split(/;/).each do |pair|
1191
+ raise ArgumentError, "Invalid RRULE format" unless pair =~ /\A([^\/=]+)(\/(.+))?=(.*)\z/
1192
+ k, f, c, v = $~[1..4]
1193
+ case k
1194
+ when 'BYYEAR', 'BYDAY', 'YEARPOS', 'DAYPOS', 'YEARST', 'DAYST'
1195
+ rule[k] ||= {}
1196
+ c ||= ""
1197
+ raise ArgumentError, "The #{k}#{f} part MUST NOT occur more than once" if (rule[k][c])
1198
+ rule[k][c] = v
1199
+ else
1200
+ raise ArgumentError, "Invalid RRULE format" if (f)
1201
+ raise ArgumentError, "The #{k} part MUST NOT occur more than once" if (rule[k])
1202
+ rule[k] = v
1203
+ end
1204
+ end
1205
+ end
1206
+
1207
+ # 整合性確認と省略された指定の設定
1208
+ raise ArgumentError, "The UNTIL and COUNT are exclusive" if (rule['UNTIL'] && rule['COUNT'])
1209
+ rule['FREQ'] = When.Duration(rule['FREQ']) unless rule['FREQ'].kind_of?(When::TM::Duration) ||
1210
+ FreqIndex.key?(rule['FREQ'])
1211
+ if (rule['UNTIL'].kind_of?(String))
1212
+ rule['UNTIL'] += new_zone unless rule['UNTIL'] =~ /Z\z/
1213
+ rule['UNTIL'] = When.when?(rule['UNTIL'])
1214
+ end
1215
+ unless rule['UNTIL']
1216
+ rule['UNTIL'] = When.now + Event.default_until
1217
+ end
1218
+ rule['COUNT'] = rule['COUNT'].to_i if (rule['COUNT'])
1219
+ rule['INTERVAL'] = (rule['INTERVAL'] || 1).to_i
1220
+ rule['WKST'] = Residue.day_of_week(rule['WKST'] || 'MO')
1221
+
1222
+ if (rule['BYSETPOS'])
1223
+ rule['BYSETPOS'] = rule['BYSETPOS'].split(/,/).map {|v|
1224
+ pos = v.to_i
1225
+ pos -= 1 if (pos>0)
1226
+ pos
1227
+ }
1228
+ end
1229
+ ['YEARPOS', 'DAYPOS', 'YEARST', 'DAYST'].each do |by_part|
1230
+ if (rule[by_part])
1231
+ rule[by_part].each_pair do |c,v|
1232
+ rule[by_part][c] = v.to_i
1233
+ end
1234
+ else
1235
+ rule[by_part] = {}
1236
+ end
1237
+ end
1238
+
1239
+ if (rule['BYDAY'])
1240
+ v = rule['BYDAY'].delete("")
1241
+ if (v)
1242
+ raise ArgumentError, "The BYWEEKDAY and BYDAY are exclusive" if (rule['BYWEEKDAY'])
1243
+ rule['BYWEEKDAY'] = v
1244
+ end
1245
+ rule.delete('BYDAY') if rule['BYDAY'].size == 0
1246
+ end
1247
+
1248
+ count = 0
1249
+ if (rule['BYWEEKNO'])
1250
+ raise ArgumentError, "The 'BYWEEKNO' is not allowed" unless (rule['FREQ'] == 'YEARLY')
1251
+ count += 1
1252
+ end
1253
+ count += 1 if (rule['BYYEARDAY'])
1254
+ raise ArgumentError, "The 'BYMONTHDAY' is not allowed" if (rule['BYMONTHDAY'] && (count>0))
1255
+ count += 1 if (rule['BYMONTH'])
1256
+ raise ArgumentError, "The BYMONTH, BYWEEKNO and BYYEARDAY are exclusive" if (count > 1)
1257
+
1258
+ # ロジックオブジェクトの生成
1259
+ if (rule['BYWEEKDAY'])
1260
+ rule['BYWEEKDAY'] = Logic::Weekday.new('BYWEEKDAY', rule['BYWEEKDAY'])
1261
+ end
1262
+
1263
+ ['YEAR', 'DAY'].each do |part|
1264
+ by_part = 'BY' + part
1265
+ if (rule[by_part])
1266
+ rule[by_part].each_pair do |ref,list|
1267
+ position = rule[(by_part == 'BYYEAR') ? 'YEARPOS' : 'DAYPOS'][ref]
1268
+ start = rule[(by_part == 'BYYEAR') ? 'YEARST' : 'DAYST' ][ref]
1269
+ rule[by_part][ref] = ((ref.to_i==0) ? Logic::Enumerator :
1270
+ Logic::Residue).new(by_part, list, position, ref, start)
1271
+ end
1272
+ end
1273
+ end
1274
+
1275
+ ['MONTH', 'WEEKNO', 'YEARDAY', 'MONTHDAY'].each do |part|
1276
+ by_part = 'BY' + part
1277
+ rule[by_part] = Logic.const_get(part.capitalize).new(by_part, rule[by_part]) if rule[by_part]
1278
+ end
1279
+
1280
+ ['HOUR', 'MINUTE', 'SECOND'].each do |part|
1281
+ by_part = 'BY' + part
1282
+ clock = dtstart.clock
1283
+ leap = dtstart.time_standard.has_leap?
1284
+ if rule[by_part]
1285
+ if clock
1286
+ index = When::Coordinates::PRECISION[part]
1287
+ lower = clock.base[index]
1288
+ upper = clock.unit[index] + lower
1289
+ end
1290
+ rule[by_part] = Logic.const_get(part.capitalize).new(by_part, rule[by_part], lower, upper, leap)
1291
+ end
1292
+ end
1293
+
1294
+ freq_index = FreqIndex[rule['FREQ']]
1295
+ if (dtstart && dtstart.has_time?)
1296
+ ['HOUR', 'MINUTE', 'SECOND'].each do |part|
1297
+ by_part = 'BY' + part
1298
+ if (rule[by_part]==nil && freq_index && freq_index < PostFreqIndex[by_part])
1299
+ base = dtstart[PostFreqIndex[by_part]]
1300
+ rule[by_part] = Logic.const_get(part.capitalize).new(by_part, base) unless (base == 0)
1301
+ end
1302
+ end
1303
+ end
1304
+
1305
+ # 配列に登録
1306
+ logics = []
1307
+ ['BYYEAR', 'BYMONTH', 'BYWEEKNO', 'BYYEARDAY', 'BYMONTHDAY', 'BYDAY', 'BYWEEKDAY',
1308
+ 'BYHOUR', 'BYMINUTE', 'BYSECOND'].each do |by_part|
1309
+ if (rule[by_part])
1310
+ if (rule[by_part].kind_of?(Hash))
1311
+ rule[by_part].each_pair do |ref,logic|
1312
+ raise ArgumentError, "The #{by_part} must have class specification" if (ref == "")
1313
+ logic.freq_index = freq_index
1314
+ logics << logic
1315
+ end
1316
+ else
1317
+ rule[by_part].freq_index = freq_index
1318
+ logics << rule[by_part]
1319
+ end
1320
+ rule.delete(by_part)
1321
+ freq_index = [PostFreqIndex[by_part], freq_index].max if freq_index
1322
+ end
1323
+ end
1324
+ raise ArgumentError, "BY part must not be specified" if rule['FREQ'].kind_of?(When::TM::Duration) &&
1325
+ logics.length > 0
1326
+ rule[:logics] = logics
1327
+ rule[:parsed] = description
1328
+ return rule
1329
+ end
1330
+
1331
+ #
1332
+ # 多重繰り返しの現在状態
1333
+ #
1334
+ class Step
1335
+
1336
+ attr_reader :index
1337
+ attr_reader :date
1338
+
1339
+ # @private
1340
+ def _current_date
1341
+ return @date[@index]
1342
+ end
1343
+
1344
+ # @private
1345
+ def _previuos_date
1346
+ return @date[@index-1]
1347
+ end
1348
+
1349
+ # @private
1350
+ def _inc
1351
+ @index += 1
1352
+ return self
1353
+ end
1354
+
1355
+ # @private
1356
+ def initialize(date)
1357
+ @index = 0
1358
+ @date = date.kind_of?(Array) ? date : [date]
1359
+ end
1360
+ end
1361
+
1362
+ #
1363
+ # 多重繰り返しロジック
1364
+ #
1365
+ class Logic
1366
+ attr_accessor :freq_index
1367
+ attr_reader :by_part
1368
+ attr_reader :list
1369
+ attr_accessor :cash
1370
+ #protected :cash=
1371
+
1372
+ # @private
1373
+ def _bound(seed, week_start)
1374
+ if @freq_index == When::WEEK
1375
+ # 週の初め
1376
+ bound = seed & week_start
1377
+ if bound == seed
1378
+ lower_bound = bound.floor(When::DAY, nil)
1379
+ higher_bound = lower_bound + week_start.duration
1380
+ else
1381
+ higher_bound = bound.floor(When::DAY, nil)
1382
+ lower_bound = higher_bound - week_start.duration
1383
+ end
1384
+ else
1385
+ # 指定桁で切り捨て/切り上げ
1386
+ lower_bound = seed.floor(@freq_index, nil)
1387
+ higher_bound = seed.ceil(@freq_index, nil)
1388
+ end
1389
+ return [lower_bound, higher_bound]
1390
+ end
1391
+
1392
+ # 候補日時の生成
1393
+ # @private
1394
+ def _candidates(lower_bound, higher_bound)
1395
+ # TODO unitBaseの扱い
1396
+ return @list.map { |ord|
1397
+ if (ord >= 0)
1398
+ period = ord - 1
1399
+ bound = lower_bound
1400
+ else
1401
+ period = ord
1402
+ bound = higher_bound
1403
+ end
1404
+ index = PostFreqIndex[@by_part]
1405
+ period = ord-bound[index] if (index>0)
1406
+ result = bound + When::TM::PeriodDuration.new(period, index)
1407
+ if period > 0 && result.universal_time < lower_bound.universal_time
1408
+ clock = result.clock
1409
+ case clock.tz_prop
1410
+ when When::V::TimezoneProperty ; clock = clock.tz_prop.tzoffsetto
1411
+ when When::Parts::Timezone ; clock = clock.tz_prop.standard
1412
+ end
1413
+ result = result._copy({:date=>result.cal_date, :validate=>:done, :events=>nil,
1414
+ :time=>result.clk_time._copy({:clock=>clock})})
1415
+ end
1416
+ result
1417
+ }
1418
+ end
1419
+
1420
+ # @private
1421
+ def initialize(by_part, list)
1422
+ @by_part = by_part
1423
+ @list =
1424
+ case list
1425
+ when String ; When::Coordinates::Pair::_en_pair_array(list)
1426
+ when Array ;
1427
+ else ; [list]
1428
+ end
1429
+ end
1430
+
1431
+ #
1432
+ # 剰余類を用いた繰り返し
1433
+ # 曜日の指定などに用いる
1434
+ #
1435
+ class Residue < Logic
1436
+ # @private
1437
+ def _candidates(lower_bound, higher_bound)
1438
+ candidate = []
1439
+ @list.each do |ord|
1440
+ nth, residue, period = ord
1441
+ shift = (period[PostFreqIndex[@by_part]] != 0)
1442
+ raise ArgumentError, "n*m+/-s format not permitted" if (nth || shift) && @freq_index >= When::DAY
1443
+ if (nth)
1444
+ date = ((nth >= 0) ? lower_bound & residue :
1445
+ higher_bound & residue)
1446
+ date += period if shift
1447
+ candidate << date
1448
+ else
1449
+ (lower_bound ^ residue).each do |date|
1450
+ date += period if shift
1451
+ break unless date < higher_bound
1452
+ candidate << date
1453
+ end
1454
+ end
1455
+ end
1456
+ return candidate
1457
+ end
1458
+
1459
+ # @private
1460
+ def initialize(by_part, list, position, ref, start)
1461
+ @by_part = by_part
1462
+ divisor = When::Coordinates::Pair._en_number(ref)
1463
+ @list = list.split(/,/).map {|w|
1464
+ raise ArgumentError, "The #{by_part} rule format error" unless w =~ /\A(([-+]?\d+)\*)?(.+?)([-+]\d+)?\z/
1465
+ nth, spec, period = $~[2..4]
1466
+ nth = (nth) ? nth.to_i : position
1467
+ period = When::TM::PeriodDuration.new((period||0).to_i, PostFreqIndex[@by_part])
1468
+ args = [spec.to_i, divisor, (nth) ? ((nth>0) ? nth-1 : nth) : 0]
1469
+ case by_part
1470
+ when 'BYYEAR'
1471
+ start ||= 4
1472
+ start = start % divisor
1473
+ args << {'year'=>start}
1474
+ when 'BYDAY'
1475
+ start ||= 11
1476
+ start = start % divisor
1477
+ args << {'day'=>start} unless (start == 0)
1478
+ end
1479
+ residue = When::Coordinates::Residue.new(*args)
1480
+ [nth, residue, period]
1481
+ }
1482
+ end
1483
+ end
1484
+
1485
+ #
1486
+ # 外部 Enumerator 用いた繰り返し
1487
+ # 春分日、秋分日、復活祭などの指定に用いる
1488
+ #
1489
+ class Enumerator < Logic
1490
+ attr_reader :ref
1491
+ attr_reader :start
1492
+
1493
+ # @private
1494
+ def _candidates(lower_bound, higher_bound)
1495
+ candidate = []
1496
+ @list.each do |ord|
1497
+ nth, spec, period = ord
1498
+ shift = (period[PostFreqIndex[@by_part]] != 0)
1499
+ raise ArgumentError, "n*e+/-s format not permitted" if (nth || shift) && @freq_index >= When::DAY
1500
+ if (nth==nil || nth>0)
1501
+ enum = @ref.enum_for(lower_bound, :forward, {:event=>spec})
1502
+ else
1503
+ enum = @ref.enum_for(higher_bound, :reverse, {:event=>spec})
1504
+ end
1505
+ if (nth)
1506
+ date = nil
1507
+ nth.abs.times {date = enum.succ}
1508
+ date += period if shift
1509
+ candidate << date
1510
+ else
1511
+ enum.each do |date|
1512
+ date += period if shift
1513
+ break unless date < higher_bound
1514
+ candidate << date
1515
+ end
1516
+ end
1517
+ end
1518
+ return candidate
1519
+ end
1520
+
1521
+ # @private
1522
+ def initialize(by_part, list, position, ref, start)
1523
+ @by_part = by_part
1524
+ @ref = When.Resource(ref, '_n:')
1525
+ @start = start
1526
+ @list = list.split(/,/).map {|w|
1527
+ raise ArgumentError, "The #{by_part} rule format error" unless w =~ /\A(([-+]?\d+)\*)?(.+?)([-+]\d+)?\z/
1528
+ nth, spec, period = $~[2..4]
1529
+ nth = (nth) ? nth.to_i : position
1530
+ period = When::TM::PeriodDuration.new((period||0).to_i, PostFreqIndex[@by_part])
1531
+ [nth, spec, period]
1532
+ }
1533
+ end
1534
+ end
1535
+
1536
+ #
1537
+ # BYMONTHを実装
1538
+ # @private
1539
+ class Month < Logic
1540
+ end
1541
+
1542
+ #
1543
+ # BYWEEKNOを実装
1544
+ # @private
1545
+ class Weekno < Logic
1546
+ # @private
1547
+ def _bound(seed, week_start)
1548
+ duration = week_start.duration
1549
+ center = duration / 2
1550
+ # 1月4日を含む週
1551
+ return [seed.floor(When::YEAR, nil), seed.ceil(When::YEAR, nil)].map { |s|
1552
+ s += center
1553
+ bound = s & week_start
1554
+ bound -= duration if (bound > s)
1555
+ bound
1556
+ }
1557
+ end
1558
+ end
1559
+
1560
+ #
1561
+ # BYYEARDAYを実装
1562
+ # @private
1563
+ class Yearday < Logic
1564
+ end
1565
+
1566
+ #
1567
+ # BYMONTHDAYを実装
1568
+ # @private
1569
+ class Monthday < Logic
1570
+ end
1571
+
1572
+ #
1573
+ # BYWEEKDAY(=BYDAY)を実装
1574
+ # @private
1575
+ class Weekday < Residue
1576
+ # @private
1577
+ def initialize(by_part, list)
1578
+ @by_part = by_part
1579
+ @list = list.split(/,/).map {|w|
1580
+ raise ArgumentError, "The BYDAY rule format error" unless w =~ /\A([-+]?\d+)?(MO|TU|WE|TH|FR|SA|SU)([-+]\d+)?\z/
1581
+ nth, spec, period = $~[1..3]
1582
+ if nth
1583
+ nth = nth.to_i
1584
+ nth = (nth>0) ? nth-1 : nth
1585
+ end
1586
+ residue = When::Coordinates::Residue.day_of_week(spec) >> (nth||0)
1587
+ [nth, residue, When::TM::PeriodDuration.new([0,0,(period||0).to_i])]
1588
+ }
1589
+ end
1590
+ end
1591
+
1592
+ #
1593
+ # BYHOURを実装
1594
+ # @private
1595
+ class Hour < Logic
1596
+ # @private
1597
+ def _bound(seed, week_start)
1598
+ return super unless @freq_index == When::WEEK
1599
+ lower_bound = seed.floor(When::DAY, nil)
1600
+ higher_bound = lower_bound + week_start.duration
1601
+ [lower_bound, higher_bound]
1602
+ end
1603
+
1604
+ # @private
1605
+ def initialize(by_part, list, lower=nil, upper=nil, leap=false)
1606
+ super(by_part, list)
1607
+ if lower
1608
+ @list.each do |v|
1609
+ raise ArgumentError, "#{by_part} out of range: #{v}" unless lower <= v && v < upper
1610
+ end
1611
+ end
1612
+ end
1613
+ end
1614
+
1615
+ #
1616
+ # BYMINUTEを実装
1617
+ # @private
1618
+ class Minute < Hour
1619
+ end
1620
+
1621
+ #
1622
+ # BYSECONDを実装
1623
+ # @private
1624
+ class Second < Logic
1625
+ # @private
1626
+ def initialize(by_part, list, lower=nil, upper=nil, leap=false)
1627
+ super(by_part, list)
1628
+ if lower
1629
+ @list = @list.map {|v|
1630
+ v -= 1 if leap && upper <= v && v < upper+1
1631
+ raise ArgumentError, "#{by_part} out of range: #{v}" unless lower <= v && v < upper
1632
+ v
1633
+ }
1634
+ end
1635
+ end
1636
+ end
1637
+ end
1638
+ end
1639
+ end
1640
+ end