Archive for the 'iCal/Calendar' Category

2017/01/05 iCal4ObjCのじっけん

オープンソースのiCalendarファイルの読み込み/作成を行うフレームワーク「iCal4ObjC」(By Satoshi Konno)をビルドしてAppleScriptから呼び出してみました。

iCalendarファイルの生成については、カレンダー.app(旧称:iCal)を使う方法もありますが、同アプリはApple純正アプリケーションの中でもトップクラスの「動作内容に信用がおけない」ものなので、こうして他のプログラム経由でも読み書きできるようにしておくことは重要と思われます。

とりあえず、OS X 10.10以降で動作するようにiCal4ObjCをFramework化してビルドしたバイナリを用意しておきました。自己責任でframeworkを~/Library/Frameworksフォルダ以下にアーカイブを展開してコピーしたうえで本Scriptをおためしください。

–> Download Framework Binary

AppleScript名:iCal4ObjCのじっけん(iCalendarファイルの作成)
– Created 2017-01-05 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “iCalendarKit”
–https://github.com/cybergarage/iCal4ObjC
–http://piyocast.com/as/archives/4377

set aCal to current application’s CGICalendar’s alloc()’s init()

–Add a object
set aCalObj to current application’s CGICalendarObject’s alloc()’s initWithProdid:“//CyberGarage//iCal4ObjC//EN”
aCal’s addObject:aCalObj

–Add a component
set aCalComp to current application’s CGICalendarComponent’s alloc()’s initWithType:“VTODO”
aCalObj’s addComponent:aCalComp

–Add a property
set aCalProp to current application’s CGICalendarProperty’s alloc()’s init()
aCalProp’s setName:“SUMMARY”
aCalProp’s setValue:“Write report”
aCalComp’s addComponent:aCalProp

–Write to File
set aFilePath to POSIX path of (((path to desktop) as string) & (current application’s NSUUID’s UUID()’s UUIDString()) as string) & “.ics”
set calRes to (aCal’s writeToFile:aFilePath) as boolean

★Click Here to Open This Script 

AppleScript名:iCal4ObjCのじっけん(iCalendarファイルのparse)
– Created 2017-01-05 by Takaaki Naganoya
– 2017 Piyomaru Software
use AppleScript version “2.4″
use scripting additions
use framework “Foundation”
use framework “iCalendarKit”
–https://github.com/cybergarage/iCal4ObjC
–http://piyocast.com/as/archives/4377

set aCalFile to POSIX path of (choose file of type {“com.apple.ical.ics”} with prompt “ICSファイルを選択”)

set aCal to current application’s CGICalendar’s alloc()’s init()
if (aCal’s parseWithPath:aCalFile |error|:(missing value)) as boolean is not equal to true then
  error “ics parse error”
end if

–iCalendar(.ics)ファイル中には複数のカレンダー情報が含まれる可能性があるため、ループ処理
set icalObjList to (aCal’s objects()) as list

repeat with i in icalObjList
  set j to contents of i
  
set compList to (j’s components()) as list
  
  
repeat with i2 in compList
    set j2 to contents of i2
    
    
repeat with i3 in j2’s |properties|()
      set j3 to contents of i3
      
set iCalPropName to j3’s |name|()
      
set iCalPropValue to j3’s value()
      
      
repeat with i4 in (j3’s parameters())
        set j4 to contents of i4
        
set icalParamName to j4’s |name|()
        
set iCalPropValue to j4’s value()
        
log {icalParamName, iCalPropValue}
      end repeat
    end repeat
  end repeat
end repeat

★Click Here to Open This Script 

2016/11/18 ライブラリを使って指定カレンダー(iCloud)の指定日のイベントを削除する

Shane StanleyのAppleScriptライブラリ「Calendar Lib」を利用して、EventKit経由で指定カレンダーの指定日時のイベントを削除するAppleScriptです。

構文確認(コンパイル)および実行のためには、Shane Stanleyの「CalendarLib」を~/Library/Script Librariesフォルダにインストールしておくことが必要です(作成時にはバージョン1.1.2を使用)。

昨日のCalendar.appを操作するScriptがあまりに実用的でなかったため、ライブラリを使ってOSのAPIを直接呼んでみることにしました。あらかじめ、できることはわかっていたものの、ドキュメント中に削除のサンプルが存在しておらず、ライブラリ内を調査してイベントの削除方法をみつけました。

本Script実行時にCalendar.appを起動していると、実行後3秒程度でCalendar.appの画面側でもイベントが削除されたことが確認できました。

Calendar Libではカレンダー種別を cal local/ cal cloud/ cal exchange/ cal subscriptionと明示的に指定できるため、Calendar.appを直接操作するよりも(はるかに)便利です。

AppleScript名:ライブラリを使って指定カレンダー(iCloud)の指定日のイベントを削除する
– Created 2016-11-18 17 by Takaaki Naganoya
– 2016 Piyomaru Software
use AppleScript version “2.5″
use scripting additions
use framework “Foundation”
use calLib : script “CalendarLib EC”
–http://piyocast.com/as/archives/4322

set sDat to “2016/11/21 0:00:00″
set eDat to “2016/11/22 0:00:00″
set sDatO to date sDat
set eDatO to date eDat

set theStore to fetch store
set theCal to fetch calendar “ぴよまるソフトウェア” cal type cal cloud event store theStore – change to suit

set theEvents to fetch events starting date sDatO ending date eDatO searching cals {theCal} event store theStore

repeat with i in theEvents
  set j to contents of i
  
remove event event j event store theStore without future events
end repeat

★Click Here to Open This Script 

2016/11/18 Calendarで指定カレンダー、指定日のイベントを削除する

カレンダー.app(Calendar.app)で、指定カレンダーに登録されている指定日のイベントを削除するAppleScriptです。

カレンダー.app(旧称iCal)相手のScriptingは、「やってできないこともないが、落とし穴があちこちに空いているので、GUIアプリケーション相手に命令を投げるよりも、フレームワーク経由で操作したほうがいい雰囲気が漂っている」ものです。macOS標準装備のApple純正アプリケーションの中でも、ダントツの出来の悪さが光ります。

カレンダー.appには罠や要注意点がゴロゴロしているので、出来の悪さを知っていないと悩むことになります。特定のOSバージョンに固有のバグが存在していたりもします。

仕様上の罠(1)カレンダーの特定

カレンダー.app上でイベントを特定する前に、どのカレンダーに登録されているイベントなのかを特定する必要があります。名称で指定することも可能ですが、「このMac内」(Local)と「iCloud」(クラウド上)など、複数の場所で同じ名前のカレンダーを作成できてしまうため、あらかじめカレンダーの「説明」欄に区別するためのテキストを入れておくなどして、任意のカレンダーを特定する必要があります。

仕様上の罠(2)イベントの特定

カレンダー上のイベントを特定するためには、開始日時と終了日時を指定して、その条件に合致するものだけを抽出(フィルタ参照)することになります。このフィルタ参照を書けるかどうかでイベントの特定が行えるかどうかが決まってきます。開始日時と終了日時という2つの条件をandで指定するフィルタ参照で、2つ目の条件記述に「of it」という書き方をしないと抽出ができませんでした。慣れが必要な部分なので、いきなりこれを書けと言われても困るところです。

さらに、イベントには開始〜終了日時が明確に記述されているものと、複数日にまたがるイベントや、毎週金曜日といった指定が行われているものもあります。こういうタイプのイベントが存在している場合には、別途対処する必要が出てきます(けっこうたいへん、というか無理。この手のプログラムを仕事で書く必要がある場合には仕様を決定する際に真っ先に「この手のイベントは例外とさせてほしい」と提案する箇所)。

仕様上の罠(3)イベントはリストで返ってくる

これはカレンダー.appにかぎったことではないのですが、フィルタ参照で抽出したオブジェクト(ここではイベント)は、1件かもしれないし、0件かもしれないし、100件かもしれません。結果はリストで返ってくるのでループで1件ずつ削除するか、あるいはフィルタ参照で抽出した結果に対して削除コマンドを実行することになります。

仕様上の罠(4)イベントを削除しても、すぐには画面に反映されない

AppleScriptからカレンダー.app上のイベントを削除しても、画面表示にすぐには反映されません。このため、いったんカレンダー.appを終了させて、ふたたび起動することで確認できました。reload calendar命令もあるので、実行すると最新の状態に更新してくれるのかと思ったものの、いったん終了しないと削除された状態を確認できませんでした。

これはキツい(^ー^;;;;

AppleScript名:Calendarで指定カレンダー、指定日のイベントを削除する
– Created 2016-11-18 by Takaaki Naganoya
– 2016 Piyomaru Software
–http://piyocast.com/as/archives/4321
use AppleScript version “2.4″
use scripting additions

set sDat to “2016/11/21 0:00:00″
set eDat to “2016/11/22 0:00:00″
set sDatO to (date sDat)
set eDatO to date eDat

tell application “Calendar”
  –対象となるカレンダーを特定
  
set cList to every calendar whose name is “ぴよまるソフトウェア” and description is equal to “iCloud”
  
if cList = {} then return
  
set theCalendar to first item of cList
  
  
–カレンダー内のイベントを特定
  
tell theCalendar
    set eList to every event whose start date sDatO and end date of it < eDatO
  end tell
  
  
–削除する(一括削除もできるが、デバッグのために1件ずつ削除)
  
repeat with i in eList
    set j to contents of i
    
delete j
  end repeat
  
  
quit
end tell

delay 1

tell application “Calendar”
  activate
end tell

★Click Here to Open This Script 

2016/03/23 AppleがMac Automation Scripting Guideを公開

3/21にAppleが「Mac Automation Scripting Guide」を公開しました。

macautomation.png

比較的初心者向けの内容となっています。以前からさんざん批判されていたような、「the」などの単語をわざわざ書いて英語に寄せた英語ライクな記法(Appleのエンジニアだけが書いている方言)をとっていないところは評価できますが、短く書けるところを長々と書いているあたりは見ていてゲッソリします。

そのほか、Appedixesの項目に「Objecitve-C to AppleScript(ASOC) Quick Translation Guide」もついています。

また、ほぼ同時に「Calendar Scripting Guide」も公開されています。

calendarscripting_resized.png

Calendar.appについてはReminder.appとの間でcalendarが識別できず、直す気もさらっさらないのかと個人的に思っていましたが、いま確認したら直っているようで。

2015/02/02 カレンダー.app(旧称iCal)で選択中のイベントの情報を取得

海外のBlogで、カレンダー.app(Calendar.app)の画面上で選択中のイベントの情報を取得するAppleScriptが公開されていました。

→ Reference selected Calendar events in AppleScript

カレンダー.appのAppleScript用語辞書にはselectionとかselected eventなどの予約語が用意されておらず、そのうえ「リマインダー.app」とイベントが共有されており区別できないなど、Apple純正アプリとしてはかなり「残念な出来」になっている筆頭残念アプリケーションです。

同Blogによれば、AppleScript用語辞書にselectionなどのプロパティは存在しないのに、設定ファイル”com.apple.ical.plist”には「SelectedEvents」のエントリが存在しているので、それをdefaults readコマンドで読み取って、parseして、sqlite3経由でsqlを発行して該当するイベントの情報を取得するのだとか。

・・・ここまでしないと取得できないことを嘆くべきか、存在しない機能を実現した努力を讃えるべきなのか、そもそも最初からselectionの機能があるべきという主張をすべきなのか、なかなか悩ましいところです。

カレンダー.app上でイベントを選択して同AppleScriptを実行してみたところ、どうも「選択中のイベントを取得できる」ときと「できない」ときがあります。ソース中にもそのように書いてあり、いろいろ試行錯誤してみたところ・・・

数行追加したら、バッチリ取れるようになりました。ただ、フィードバックするにも同Blogはコメント読んでないようで・・・修正版を掲載しておきます。オリジナルのScriptでは、取得した「選択中のイベント」の発生15分前にアラームを鳴らすようにしてありましたが、変更されるのがいやだったのでコメントアウトしてあります。

AppleScript名:カレンダー.app上で選択中のイベントを取得する
–https://www.johneday.com/1086/reference-selected-calendar-events-applescript
– Title: Reference selected Calendar events in AppleScript
– Created 2015-02-02 by @johneday on Twitter
– Modified 2015-02-02 by Takaaki Naganoya

use AppleScript version "2.4"
use scripting additions

——————————
– ABOUT
——————————
– Written and tested in Yosemite, Calendar Version 8.0
– The plist may take several seconds to update after a new event has been selected.
– Only the first instance of a recurring event will be referenced.

——————————-
– Modify by Takaaki Naganoya
——————————
tell application "Calendar"
  if running = false then return
  
activate
end tell
do shell script "sync"

——————————-
– MAIN CODE
——————————
set defaultsReply to (do shell script "defaults read com.apple.ical SelectedEvents")
set selectedEvents to parseDefaults(defaultsReply)

if selectedEvents = {} then
  display notification "Please try again" with title "No Calendar Event Selected"
  
return
end if

set eventReferenceList to {}
repeat with sEvent in selectedEvents
  set {eventID, calendarID} to sqlQuery(sEvent)
  
tell application "Calendar"
    set eventReference to event id eventID of calendar id calendarID
    
– INSERT YOUR CODE TO PROCESS EACH EVENT
    
    – Example of "Alert 15 minutes before start"
    
–my addDisplayAlarm(eventReference, -15)
    
    – OR BUILD A LIST OF EVENTS
    
set end of eventReferenceList to eventReference
  end tell
end repeat
return eventReferenceList

——————————
– HANDLERS
——————————
on parseDefaults(resultText)
  set localUIDs to {}
  
set {TID, text item delimiters} to {text item delimiters, quote}
  
set resultItems to text items of resultText
  
set text item delimiters to TID
  
repeat with i from 1 to (count resultItems)
    if i mod 2 = 0 then set end of localUIDs to resultItems’s item i
  end repeat
  
return localUIDs
end parseDefaults

on sqlQuery(localUID)
  local dateString, localUID
  
if localUID contains "/" then
    set {TID, text item delimiters} to {text item delimiters, "/"}
    
set {dateString, localUID} to text items of localUID
    
set text item delimiters to TID
  end if
  
  set sqlText to "
SELECT DISTINCT zcalendaritem.zshareduid AS eventID
, znode.zuid as calID
FROM zcalendaritem
JOIN znode
ON znode.z_pk = zcalendaritem.zcalendar
AND zcalendaritem.zlocaluid = ’" & localUID & "’
;"
  
  set sqlPath to POSIX path of (path to library folder from user domain) & "Calendars/Calendar Cache"
  
set {TID, text item delimiters} to {text item delimiters, "|"}
  
set {eID, cID} to text items of (do shell script "echo " & quoted form of sqlText & " | sqlite3 " & quoted form of sqlPath)
  
set text item delimiters to TID
  
return {eID, cID}
end sqlQuery

on addDisplayAlarm(myEvent, triggerInterval)
  tell application "Calendar"
    tell myEvent
      if not (exists (display alarms whose trigger interval = triggerInterval)) then
        set myAlarm to make new display alarm at end of display alarms with properties {trigger interval:triggerInterval}
      end if
    end tell
  end tell
end addDisplayAlarm

★Click Here to Open This Script 

2014/01/06 Calendar.appのfile open eventを登録できないバグ@10.9

OS X 10.9, MavericksのCalendar.appで、AppleScriptからfile open alarmを作成しようとすると、

calbug1.png

というド派手なダイアログが出て、イベントの登録が行えないというバグの存在を(いまさら)知りました。

正直、AppleScriptのタイマー実行用にCalendar.appは使っていないので、個人的には影響はないのですが、困っている人もいるようです。

# Calendar.app/iCalは昔からいろいろ問題を起こしていたので、「信用していない」というのが、最大の理由です

コンソールでエラーメッセージを確認してみると……

calbug2.png

などと出ており、まったくお手上げの状況のようです。

file open event作成時のファイルパスをPOSIX pathで渡しても(これが普通)、aliasで渡しても、URL形式で渡しても状況は変わりません(全部試しました)。

Calendar.appやiCal.app、さらにMac App Storeで販売されているタイマーアプリケーションは、どいつもこいつもAppleScriptの定期実行には役立たずだと感じています。で、結局自前でタイマー機能までAppleScriptで作ってしまうので、個人的には困っていません。

スクリプト名:alarm_script
–Calendar.appのopen file alarmのバグの検証用AppleScript
–AppleScript自体の実行は完了するが、Calendar.appがエラーダイアログを表示する

set targetDate to “2015/01/06 20:20:00″
set targetDate to date targetDate

set theoutdatecal to “Timers”
set eventname to “てすと”

set aFilePath to choose file
set aPOSIXpath to POSIX path of aFilePath

(*
tell application “Finder”
  set aURL to URL of aFilePath
end tell
*)

–add outdate events to calendar
tell application “Calendar”
  tell calendar theoutdatecal
    set deactivate_date to make new event with properties {start date:targetDate, summary:eventname, allday event:false, status:confirmed}
  end tell
  
tell deactivate_date
    make new open file alarm at end with properties {trigger interval:1440, filepath:aPOSIXpath}
  end tell
end tell

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/12/19 Calendar.appのカレンダーを名称で抽出。Reminderのlistが見えてしまうものは除く

カレンダー(Calendar.app。旧称=iCal)上でカレンダーを名称で指定して取得するAppleScriptです。

カレンダー上では、自分自身のカレンダーだけでなく、あろうことかリマインダーのリストまで取得できてしまいます。これは、OS Xのバグというよりは、バカというべきか……カレンダー(Calendar.app)上では両者の識別方法はまったくありません。

さらに、両方とも「ホーム」といった同じ名前のカレンダー/リストがデフォルトで存在しており、カレンダー(Calendar.app)上では両者がまったく識別できません。

これは、Wordでオープン中の書類を問い合わせたら、ついでにPowerPointの書類まで返ってきて、さらにAppleScriptからはWord書類もPowerPoint書類も区別がつかない状態、といったら分りやすいでしょうか。関係ない他のアプリケーションのデータが見えてしまうなんて、バカもやすみやすみ言っていただきたいものです。

cal_rem.png

海外の仕事で調査中にこれに気付いて真っ青になりました。リマインダー側のリストの名称を手作業で替えてもらってなんとかなりましたが、1年に数回ある「クパティーノの某社に殺意を感じる一瞬」のうちの1回をカウントアップしてしまった次第。

AppleScriptから操作する分には、AppleScriptで取得できるデータの範囲内でしか処理できません。この、Appleのバグ的な仕様のためにひどい目にあわされています。バグレポートに書いたものの、Appleのエンジニアは文字が読めないか、自分たちで仕事をしていないためか、今日に至るも直っていません。

そのため、これを避けるための処理を書いてみました。動作原理は(苦労させられている割には)わりと簡単です。

まず、リマインダーにリストのIDをすべて問い合わせておいて、カレンダー(Calendar.app)で取得されるカレンダーのIDと付け合わせを行って、「カレンダー(Calendar.app)上で見えてしまう、リマインダーのリスト」を除外するという処理を行っています。

スクリプト名:Calendar.appのカレンダーを名称で抽出。Reminderのlistが見えてしまうものは除く
set aRes to getCalendarObjFromCal(“ホーム”) of me
–> {calendar id “8D0AC013-2AF0-4EA1-A563-AE59C811EB36″ of application “Calendar”}

–Calendar.appのカレンダーを名称で抽出。Reminderのlistが見えてしまうものは除く
on getCalendarObjFromCal(aName)
  
  
–先に、リマインダーのカレンダー(list)のIDを取得しておく
  
tell application “Reminders”
    set rList to id of every list
  end tell
  
  
–カレンダーを抽出するさいに、名称でしぼりこむほか、リマインダーのカレンダーを除外する
  
tell application “Calendar”
    set r2List to every calendar whose name is equal to aName
    
    
set r3List to {}
    
    
repeat with i in r2List
      set anID to uid of i
      
if anID is not in rList then
        set the end of r3List to (contents of i)
      end if
    end repeat
    
    
return r3List
  end tell
  
end getCalendarObjFromCal

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2013/08/22 Calendar(iCal)からリマインダーのカレンダーが見える上に区別ができない

完全にバグだと思います。カレンダー(iCal)上のカレンダーやイベントを処理しようとしたときに、いろいろ問題が起こるという話です。カレンダー(iCal)のScriptingをそれほど本気で行うことはなかったのですが、仕事で依頼を受けて(本気で)調べてみたら、驚愕の事実が判明。

cal1.png

カレンダー(iCal)アプリケーションには、オブジェクトとして「カレンダー(calendar)」が定義されており、AppleScriptから各calendarにアクセスできます。

cal2.png

GUI上では、↑のように見えます。これらの名称をAppleScriptから取得すると……

cal3.png

カレンダー(iCal)上に存在していないカレンダーの名称まで取得できてしまいます。

致命的なのは、同じ「ホーム」といった名称のカレンダーが存在しているにもかかわらず、同じcalendarのプロパティを見てみても、識別するための属性値が何もないということです。

「ホーム」というありがちなcalendar名称はともかく、「マンガの発売予定日」という名称はどこかで見たことがありました。そういえば………

cal4.png

「リマインダー」アプリに、そういうリストを登録していました。

cal5.png

そこで、「リマインダー」上のリスト名称をちょこっと変更。「ホーム」を「ホーム(reminder)」に。他も同様。

cal6.png

この状態で、カレンダー(iCal)アプリにAppleScriptから名称の取得を実行すると……

cal7.png

うわ、カレンダー(iCal)に問い合わせたはずなのに、リマインダーのリストまで取得できてしまっていることが確認できました。

カレンダー(iCal)もリマインダーも同じくEvent Kitを利用しているのだと思いますが……違うアプリのデータ内容が見えてしまうのは、いかがなものでしょうか?

早くバグレポートを書かないと、10.9でも同じ症状が発生しているので困ります。

2011/01/13 iCalで選択中のイベントのカレンダー情報を取得する

iCal上で選択中のイベントが所属しているカレンダーの名称を取得するAppleScriptです。

iCalのAppleScript用語辞書には致命的な欠陥が山のようにあって……GUI上で選択中のイベント(selection)を取得できません。普通のやり方では、選択中のイベントのカレンダー名を取得できません。もっと基本的なところで、いま表示中の月が何月なのかも分かりません。

日時を指定してイベントを登録したり、フィルタ参照でイベントを抽出したりするぐらいで、画面上で選択したイベントから対話的に何かの処理を行うようなAppleScriptを作りにくい(というよりも、作れない)状況にあります。

ical1.jpg

はい、おしまいおしまい〜

……などと言っていては海外の強豪Scripter連中に顔向けできません。

選択中のイベントが存在するのであれば、コピーを実行すると何らかの情報が得られるはずです。

実際にiCal上でコピーを実行してみると、

<ここから>
退去確認@ヴィラ山田
2011年1月22日 を 16:00 〜 17:00 にスケジュールしました
</ここまで>

のような内容が得られました。この情報から日付、開始時刻、終了時刻、イベント名 などを取得できます。

これらの情報を手がかりに、すべてのカレンダー上で検索(フィルタ参照による抽出)を行ってみました。

実行条件は、GUI Scriptingがオンになっていることと、iCal上でなにがしかのイベントが選択状態になっていることです。

ical2.jpg

GUI Scripting(UI Element Scripting)経由で選択イベントのコピー動作を行い、クリップボードの内容を解析。得られた情報をもとに今度はiCalに正攻法ですべてのカレンダーに対して「日付、開始時刻、終了時刻、イベント名」を手がかりにイベントの検索を行います。条件に合致するイベントが見つかったら、カレンダーの名称を取得して返します。これを複数のイベントに対してループで実行してみました。

実験してみたところ、複数のイベントを一度に選択した場合、その中に「繰り返しイベント」が含まれているとiCalがダイアログで警告します。このダイアログが邪魔なので、ダイアログのクローズ処理も行わなくてはなりません。

ical3.jpg

作成時間はトータルで40分程度でしょうか。かなり作り込みましたが、驚くべきことに、さほど実用性がありません、、、

スクリプト名:iCalで選択中のイベントの情報を取得する
set aRes to retSelectedEvent() of me

set icalRes to {}
repeat with i in aRes
  set j to contents of i
  
set the end of icalRes to getCalName(j) of me
end repeat
icalRes
–> {”ホーム”}

–選択中のiCal上のイベントが所属しているカレンダーの名称を取得する
on getCalName(aStr)
  
  
set aList to paragraphs of aStr
  
set aTitle to contents of item 1 of aList
  
set dStr to contents of item 2 of aList
  
  
set dOffset to offset of “日 を “ in dStr
  
set dDate to text 1 thru dOffset of dStr
  
–set dDateObj to date dDate
  
  
set tSeparator to offset of ” 〜 “ in dStr
  
if tSeparator = 0 then
    –「〜」がないことから、終日のスケジュールと判断
    
set timeFrom to “0:00″
    
set timeTo to “23:59:59″
  else
    –開始時刻と終了時刻のあるスケジュールと判断
    
set timeFrom to text (dOffset + 4) thru (tSeparator - 1) of dStr
    
set timeTo to text (tSeparator + 3) thru ((length of dStr) - 12) of dStr
  end if
  
  
set timeFromObj to date (dDate & ” “ & timeFrom)
  
set timeToObj to date (dDate & ” “ & timeTo)
  
  
  
  
tell application “iCal”
    set cnList to name of every calendar
    
set cList to every calendar
    
    
set cCount to 1
    
set findF to false
    
repeat with i in cList
      tell i
        set eList to (every event whose start date is not less than timeFromObj and start date is not greater than timeToObj and summary of it is aTitle)
        
if eList is not equal to {} then
          set findF to true
          
exit repeat
        end if
      end tell
      
set cCount to cCount + 1
    end repeat
    
    
if findF = true then
      set aName to contents of item cCount of cnList
    else
      set aName to “”
    end if
  end tell
  
  
return aName
end getCalName

–選択中のイベント情報をコピー(エラー時にはfalseをリターン)
on retSelectedEvent()
  activate application “iCal”
  
tell application “System Events”
    tell process “iCal”
      –「編集」メニューの項目「コピー」がイネーブルかどうかを取得
      
set copyEnabled to (enabled of menu item 5 of menu 1 of menu bar item 4 of menu bar 1)
      
if copyEnabled = false then return false
      
      
–コピーしてみる
      
try
        
        
keystroke “c” using {command down}
      on error
        –何らかの都合でコピー等の動作を行えなかった場合
        
return false
      end try
      
      
–ダイアログ表示時のボタンクリック
      
clickiCalDialog() of me
      
      
–クリップボードから文字情報を取得
      
set aConList to the clipboard
      
set aCon to aConList as string
      
    end tell
  end tell
  
  
set aList to parseByDelim(aCon, (ASCII character 13) & (ASCII character 13)) of me
  
  
return aList
  
end retSelectedEvent

–iCalでイベントをコピー時に、繰り返しイベントが選択されていた時に表示されるダイアログを乗り越えるためのルーチン
–(「キャンセル」ではない方のボタンを押す)
on clickiCalDialog()
  –ダイアログ検出
  
tell application “System Events”
    tell process “iCal”
      if (count every window) > 1 then
        if subrole of window 1 = “AXDialog” then
          tell window 1
            –キャンセル「ではない」方のボタンを取得
            
set bList to every button whose title is not equal to “キャンセル”
            
if bList is not equal to {} then
              set aButton to first item of bList
              
tell aButton
                click
              end tell
            end if
          end tell
        end if
      end if
    end tell
  end tell
end clickiCalDialog

on parseByDelim(aData, aDelim)
  set curDelim to AppleScript’s text item delimiters
  
set AppleScript’s text item delimiters to aDelim
  
set dList to text items of aData
  
set AppleScript’s text item delimiters to curDelim
  
return dList
end parseByDelim

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2008/12/11 iCal上で指定月の重複イベントを削除する

iCalデータが、さまざまなデバイスとのシンクロを繰り返した末に重複しまくってしまった、という友人から頼まれて作ったScriptです。

ical.jpeg

Script Menuなどに入れておいて実行し、実行対象月を入力すると(期間指定も可能)重複しているスケジュールを削除します。このあたり、iCalのAppleScript仕様がいまひとつ実用的でなく、「iCalで表示中の月」というのがAppleScript側から取得できないため、やむなくダイアログから入力してもらっています。

ical2.jpeg

対象月を入力すると重複イベントが削除されます。

ical3.jpeg

普通の環境であれば、「あっ!」という間に実行されてしまう程度のAppleScriptですが、残念なことに友人の環境ではデータ内容がすでにこわれかけていて、イベントの件数を求めたり1件削除するだけでも1日以上かかる状態に。

結局、このAppleScriptでは役に立たず、さらにiCalendarファイルを解析して重複イベントを削除するという、超強力で手の込んだものを作る羽目に。世界中探しまわっても、そこまで作り込んだAppleScriptにお目にかからなかったので、ちょっとした達成感はあったものの……友人にそのScriptを渡したところ、またスケジュールが重複したとか(汗) それはきっと、何か壊れているか、そうでなければたたられているに違いありません。

スクリプト名:iCal上で指定月の重複イベントを削除する v4
global g_tmpList, g_tmpList_ref

指定期間のイベントを取得
set todayDat to current date
set targYear to (year of todayDat) as string
set targMonth to (month of todayDat as number) as string
set aTarg to text returned of (display dialog 重複イベントの削除対象月は?(YYYY/MM) default answer (targYear & / & targMonth))
set {sDate, eDate} to getRangeFromDateText(aTarg) of me

with timeout of 3600 seconds
  tell application iCal
    set theEvents to properties of every event of every calendar whose start date is greater than sDate and end date is less than eDate and summary is not equal to “”
  end tell
end timeout

入り組んだリストをフラットなリストに
set g_tmpList to {} 初期化
set g_tmpList_ref to a reference to g_tmpList
makeFlatList(theEvents) of me

set dIDList to {}

with timeout of 3600 seconds
  tell application iCal
    repeat with i from 1 to (length of g_tmpList_ref)
      set anItem to first item of g_tmpList_ref
      
set anRec to {start date of anItem, end date of anItem, summary of anItem}
      
set g_tmpList to rest of g_tmpList
      
      
repeat with ii in g_tmpList_ref
        set bRec to {start date of ii, end date of ii, summary of ii}
        
if anRec = bRec then
          set anID to uid of ii
          
if anID is not in dIDList then
            display dialog ((”重複イベント” & return & (item 1 of anRec) as string) & return & (item 2 of anRec) as string) & return & item 3 of anRec
            
set the end of dIDList to anID
          end if
        end if
      end repeat
    end repeat
    
    
    
set delSucCount to 0
    
set delFailCount to 0
    
    
repeat with i in dIDList
      set delEvents to (every event of every calendar whose uid is equal to i)
      
makeFlatList(delEvents) of me
      
      
repeat with ii in g_tmpList_ref
        set aDelEv to contents of ii
        
if aDelEv is not equal to {} then
          try
            delete (contents of aDelEv)
            
set delSucCount to delSucCount + 1
          on error
            set delFailCount to delFailCount + 1
          end try
        end if
      end repeat
    end repeat
    
    
if delSucCount = 0 then
      display dialog 重複スケジュールは存在しませんでした。 buttons {”OK“} default button 1 with icon 1
    else
      set aMes to (delSucCount as string) & 件のスケジュールを削除。
      
(*
    if delFailCount is not equal to 0 then
      set aMes to aMes & “うち、” & (delFailCount as string) & “件の削除に失敗しました。”
    end if
    
*)
      display dialog aMes buttons {”OK“} default button 1 with icon 1
    end if
  end tell
end timeout

再帰でフラットなリストを作成する
on makeFlatList(aList)
  repeat with i in aList
    set aClass to (class of i) as string 2行に分けたり、ここでstringにcastしなかったりすると……AppleScript Studio環境に持っていったときに動かなくなる!!
    
if aClass = list then
      makeFlatList(i) of me
    else
      set the end of g_tmpList to (contents of i)
    end if
  end repeat
end makeFlatList

on getRangeFromDateText(aText)
  if aText does not contain then
    普通にYYYY/MM指定のみ行った場合
    
set sDate to (aText & /1“)
    
set eDate to (date sDate) + (getMlen(year of (date sDate), month of (date sDate)) of me) * days
    
set sDate to date sDate
    
  else
    期間を「YYYY/MM…YYYY/MM」  で指定した場合
    
set curDelim to AppleScript’s text item delimiters
    
set AppleScript’s text item delimiters to
    
set tList to text items of aText
    
set AppleScript’s text item delimiters to curDelim
    
    
set item1T to contents of item 1 of tList
    
set item2T to contents of item 2 of tList
    
    
set sDate to date retYYYYMDD(item1T) of me
    
set eDate1 to date retYYYYMDD(item2T) of me
    
set eDate to eDate1 + (getMlen(year of sDate, month of eDate1) of me) * days
  end if
  
  
return {sDate, eDate}
end getRangeFromDateText

on retYYYYMDD(aText)
  if aText contains / then
    set bText to aText & /1
  else
    set thisYear to (year of (current date)) as string
    
set bText to thisYear & / & aText & /1
  end if
  
return bText
end retYYYYMDD

指定月の長さを得る(日数)
on getMlen(aYear, aMonth)
  
  
set aYear to aYear as number
  
set aMonth to aMonth as number
  
  
set aDat to (aYear as text) & / & (aMonth as text) & /1
  
if aMonth is not equal to 12 then
    set eDat to ((aYear as text) & / & (aMonth + 1) as text) & /1
  else
    set eDat to ((aYear + 1) as text) & / & (1 as text) & /1
  end if
  
  
set sDat to date aDat
  
set eDat to date eDat
  
set eDat to eDat - 1
  
  
set mLen to day of eDat
  
return mLen
  
end getMlen

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2008/07/28 iCalに「日本の祝日」を追加

iCalに、「Japanese Holidays」というAppleが配布しているWebcalの登録確認を行い、登録していない場合には登録するという処理を行います。

一部、GUI Scriptingによる処理を含んでいるため、Mac OS X 10.6以降でそのまま動くという保証はまったくないのですが、自前で日本の祝日を計算しなくてもiCalに問い合わせるだけでいい(しかも、祝日が増えた場合でも自動対応してくれる)ため、実装実験を行ってみたものです。

結局、このルーチンは実戦投入されませんでしたが、何かの折に使ってみたいものです。

スクリプト名:iCalに日本の祝日を追加
set aRes to addJapaneseHolidays() of iCalLib

script iCalLib
  iCalに「日本の休日」のwebcalを追加する
  
on addJapaneseHolidays()
    OSバージョンの確認を行う
    
set v2 to system attribute sys2 > 4/5
    
if v2 < 4 then return false 10.3や10.2などであれば実行禁止
    
    
GUI scriptingの有効チェックを行う
    
set guiRes to retGUIScriptingEnabled() of me
    
if guiRes = false then return false
    
    
repeat
      using terms from application iCal
        tell application iCal
          set cList to name of every calendar
          
          
if Japanese Holidays is not in cList then
            インターネット接続確認
            
set a to connection_check() of me
            
if a = false then return false
            
            
ignoring application responses
              GetURL webcal://ical.mac.com/ical/Japanese32Holidays.ics
            end ignoring
          else
            return true
          end if
        end tell
      end using terms from
      
      
activate application iCal
      
tell application System Events
        tell process iCal
          tell window 1
            tell sheet 1
              click button 2
            end tell
            
            
repeat
              delay 1
              
tell sheet 1
                set bCount to count every button
                
if bCount is not equal to 1 then exit repeat
              end tell
            end repeat
            
            
tell sheet 1
              if v2 = 4 then
                Mac OS X 10.4の場合
                
set value of checkbox 1 to 1
                
set value of checkbox 2 to 1
                
                
click button 2 of list 1 OKボタン
              else if v2 = 5 then
                Mac OS X 10.5の場合
                
tell list 1
                  set value of checkbox 1 to 1
                  
set value of checkbox 2 to 1
                  
set value of checkbox 3 to 1
                end tell
                
                
click button 1 OK
                
              else
                return false 10.6などの想定外のバージョンであった場合    
              end if
            end tell
          end tell
        end tell
      end tell
    end repeat
  end addJapaneseHolidays
  
  
on connection_check()
    try
      tell application System Events
        do shell script sbin/ping -c 1 www.google.com
      end tell
      
set the connection_status to true
    on error
      set the connection_status to false
    end try
    
return connection_status
  end connection_check
  
  
GUI Scriptingの設定判定。10.4/10.5対応
  
on retGUIScriptingEnabled()
    set v2 to system attribute sys2 > 4, 5
    
    
Mac OS X 10.4以上なら実行
    
if v2 > 3 then
      tell application System Events
        set anUIe to UI elements enabled
      end tell
      
      
if anUIe = true then
        return true UI Element Scripting (GUI Scripting)がイネーブルならtrueを返す
      else
        beep
        
display dialog UI Element Scriptingが有効になっていません。 & return & return & 「システム環境設定」の「ユニバーサルアクセス」で、「補助装置を使用可能にする」のチェックボックスにチェックを入れてから再実行してください。 with icon stop
        
if button returned of result is OK then
          tell application System Preferences
            activate
            
set current pane to pane com.apple.preference.universalaccess
          end tell
          
return false
        end if
      end if
    end if
  end retGUIScriptingEnabled
end script

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2008/04/07 iCalでReadOnlyではないカレンダーの番号を取得

iCalのカレンダーのうち、ローカルで情報を持っているもの(書き換え可能)のほか、Webサーバー上からダウンロードしてくるもの(書き換え不可)があり、後者は書き換えできないのでAppleScriptで処理するには向いていないケースが多いので、処理対象から外すためにこのようなサブルーチンを作ったものと思われます。

スクリプト名:iCalでReadOnlyではないカレンダーの番号を取得

set iCalList to getWritableCalendarNo() of me

R/Oではないカレンダーの番号を取得
on getWritableCalendarNo()
  set aList to {}
  
tell applicationiCal
    set calCount to count every calendar
    
repeat with i from 1 to calCount
      set writableF to writable of calendar i
      
if writableF is equal to true then
        set the end of aList to i
      end if
    end repeat
  end tell
  
return aList
end getWritableCalendarNo

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

2008/03/28 1月分のワークログ作成(iCal連携Ver)

ワークログ作成3部作の最終バージョンです。結局、カレンダーどおりに休めるわけでもなく、休みの情報はiCal上に休日カレンダーを作成していたので、それを参照してワークログを作ってくれるようにしたのがこのバージョンです。かといって、それほど時間をかけて作ったわけでもなく……実に効率よく作成できました。

iCalに各日の休み情報を問い合わせると、たとえば31日ある月であれば31回iCalにリクエストを投げる必要があるわけで、アプリケーションへの問い合わせオーバーヘッドを考えるとあまり得策とはいえません。AppleScriptそのものの処理速度はそんなに遅くはないのですが、アプリケーションとの通信を行うとずいぶん待たされます。そのため、本Scriptでは1か月分をまるごと問い合わせて、31回問い合わせるべきところを1回で済ませています。そのことで、初期試作バージョンよりも大幅に処理速度を向上させた次第です。

(more…)