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