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