福祉支援サービス コミル
コミルは障害をお持ちの方の生活をモノの工夫によって豊かにすることをお手伝いします。
コミルは障害をお持ちの方の生活をモノの工夫によって豊かにすることをお手伝いします。
コミルは障害をお持ちの方の生活をモノの工夫によって豊かにすることをお手伝いします。
スケジュール管理は、Lightning で行っています。 カレンダーファイルは自分のPC内には置かず、LAN内の WebDAV サーバにおいた ics ファイルを Lightning で閲覧、編集するようにしています。 この ics ファイルは PHP iCalendar でLANで共有閲覧できるようにしており、家族のPCからも私のスケジュールの閲覧が出来るようにしています。
さて、このサーバ上の ics ファイルをサーバのコマンドラインから直接閲覧、編集できるようにしました。そうした理由は外出先からiモードメールでスケジュール閲覧、編集できるようにすることなのですが、メールとのインターフェースは後日追って記します。
コマンドラインの書式は以下。
lsch l[ist] [開始日時[#終了日時]] lsch c[reate] 開始日時[#終了日時] % 概要[@場所][-詳細] lsch s[earch] 検索文字列 開始日時、終了日時のフォーマット : *a a日後,a日間,a時間 a..aa a日,a時 abb..aabb a月b日,a時b分 a/b..aa/bb a月b日 a-b..aa-bb a月b日 a:b..aa:bb a時b分 abbcc..aabbcc a月b日c時 a/b/c..aa/bb/cc a年b月c日 a-b-c..aa-bb-cc a年b月c日 abbccdd..aabbccdd a月b日c時d分
開始/終了日時のフォーマットは、全て携帯電話のテンキーから打てる文字で指定できるように考案しています。
また、オプションがList(閲覧)、Create(追加)、Search(検索)しかなく、削除や変更がないのは、敢えて限られたインターフェースの中でリスクのあるオプションを盛り込みたくなかったからです。 削除や編集を行いたければ、帰社後 Lightning から行えばいいだけですのでね。
icsファイルをいじるRubyライブラリは、iCalendar を呼び出しています。適宜 RubyGems などでインストールして下さい。
ソースは以下をご覧下さい。
#!/usr/bin/env ruby # #= コマンドラインでスケジュール管理 # # コマンドライン操作で # スケジュールリストを表示もしくはスケジュールを登録する。 # ライブラリを含む # #ver.0.1.0:: 2008/09/ # #copyright:: Hiroyuki Mabuchi @ Comil[http://www.comil.jp/] require 'rubygems' require 'icalendar' require 'date' #= 文字列でスケジュール管理をするためのクラス # #Lsch.new(path):: iCalファイルのパスを与えてインスタンスを作成 #Lsch::list_event(string):: 開始終了文字列からイベントの配列を返す #Lsch::create_event(string):: イベントを登録する #Lsch::search_event(string):: 文字列を含むイベントの配列を返す class Lsch # Time Zone Offset (+9:00:00 Japan) TZ = Rational(9,24) # 出力範囲月日の初期値 Def_date_range = 14 Def_start_date = Date.today Def_end_date = Date.today + Def_date_range def initialize(icalpath) # icalpath:: iCalファイルのパス @icalpath = icalpath end def path @icalpath end # *n def self.parse_asterisk_num(pre = nil, res = nil, *par) if pre.instance_of?(DateTime) # start日時が既にある # start日時のn時間後を返す dt, hour = Date.new(pre.year, pre.mon, pre.day), pre.hour hour += par[0].to_i if hour >= 24 hour -= 24 dt = dt + 1 end res = DateTime.new(dt.year, dt.mon, dt.day, hour, pre.min, pre.sec, TZ) elsif pre.instance_of?(Date) # start日が既にある # start日のn日後を返す res = pre + par[0].to_i else # fromの解析である # 今日からn日後を返す res = Date.today + par[0].to_i end res end # 7..8桁の数 def self.parse_8figures(pre = nil, res = nil, *par) # 月 日 時 分 を返す year = Date.today.year year += 1 if Date.new(year, par[0].to_i, par[1].to_i) < Date.today res = DateTime.new(year, par[0].to_i, par[1].to_i, par[2].to_i, par[3].to_i, 0, TZ) res end # / もしくは - 入り7..8桁の数 def self.parse_8figures_w_sla(pre = nil, res = nil, *par) # 年 月 日 を返す Date.new(par[0].to_i, par[1].to_i, par[2].to_i) end # 5..6桁の数 def self.parse_6figures(pre = nil, res = nil, *par) # 月 日 時 を返す year = Date.today.year year += 1 if Date.new(year, par[0].to_i, par[1].to_i) < Date.today DateTime.new(year, par[0].to_i, par[1].to_i, par[2].to_i, 0, 0, TZ) end # / もしくは - 入り5..6桁の数 def self.parse_6figures_w_sla(pre = nil, res = nil, *par) # 年 月 日 を返す Date.new((par[0].to_i)+2000, par[1].to_i, par[2].to_i) end # 3..4桁の数 def self.parse_4figures(pre = nil, res = nil, *par) if pre.instance_of?(DateTime) # start が日時なら 同日の 時 分 を返す res = DateTime.new(pre.year, pre.mon, pre.day, par[0].to_i, par[1].to_i, 0, TZ) # 返す日時がstartより過去なら翌日の時分を返す if res < pre nd = Date.new(res.year, res.mon, res.day).next res = DateTime.new(nd.year, nd.mon, nd.day, res.hour, res.min, 0, TZ) end elsif res.instance_of?(Date) # 先に解析された結果が日なら 時,分 を加える res = DateTime.new(res.year, res.mon, res.day, par[0].to_i, par[1].to_i, 0, TZ) else # start が月日もしくは未定義、もしくは先に解析された結果が日でないなら月日を返す # 返す 日 が過去なら、来年同日を返す res = Date.new(Date.today.year, par[0].to_i, par[1].to_i) res = Date.new(res.year + 1, res.mon, res.day) if res < Date.today end res end # / もしくは - 入り 3..4桁の数 def self.parse_4figures_w_sla(pre = nil, res = nil, *par) # 月日を返す # 返す 日 が過去なら、来年同日を返す res = Date.new(Date.today.year, par[0].to_i, par[1].to_i) res = Date.new(res.year + 1, res.mon, res.day) if res < Date.today res end # : 入り3..4桁の数 def self.parse_4figures_w_colon(pre = nil, res = nil, *par) if pre.instance_of?(DateTime) # start が日時なら 同日の 時 分 を返す res = DateTime.new(pre.year, pre.mon, pre.day, par[0].to_i, par[1].to_i, 0, TZ) # 返す日時がstartより過去なら翌日の時分を返す if res < pre nd = Date.new(res.year, res.mon, res.day).next res = DateTime.new(nd.year, nd.mon, nd.day, res.hour, res.min, 0, TZ) end elsif res.instance_of?(Date) # 先に解析された結果が日なら 時,分 を加える res = DateTime.new(res.year, res.mon, res.day, par[0].to_i, par[1].to_i, 0, TZ) else # start が月日もしくは未定義、もしくは先に解析された結果が日でないなら今日の時分を返す res = DateTime.new(Date.today.year, Date.today.mon, Date.today.day, par[0].to_i, par[1].to_i, 0, TZ) # 返す日時が今より過去なら翌日の時分を返す if res < DateTime.now nd = Date.new(res.year, res.mon, res.day).next res = DateTime.new(nd.year, nd.mon, nd.day, res.hour, res.min, 0, TZ) end end res end # 1..2桁の数 def self.parse_2figures(pre = nil, res = nil, *par) if pre.instance_of?(DateTime) # start が日時なら 同日の 時 を返す res = DateTime.new(pre.year, pre.mon, pre.day, par[0].to_i, 0, 0, TZ) # 返す日時がstartより過去なら翌日の時を返す if res < pre nd = Date.new(res.year, res.mon, res.day).next res = DateTime.new(nd.year, nd.mon, nd.day, res.hour, res.min, 0, TZ) end elsif res.instance_of?(Date) # 先に解析された結果が日なら 時 を加える res = DateTime.new(res.year, res.mon, res.day, par[0].to_i, 0, 0, TZ) elsif pre.instance_of?(Date) # start が月日ならstart月n日を返す # 返す 日 がstartより過去なら、start以降のn日を返す res = Date.new(pre.year, pre.mon, par[0].to_i) res = res >> 1 if res < pre else # start が未定義、もしくは先に解析された結果が日でないなら今月n日を返す # 返す 日 が過去なら、来月n日を返す res = Date.new(Date.today.year, Date.today.mon, par[0].to_i) res = res >> 1 if res < Date.today end res end # 日付、時刻指示の文字列を解析する # str:: 解析対象文字列 # pre:: 直前の解析結果の日時(startの日時) # 戻り値:: 解析結果の日時 def self.parse_date_line(str, pre = nil) res = nil while /^\s*$/ !~ str case str when /^\s*\*(-?\d+)(.*)/ # *n 形式 res = self.parse_asterisk_num(pre, res, $1) # 解析済み部分を削除 str = $2 when /\s*(\d{1,2})(\d{2})(\d{2})(\d{2})(.*)/ # 7..8桁の数 res = self.parse_8figures(pre, res, $1, $2, $3, $4) # 解析済み部分を削除 str = $5 when /\s*(\d{3,4})[\/\-](\d{1,2})[\/\-](\d{1,2})(.*)/ # / もしくは - 入り5..8桁の数 res = self.parse_8figures_w_sla(pre, res, $1, $2, $3) # 解析済み部分を削除 str = $4 when /\s*(\d{1,2})(\d{2})(\d{2})(.*)/ # 5..6桁の数 res = self.parse_6figures(pre, res, $1, $2, $3) # 解析済み部分を削除 str = $4 when /\s*(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{1,2})(.*)/ # / もしくは - 入り3..6桁の数 res = self.parse_6figures_w_sla(pre, res, $1, $2, $3) # 解析済み部分を削除 str = $4 when /\s*(\d{1,2})(\d{2})(.*)/ # 3..4桁の数 res = self.parse_4figures(pre, res, $1, $2) # 解析済み部分を削除 str = $3 when /\s*(\d{1,2})[\/\-](\d{1,2})(.*)/ # / もしくは - 入り 2..4桁の数 res = self.parse_4figures_w_sla(pre, res, $1, $2) # 解析済み部分を削除 str = $3 when /\s*(\d{1,2}):(\d{1,2})(.*)/ # : 入り2..4桁の数 res = self.parse_4figures_w_colon(pre, res, $1, $2) # 解析済み部分を削除 str = $3 when /\s*(\d{1,2})(.*)/ # 1..2桁の数 res = self.parse_2figures(pre, res, $1) # 解析済み部分を削除 str = $2 else if pre.instance_of?(DateTime) res = DateTime.parse(str) else res = Date.parse(str) end end end res end # 開始および終了日付、時刻指示の文字列を解析する # str:: 解析対象文字列 # 戻り値:: 解析結果の日時の配列 def self.parse_from_to(str) par = str.split(/#/) return nil unless par[0] from = self.parse_date_line(par[0]) if par[1] # toがある to = self.parse_date_line(par[1], from) else # toがない to = nil end return from, to end # 開始/終了月日からイベントをリストアップする # range_start:: 開始/終了月日のコマンド文字列もしくは開始月日 # range_end:: 終了月日 # 戻り値:: イベントの配列 def list_event(range_start, range_end = nil) # 引数が文字列の時は解析変換 if range_start.instance_of?(String) range_start, range_end = Lsch.parse_from_to(range_start) end # 範囲が指定されていない時は初期値 range_start = Def_start_date unless range_start range_end = range_start + Def_date_range unless range_end # 範囲が DateTime 型なら Date 型に変換 (時刻は無視する) range_start = range_start.to_date if range_start.instance_of?(DateTime) range_end = range_end.to_date if range_end.instance_of?(DateTime) range_start = range_start.to_datetime range_end = range_end.to_datetime # 戻り値 res = [] # イベント抽出 File.open(@icalpath) do |cal_file| cals = Icalendar::parse(cal_file) cal = cals.each do |one_cal| # イベント毎に戻り値配列に格納 one_cal.events.each do |event| e_start = event.start e_end = event.end e_end = e_start unless e_end e_start = e_start.add_tz e_end = e_end.add_tz next if e_end <= range_start or e_start >= range_end + 1 res << event end end end # 戻り値を日付順にソートして返す res.sort do |a, b| a.dtstart <=> b.dtstart end end # イベントを登録する # event_start:: 開始/終了日時のコマンド文字列もしくは開始日時 # event_end:: 終了日時 # summary:: 件名 # location:: 場所 # description:: 詳細 # 戻り値:: イベントの配列 def create_event(event_start, event_end = nil, summary = nil, location = nil, description = nil) # 引数が文字列の時は解析変換 if event_start.instance_of?(String) event_start, event_end = Lsch.parse_from_to(event_start) end event_end = event_start unless event_end # 終日イベントなら終了を翌日にする if event_start.instance_of?(Date) event_end = event_end + 1 end cal = Icalendar::Calendar.new cal.event do dtstart event_start dtend event_end summary summary location location if location description description if description end File.open(@icalpath, "a+b") do |f| f.write(cal.to_ical) f.flush end end # イベントを検索する # 検索対象は、summary, location, description # tarm:: 検索する文字列 # 戻り値:: ヒットしたイベントの配列 def search_event(tarm) # 戻り値 res = [] # イベント抽出 File.open(@icalpath) do |cal_file| cals = Icalendar::parse(cal_file) cal = cals.each do |one_cal| # イベント毎に戻り値配列に格納 one_cal.events.each do |event| if /#{tarm}/ =~ event.summary res << event elsif /#{tarm}/ =~ event.location res << event elsif /#{tarm}/ =~ event.description res << event end end end end # 戻り値を日付順にソートして返す res.sort do |a, b| a.dtstart <=> b.dtstart end end end # Date クラスにメソッドを追加 class Date # Time Zone Offset (+9:00:00 Japan) TZ = Rational(9,24) # Date クラスのオブジェクトを DateTime クラスに変換する # 時刻は 00:00:00+9:00:00 とする def to_datetime DateTime.new(self.year, self.mon, self.day, 0, 0, 0, TZ) end # Date クラスのオブジェクトを DateTime クラスに変換する # to_datetime の別名 def to_datetime_begin to_datetime end # Date クラスのオブジェクトを DateTime クラスに変換する # 時刻は 23:59:59+9:00:00 とする def to_datetime_end DateTime.new(self.year, self.mon, self.day, 23, 59, 59, TZ) end end # DateTime クラスにメソッドを追加 class DateTime # Time Zone Offset (+9:00:00 Japan) TZ = Rational(9,24) # DateTime クラスのオブジェクトを Date クラスに変換する # 時刻は切り捨てる def to_date Date.new(self.year, self.mon, self.day) end # タイムゾーンを加える # 時刻の数値は変更しない点が new_offset とは異なる def add_tz DateTime.new(self.year, self.mon, self.day, self.hour, self.min, self.sec, TZ) end end # iCalファイルのパス iCalFile = "/var/www/dav/mycalender.ics" # 曜日出力文字 Wdays = %w[日 月 火 水 木 金 土] # 全日イベントかどうか def full_day(event) s = event.start e = event.end e = s unless e s != e && s.hour == 0 && s.min == 0 && e.hour == 0 && e.min == 0 end # 複数日イベントかどうか def multi_day(event) full_day(event) && (event.start + 1 != event.end) end # 日付の出力形式 def date_str(d) month = sprintf("%2d", d.month) day = sprintf("%2d", d.day) wday = Wdays[d.wday] "#{month}/#{day}(#{wday})" end # 時刻の出力形式 def time_str(d) return "*****" unless d hour = sprintf("%2d", d.hour) min = sprintf("%02d", d.min) "#{hour}:#{min}" end # イベントの表示 def list(icalpath, dates_line, to = nil) res = "" # iCalから出力 Lsch.new(icalpath).list_event(dates_line, to).each do |event| start_date = date_str(event.start) end_date = multi_day(event) ? date_str(event.end - 1) : "" start_time = full_day(event) ? "" : time_str(event.start) end_time = full_day(event) ? "" : time_str(event.end) location = event.location summary = event.summary res << "#{start_date} #{start_time}-#{end_date} #{end_time},#{summary},#{location}\n" end res end # イベントの登録 def create(icalpath, dates_line, dest_line) res = "" from, to = Lsch.parse_from_to(dates_line) if /(.*)@(.*)/ =~ dest_line summary = $1 dest_line = $2 if /(.*)\-(.*)/ =~ dest_line location = $1 description = $2 else location = dest_line end else if /(.*)\-(.*)/ =~ dest_line summary = $1 description = $2 else summary = dest_line end end res << "Start: #{from} - End:#{to}\n" res << "Summary: #{summary}\n" res << "Location: #{location}\n" res << "Description: #{description}\n" Lsch.new(icalpath).create_event(from, to, summary, location, description) res << "Created !\n" res end # イベントの検索 def search(icalpath, tarm) res = "" # iCalから出力 Lsch.new(icalpath).search_event(tarm).each do |event| start_date = date_str(event.start) end_date = multi_day(event) ? date_str(event.end - 1) : "" start_time = full_day(event) ? "" : time_str(event.start) end_time = full_day(event) ? "" : time_str(event.end) location = event.location summary = event.summary res << "#{start_date} #{start_time}-#{end_date} #{end_time},#{summary},#{location}\n" end res end usage = <<EOS lsch l[ist] [start[#end]] lsch c[reate] start[#end] % summary[@location][-description] lsch s[earch] search_tarm start, end format : *a a日後,a日間,a時間 a..aa a日,a時 abb..aabb a月b日,a時b分 a/b..aa/bb a月b日 a-b..aa-bb a月b日 a:b..aa:bb a時b分 abbcc..aabbcc a月b日c時 a/b/c..aa/bb/cc a年b月c日 a-b-c..aa-bb-cc a年b月c日 abbccdd..aabbccdd a月b日c時d分 EOS # コマンドライン時 if $0 == __FILE__ if ARGV.size == 0 print usage else command = ARGV.shift line = ARGV.join(' ') # 構文エラーに対して例外補足 begin case command when /^l(ist)?$/i print list(iCalFile, line) when /^c(reate)?$/i lines = line.split(/%/) dates_line = lines.shift dates_line = "" unless dates_line dest_line = lines.join dest_line = "" unless dest_line print create(iCalFile, dates_line, dest_line) print list(iCalFile, dates_line) when /^s(earch)?$/i print search(iCalFile, line) else print usage end rescue => evar print "構文エラー:#{evar}\n" print usage end end end
なお、適宜変数 iCalFile の示すパスを変更して下さい。
blog comments powered by Disqus