when_exe 0.3.6 → 0.3.7

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