RRULE文字列とEKRecurrenceRuleを変換するAppleScriptハンドラ



* カレンダーやリマインダーを扱うEventKitフレームワーク
	* アプリケーションを起動しなくても操作できる
	* AppleScriptに対応していない項目を取得、設定できる
* EverntKitフレームワークにあるイベントの繰り返しを管理するEKRecurrenceRuleクラス
	* `initRecurrenceWithFrequency:interval:end:`メソッドで作成
* 一方で、一般的なカレンダーで繰り返しルールを表す文字列としてよく使われるRRULE (Recurrence Rule)
	* 例：`FREQ=DAILY;INTERVAL=2`は「隔日の繰り返し」を意味する
	* [iCalendar RFC](http://www.ietf.org/rfc/rfc2445.txt)の一部
* 両者を相互変換するAppleScriptハンドラを作成
	* EKRecurrenceRuleでは1日未満の繰り返しに対応していないため、`FREQ=MINUTELY`などは無視
	* 週の開始曜日を決める`WKST`もEKRucurrenceRuleでは設定できない模様



```AppleScript
use scripting additions
use framework "EventKit"
use framework "Foundation"

--Every other day
set recurrenceRule to my recurrenceRuleFromString("FREQ=DAILY;INTERVAL=2")
--> «class ocid» id «data optr00000000200393FFB97F0000»

my stringFromRecurrenceRule(recurrenceRule)
--> "FREQ=DAILY;INTERVAL=2"

on recurrenceRuleFromString(recur as text)
	--require framework: EventKit, Foundation
	
	--recurをパースしてdictionaryに変換
	set RRULE to current application's NSMutableDictionary's dictionaryWithObject:1 forKey:"INTERVAL"
	set scanner to current application's NSScanner's scannerWithString:(recur & ";")
	repeat until scanner's atEnd as boolean
		set {scanResult, |name|} to scanner's scanUpToString:"=" intoString:(reference)
		set scanResult to scanner's scanString:"=" intoString:(missing value)
		set {scanResult, value} to scanner's scanUpToString:";" intoString:(reference)
		set scanResult to scanner's scanString:";" intoString:(missing value)
		try
			
			if |name|'s isEqualToString:"FREQ" then
				set freqDictionary to (current application's NSDictionary's dictionaryWithObjects:{current application's EKRecurrenceFrequencyDaily, current application's EKRecurrenceFrequencyWeekly, current application's EKRecurrenceFrequencyMonthly, current application's EKRecurrenceFrequencyYearly} forKeys:{"DAILY", "WEEKLY", "MONTHLY", "YEARLY"})
				set value to (freqDictionary's objectForKey:value)
				
			else if |name|'s isEqualToString:"UNTIL" then
				if (value's |length| as integer) = 8 then
					set endDate to my dateFromStringUsingFormat(value, "yyyyMMdd")
				else if (value's |length| as integer) = 15 then
					set endDate to my dateFromStringUsingFormat(value, "yyyyMMdd'T'HHmmss")
				else if (value's |length| as integer) = 16 then
					set endDate to my dateFromStringUsingFormat(value, "yyyyMMdd'T'HHmmss'Z'")
					set endDate to my shiftDateFromGMT(endDate)
				end if
				set value to (current application's EKRecurrenceEnd's recurrenceEndWithEndDate:endDate)
				set |name| to "END"
				
			else if |name|'s isEqualToString:"COUNT" then
				set value to (current application's EKRecurrenceEnd's recurrenceEndWithOccurrenceCount:(value's integerValue))
				set |name| to "END"
				
			else if |name|'s isEqualToString:"INTERVAL" then
				set value to value's integerValue
				
			else if |name|'s isEqualToString:"BYDAY" then
				set weekdayDictionary to (current application's NSDictionary's dictionaryWithObjects:{current application's EKSunday, current application's EKMonday, current application's EKTuesday, current application's EKWednesday, current application's EKThursday, current application's EKFriday, current application's EKSaturday} forKeys:{"SU", "MO", "TU", "WE", "TH", "FR", "SA"})
				set recurrenceDayOfWeekArray to current application's NSMutableArray's array()
				repeat with weekdaynum in (value's componentsSeparatedByString:",") as list
					set |weekday| to (weekdayDictionary's objectForKey:(text -2 thru -1 of weekdaynum))
					if (count weekdaynum) = 2 then
						set recurrenceDayOfWeek to (current application's EKRecurrenceDayOfWeek's dayOfWeek:|weekday|)
					else
						set recurrenceDayOfWeek to (current application's EKRecurrenceDayOfWeek's dayOfWeek:|weekday| weekNumber:(text 1 thru -3 of weekdaynum as integer))
					end if
					(recurrenceDayOfWeekArray's addObject:recurrenceDayOfWeek)
				end repeat
				set value to recurrenceDayOfWeekArray
				
			else if (current application's NSArray's arrayWithArray:{"BYMONTHDAY", "BYYEARDAY", "BYWEEKNO", "BYMONTH", "BYSETPOS"})'s containsObject:|name| then
				set value to ((value's componentsSeparatedByString:",")'s valueForKeyPath:"self.integerValue")
				
			else
				set value to |name| & "=" & value
				set |name| to "UNRECOGNIZED NAME"
			end if
			
			--RRULEに追加
			(RRULE's setObject:value forKey:|name|)
			
			--エラー
		on error
			error "Invalid " & |name| & " in Recurrence Rule: " & |name| & "=" & value
		end try
	end repeat
	
	--EKRecurrenceRule作成
	set nameSet to current application's NSSet's setWithArray:(RRULE's allKeys())
	if nameSet's isSubsetOfSet:(current application's NSSet's setWithArray:{"FREQ", "INTERVAL", "END", "UNRECOGNIZED NAME"}) then
		return current application's EKRecurrenceRule's alloc()'s initRecurrenceWithFrequency:(RRULE's objectForKey:"FREQ") interval:(RRULE's objectForKey:"INTERVAL") |end|:(RRULE's objectForKey:"END")
	else
		return current application's EKRecurrenceRule's alloc()'s initRecurrenceWithFrequency:(RRULE's objectForKey:"FREQ") interval:(RRULE's objectForKey:"INTERVAL") daysOfTheWeek:(RRULE's objectForKey:"BYDAY") daysOfTheMonth:(RRULE's objectForKey:"BYMONTHDAY") monthsOfTheYear:(RRULE's objectForKey:"BYMONTH") weeksOfTheYear:(RRULE's objectForKey:"BYWEEKNO") daysOfTheYear:(RRULE's objectForKey:"BYYEARDAY") setPositions:(RRULE's objectForKey:"BYSETPOS") |end|:(RRULE's objectForKey:"END")
	end if
end recurrenceRuleFromString

on stringFromRecurrenceRule(recurrenceRule)
	--require framework: Foundation
	return (recurrenceRule's |description|'s componentsSeparatedByString:space)'s lastObject() as text
end stringFromRecurrenceRule

on dateFromStringUsingFormat(dateString as text, dateFormat as text)
	--reference: UTS #35: Unicode Locale Data Markup Language - http://www.unicode.org/reports/tr35/tr35-25.html#Date_Format_Patterns
	set dateFormatter to my currentDateFormatter(dateFormat)
	return (dateFormatter's dateFromString:dateString) as date
end dateFromStringUsingFormat

on shiftDateFromGMT(aDate as date)
	global timeToGMT
	try
		timeToGMT
	on error number -2753
		set timeToGMT to time to GMT
	end try
	return aDate + timeToGMT
end shiftDateFromGMT

on currentDateFormatter(dateFormat as text)
	--require framework: Foundation
	set dateFormatter to current application's NSDateFormatter's alloc()'s init()
	set dateFormatter's locale to current application's NSLocale's currentLocale()
	set dateFormatter's timeZone to current application's NSTimeZone's localTimeZone()
	set dateFormatter's dateFormat to dateFormat
	return dateFormatter
end currentDateFormatter
```


## 更新履歴

* 2016-02-03: EventKitフレームワークを使って`stringFromRecurrenceRule`ハンドラを作成
* 2016-02-05: EKRecurrenceRuleをRRULE文字列に変換する`stringFromRecurrenceRule`ハンドラを作成
* 2016-03-04: `recurrenceRuleFromString`ハンドラで`recur`のパースにNSScannerを使用