iCal からエキスポートされる .ics テキストを加工する Perl スクリプト


**動機1:** iCal からエキスポートされる .ics を .csv にしたい
**動機2:** Perl を覚えて何かしてみたい

**課題1:** .ics は同一で別の目的のものがあるかと思えば見出し以外の情報が入っていたり場当たり的とも思えるものがある
**課題2:** 改行付き DESCRIPTION:項目 の扱い

- perl での .ics の扱いついて参考:
[**Parsing iCal Data** www.perl.com](http://www.perl.com/pub/2005/08/18/ical_dot.html)  
- perl について参考:
[**多次元ハッシュ** www.kent-web.com](http://www.kent-web.com/perl/chap4.html#chap4_6)  
- 使用した perl のバージョン: 
perlv5.18.2 built for darwin-thread-multi-2level


**方針と結果:** ファイルを読み込みながらの逐次処理でいけるとは思うけれど、全データを格納して一括して書き出した。対話的に検索するとか拡張したら面白い？


データ構造：{カレンダ}->{イベント}->{項目} の 3層のハッシュ
注意した： .ics は一つのイベントに同じタグで用途の項目を含む  

**== コード ============**

	#!/usr/bin/perl
	#
	# 概要：iCal からエキスポートされる .ics 形式のテキストファイルを読み込み加工する
	# 使用方法 >Puse_iCal.pl [ファイル名 ファイル名 ...]
	# 課題：コマンドラインオプション（やる気ない）
	#
	
	use constant { 
		CONST_CARD => 0,		# 複数行/1イベント <- 読み込みの確認用に作った処理　イベントの項目がハッシュに格納されるのでイベントとイベント内の項目の出力順序は不定となる
		CONST_CSV => 1,			# 1行/1イベント(csv)　イベント内の順序は決め打ち、出力後か処理を足せばソートは簡単
		CONST_TRUE => -1,
		CONST_FALSE => 0
	};
	$order_output = CONST_CSV; # CONST_CSV:csv出力　CONST_CARD:１イベント複数行
	
	$calenderHash = {};
	
	#
	# main
	#
	# 引数から複数ファイル毎に処理
	foreach my $name_file (@ARGV){
	
		# 引数からファイルを開く
		open(FILE, $name_file) or die "Can't open $name_file: $!\n" if $ARGV[0];	
		# 俺へ : ファイルチェックでは優先順位の低い 'or' 演算子を使うこと。 '||' は、他の演算子より優先順位が高いので先に処理されてしまう。場合によって不具合が発生する。
	
		$eventHash = $calenderHash->{$name_file} = {}; # ハッシュに初期値を代入
	
		while (<FILE>){
			SWITCH: {
				# イベント始め
				if ( /^BEGIN:VEVENT/ ) {
					## We have a new event, so start fresh.
					$elementHash = {};
					$key_prev = "";
					$value_prev = "";
					$description = CONST_FALSE;
					last SWITCH; 
				}# end if
				# イベント終り
				if ( /^END:VEVENT/ ) {
					$eventHash->{$elementHash->{'UID'}} = $elementHash;
					last SWITCH;
				}# end if
			
				#タグの取得
				($key_common, $value_common) = split(":", $_, 2);	
				# 例外的なタグのキー生成 / それ以外はタグの名前をそのままハッシュのキーにする
				SWITCH2:{
					if($key_common =~ /^DESCRIPTION/){	# DESCRIPTION 処理の開始
						$description = CONST_TRUE;				# DESCRIPTION は改行が入る場合があるため
						last SWITCH2;
					}
					if($key_common =~ /^BEGIN:VALARM/){				# DESCRIPTION 処理の終了 
						$description = CONST_FALSE;
						last SWITCH2;
					}
					if($description == CONST_TRUE){
						$value_common = $value_prev.$key_common;
						$value_common =~ s/[\r\n]/\\n/g; #データの CR/LF を文字化
					$key_common = $key_prev;
					}
					if($key_common =~ /^DTSTART;/){		# '=~' is pattern match of except for $_
						$key_common = "DTSTART;";
						last SWITCH2;
					}# end if
					if($key_common =~ /^DTEND;/){			# 
						$key_common = "DTEND;";
						last SWITCH2;
					}# end if
					if($value_common =~ /^VALARM/){ 		# BEGIN:VALARM & END:VALARM
						$key_common = join(":", $key_common, "VALARM");
						last SWITCH2;
					}# end if
				}# end SWITCH2
				$key_common =~ s/[\r\n]//g; 	#タグから CR/LF を削除
				$value_common =~ s/[\r\n]//g; #データから CR/LF を削除
				$elementHash->{$key_common} = $value_common;
				$key_prev = $key_common;
				$value_prev = $value_common;
				last SWITCH;
			} # end of SWITCH
		} # end of while(<FILE>)
	}
	printCalendersAll();
	
	#
	# 全カレンダーの出力
	#
	sub printCalendersAll {
	print "Called printCalenderAll\n";
		while ((my $name_calender, my $event_hash) = each %$calenderHash){
			printCalender($event_hash, $name_calender);	
		}
	}
	
	
	#
	# 1 カレンダーの出力 1行/複数行　
	#
	sub printCalender{
		my $events = @_[0];
		my $name_file = @_[1];
	
		#
		while ((my $key_event, my $value_event) = each %$events){
			# output
			if($order_output == CONST_CSV){
					# output selected and processed members of $elementHash
					printCSV($value_event, $name_file);
			} else {
				#	output all rare members of $eventHash with tag
				while ((my $key_tag, my $value) = each %$value_event) {
						print "\|$key_tag\|$value\n";
					}
			}
			print "\n";
		}# end foreach $key_event
	}# end sub
	
	#
	# CSV形式出力
	#
	sub printCSV{
		my $hash_aEvent = @_[0];
		my $name_file = @_[1];
	
		print
			formatDateTime(removeCRLF($value = $hash_aEvent->{'DTSTART;'})), ",",
			formatDateTime(removeCRLF($value = $hash_aEvent->{'DTEND;'})), ",",
	#		formatDateTime(removeCRLF($value = $hash_aEvent->{'DTSTAMP'})), ",",
			removeCRLF($value = $hash_aEvent->{'LOCATION'}), ",",
			"\"", removeCRLF($value = $hash_aEvent->{'SUMMARY'}), "\"", ",",
			"\"", removeCRLF($value = $hash_aEvent->{'DESCRIPTION'}), "\"", ",",
			removeCRLF($value = $hash_aEvent->{'START'}), ",",
			removeCRLF($value = $hash_aEvent->{'END'}), ",",
			removeCRLF($value = $hash_aEvent->{'DURATION'}), ",",
			removeCRLF($value = $hash_aEvent->{'SEQUENCE'}), ",",
	#		removeCRLF($value = $hash_aEvent->{'URL'}), ",",
	#		removeCRLF($value = $hash_aEvent->{'UID'}), ",",
			"$name_file"
			;
	
	}# end sub
	
	#
	# 行から CR&LF を削除する
	#
	sub removeCRLF{
		my $strings = $_[0];
		$strings =~ s/[\r\n]//g;
		return $strings;
	}# end sub
	
	#
	# .ics の日付・時刻形式を YYYY/MM/DD hh:mm:ssX に変換する
	#
	sub formatDateTime{
		my $datetime = $_[0];
		$datetime =~ s/^(\d{4})(\d{2})(\d{2})/$1\/$2\/$3/g;
		if (length($datetime) == (8 + 2 + 7)) {								# example 20090711T090000
			$datetime =~ s/T(\d{2})(\d{2})(\d{2})/ $1:$2:$3/g;
		} elsif (length($datetime) == (8 + 2 + 7 + 1)) { 			# example 20090711T090000Z
			$datetime =~ s/T(\d{2})(\d{2})(\d{2}\w)/ $1:$2:$3/g;
		} elsif (length($datetime) == (8 + 2)) {
			$datetime =~ s/^(\d{4}\/\d{2}\/\d{2})/$1         /g; # example 20090711
		} else {
			$datetime = length($datetime);
		} # end if
	
		return $datetime;
	}