2016/02/18(木)Google Calendar のデータをXMLに加工して monthly.js に 流し込む

所属している草野球チームのHP制作を担当していて、「スケジュールを載せたい」という要望が来ました。数週間ぐらい前に「そういえば何かカレンダーをキレイに表示させるJSライブラリがあったよなぁ」と「monthly.js」思い出して、Googleカレンダーとこれを使って表示させることにしました。

練習場所まで全世界に公開するのも気持ちが悪いので、Googleのカレンダーは「非公開」の設定のままで、どうにか情報を取得するすべはないものかと調べると、OAuth2なGoogle Calendar API というAPIを使えばいいと載っていました。( OAuth2じゃないAPIもあるっぽいですが、実装が複雑になるのでOAuth2の方式がいいよみたいな情報が公式ドキュメントに書かれていた。うろ覚え。)

しかし、肝心のPerlの解説がない。(仕事ではRubyがメインですが、草野球のホームページぐらいの規模ならPerlで書きたい。)ライブラリ自体は「Google::API::Client」という名前でCPANに登録されているのでこれを使うことにしました。

client_secret.json みたいなファイルをGoogle APIのページから取得すると(詳細は「celient_secret.json google」でググッて)あとは、以下のコードで、初回時のみ手動で認証してリフレッシュトークンを取得すると、それ以降はよしなにしてくれます。(ずっとアクセスを待ち受ける必要があるWebアプリにも組み込めます。)

#!/usr/bin/env perl

use strict;
use warnings;
use utf8;
use feature qw/say/;
use open qw/:encoding(utf-8) :std/;
use JSON qw//;
use Time::Moment qw//;
use Google::API::Client;
use Google::API::OAuth2::Client;
use XML::Simple ();
use Cache::FileCache;
use DDP; # cpanm Data::Printer してね

my $myemail = 'xxx@gmail.com';
my $client_secrets_file = 'client_secret.json';
my $client = Google::API::Client->new;
my $gcal = $client->build('calendar', 'v3');
my $auth_driver = Google::API::OAuth2::Client->new_from_client_secrets($client_secrets_file, $gcal->{auth_doc});

my $dat_file = 'token.dat';
get_or_restore_token($dat_file, $auth_driver);

my $my_calender_id      = 'primary';
my $holiday_calendar_id = 'ja.japanese#holiday@group.v.calendar.google.com';

my $cache = Cache::FileCache->new({
    cache_root => '/tmp',
    namespace  => 'calendar',
});

my %cache_duration_of = (
    $my_calender_id      => '5s',
    $holiday_calendar_id => '7d',
);

my $now = Time::Moment->now;

my $first_day_of_month = Time::Moment->new(
    year   => $now->year,
    month  => $now->month,
    day    => 1,
    offset => 540, # +09:00(540分)
);

my $req_body = {
    timeMax => $first_day_of_month->plus_months(6)->to_string,
    timeMin => $first_day_of_month->minus_months(6)->to_string,
};

my $schedule = get_or_cache_event_items($my_calender_id);
my $holidays = get_or_cache_event_items($holiday_calendar_id);

my @events = (@{$schedule->{items}}, @{$holidays->{items}});

print make_xml(\@events);

exit;

sub get_or_restore_token
{
    my ($file, $auth_driver) = @_;

    if (-f $file)
    {
        open (my $fh, '<', $file) or die $!;
        local $/;
        my $access_token = JSON::from_json(<$fh>);
        close($fh);

        $auth_driver->token_obj($access_token);
    }
    else
    {
        my $auth_url = $auth_driver->authorize_uri;
        say 'Go to the following link in your browser:';
        say $auth_url;

        say 'Enter verification code:';
        chomp(my $code = <>);
        $auth_driver->exchange($code);
        store_token($file, $auth_driver);
    }
}

sub store_token
{
    my ($file, $auth_driver) = @_;

    my $access_token = $auth_driver->token_obj;

    open (my $fh, '>', $file) or die $!;
    print {$fh} JSON::to_json($access_token);
    close($fh);
}

sub get_or_cache_event_items
{
    my $calendar_id = shift;
    my $items = $cache->get($calendar_id);

    if ( ! $items )
    {
        $items = $gcal->events->list(calendarId => $calendar_id, body => $req_body)->execute({ auth_driver => $auth_driver });
        $cache->set($calendar_id => $items, $cache_duration_of{$calendar_id}) if $items;
    }

    return $items;
}

sub get_date_using_datetime { local $_ = $_[0]; s/(.+)T.+/$1/; $_; }
sub get_time_using_datetime { local $_ = $_[0]; s/.+T([0-9][0-9]:[0-9][0-9]):[0-9][0-9]\+.+/$1/; $_; }

sub make_xml
{
    my $items = shift;

    my $data = {};
    $data->{event} = ();

    my $i = 0;

    for my $item (@{$items})
    {
        my $event = {};

        my $startdate = $item->{start}{date} ? $item->{start}{date} : get_date_using_datetime($item->{start}{dateTime});
        my $enddate   = $item->{end}{date}   ? $item->{end}{date}   : get_date_using_datetime($item->{end}{dateTime});

        $event->{id}        = ++$i;
        $event->{name}      = $item->{summary};
        $event->{startdate} = $startdate;
        $event->{enddate}   = $enddate;

        $event->{color} = '#ffb128';
        $event->{color} = '#ddeeff' if $item->{organizer}{email} ne $myemail;

        $event->{starttime} = get_time_using_datetime($item->{start}{dateTime}) if $item->{start}{dateTime};
        $event->{endtime}   = get_time_using_datetime($item->{end}{dateTime})   if $item->{end}{dateTime};

        push(@{$data->{event}}, $event);
    }

    return XML::Simple::XMLout($data, RootName => 'monthly', XMLDecl => '<?xml version="1.0"?>', NoAttr => 1);
}

以下のようにXMLが出力されます。

<?xml version="1.0"?>
<monthly>
  <event>
    <name>練習(雨天中止)</name>
    <color>#ffb128</color>
    <enddate>2016-02-14</enddate>
    <endtime>15:30</endtime>
    <id>1</id>
    <startdate>2016-02-14</startdate>
    <starttime>11:00</starttime>
  </event>
  <event>
    <name>試合!</name>
    <color>#ffb128</color>
    <enddate>2016-03-26</enddate>
    <endtime>16:00</endtime>
    <id>2</id>
    <startdate>2016-03-26</startdate>
    <starttime>14:00</starttime>
  </event>
  <event>
    <name>グランド練習</name>
    <color>#ffb128</color>
    <enddate>2016-02-20</enddate>
    <endtime>16:00</endtime>
    <id>3</id>
    <startdate>2016-02-20</startdate>
    <starttime>14:00</starttime>
  </event>
  <event>
    <name>練習</name>
    <color>#ffb128</color>
    <enddate>2016-02-28</enddate>
    <endtime>15:00</endtime>
    <id>4</id>
    <startdate>2016-02-28</startdate>
    <starttime>10:00</starttime>
  </event>
  <event>
    <name>練習</name>
    <color>#ffb128</color>
    <enddate>2016-03-06</enddate>
    <endtime>15:00</endtime>
    <id>5</id>
    <startdate>2016-03-06</startdate>
    <starttime>10:00</starttime>
  </event>
  <event>
    <name>練習</name>
    <color>#ffb128</color>
    <enddate>2016-03-13</enddate>
    <endtime>15:00</endtime>
    <id>6</id>
    <startdate>2016-03-13</startdate>
    <starttime>10:00</starttime>
  </event>
  <event>
    <name>練習</name>
    <color>#ffb128</color>
    <enddate>2016-04-03</enddate>
    <endtime>15:00</endtime>
    <id>7</id>
    <startdate>2016-04-03</startdate>
    <starttime>10:00</starttime>
  </event>
  <event>
    <name>練習</name>
    <color>#ffb128</color>
    <enddate>2016-03-20</enddate>
    <endtime>15:00</endtime>
    <id>8</id>
    <startdate>2016-03-20</startdate>
    <starttime>10:00</starttime>
  </event>
  <event>
    <name>敬老の日</name>
    <color>#ddeeff</color>
    <enddate>2015-09-22</enddate>
    <id>9</id>
    <startdate>2015-09-21</startdate>
  </event>
  <event>
    <name>国民の休日</name>
    <color>#ddeeff</color>
    <enddate>2015-09-23</enddate>
    <id>10</id>
    <startdate>2015-09-22</startdate>
  </event>
  <event>
    <name>秋分の日</name>
    <color>#ddeeff</color>
    <enddate>2015-09-24</enddate>
    <id>11</id>
    <startdate>2015-09-23</startdate>
  </event>
  <event>
    <name>文化の日</name>
    <color>#ddeeff</color>
    <enddate>2015-11-04</enddate>
    <id>12</id>
    <startdate>2015-11-03</startdate>
  </event>
  <event>
    <name>元日</name>
    <color>#ddeeff</color>
    <enddate>2016-01-02</enddate>
    <id>13</id>
    <startdate>2016-01-01</startdate>
  </event>
  <event>
    <name>昭和の日</name>
    <color>#ddeeff</color>
    <enddate>2016-04-30</enddate>
    <id>14</id>
    <startdate>2016-04-29</startdate>
  </event>
  <event>
    <name>憲法記念日</name>
    <color>#ddeeff</color>
    <enddate>2016-05-04</enddate>
    <id>15</id>
    <startdate>2016-05-03</startdate>
  </event>
  <event>
    <name>体育の日</name>
    <color>#ddeeff</color>
    <enddate>2015-10-13</enddate>
    <id>16</id>
    <startdate>2015-10-12</startdate>
  </event>
  <event>
    <name>みどりの日</name>
    <color>#ddeeff</color>
    <enddate>2016-05-05</enddate>
    <id>17</id>
    <startdate>2016-05-04</startdate>
  </event>
  <event>
    <name>海の日</name>
    <color>#ddeeff</color>
    <enddate>2016-07-19</enddate>
    <id>18</id>
    <startdate>2016-07-18</startdate>
  </event>
  <event>
    <name>勤労感謝の日</name>
    <color>#ddeeff</color>
    <enddate>2015-11-24</enddate>
    <id>19</id>
    <startdate>2015-11-23</startdate>
  </event>
  <event>
    <name>天皇誕生日</name>
    <color>#ddeeff</color>
    <enddate>2015-12-24</enddate>
    <id>20</id>
    <startdate>2015-12-23</startdate>
  </event>
  <event>
    <name>春分の日</name>
    <color>#ddeeff</color>
    <enddate>2016-03-21</enddate>
    <id>21</id>
    <startdate>2016-03-20</startdate>
  </event>
  <event>
    <name>春分の日 振替休日</name>
    <color>#ddeeff</color>
    <enddate>2016-03-22</enddate>
    <id>22</id>
    <startdate>2016-03-21</startdate>
  </event>
  <event>
    <name>こどもの日</name>
    <color>#ddeeff</color>
    <enddate>2016-05-06</enddate>
    <id>23</id>
    <startdate>2016-05-05</startdate>
  </event>
  <event>
    <name>成人の日</name>
    <color>#ddeeff</color>
    <enddate>2016-01-12</enddate>
    <id>24</id>
    <startdate>2016-01-11</startdate>
  </event>
  <event>
    <name>建国記念の日</name>
    <color>#ddeeff</color>>
    <enddate>2016-02-12</enddate>
    <id>25</id>
    <startdate>2016-02-11</startdate>
  </event>
</monthly>

Googleカレンダーが終了時刻それ自体はイベントの期間に含めない(不等号に「=(イコール)」が付くかどうかの違いのようなもの) のに対して、monthly.js は終了時刻それ自体をイベントの期間に含めるという違いがあるため、endtime(enddateじゃなくて)がない場合は、enddate を一日早めるという面倒な処理が追加で必要になります。(上記のコードではそれが追加されていないので注意。)

上記の面倒な処理を終えて「monthly.js」のマニュアルを読んでWebアプリに組み込むと、以下のように美しく表示できるようになります。ちなみに、高速で「月」を切り替えるとイベントが移動したり増殖したりする「monthly.js」のバグを作り終えてから発見しました。(ぎりぎりクリティカルなバグでもないかというあんばいですが…)

スクリーンショット 2016-02-18 0.34.09

スクリーンショット 2016-02-18 0.34.24

お疲れ様です。( ´Д`)=3

2015/10/01(木)第8回 心理学検定 1科目受験1科目合格

情報系の学生であまりにも時間が取れなくて1科目受験にしても不合格もあり得る出来具合かと思いましたが偏差値79で合格でした。心理学専攻でなくても十分合格可能であることを示せました。

心理学検定#8

そこまで自分の正答率は高くないと思いますが、適当に受けている学生のおかげで偏差値が上がったのか? とりあえず合格してしまったので来年は「統計・測定・評価」と「臨床・障害」で心理学検定2級をとって、Serial Experiments Lain(PS版)をプレイしつつ、心理学の知識を人生に役立てたいと考えているところであります。

情報系の学生が受ける価値があるのかと少し悩みましたが、充分良い刺激になったように感じられます。

2015/08/24(月)第8回 心理学検定 受験

本日は第8回 心理学検定でした。会場は金沢工業大学。JAISTからは北陸鉄道石川線の一日フリーエコきっぷで行けるので非常に行きやすい。周りは飲食店やコンビニも豊富で羨ましかった。

修論発表の3日後が検定試験で、8月20日に修論発表をして直後に修士論文の修正を命じられて、勉強期間は4日程度しか取れませんでした。当日は4時間睡眠ぐらいで、「社会・感情・性格」のみ受験しました。

1問目は「社会的手抜き」の誤りを選ぶ問題でしたが、初っ端から「調整ロス」「集団同一視」「加算的課題」と覚えていない・知らないキーワードがそこそこ出てきて苦しい闘いを強いられました。「集団同一視」が「社会的手抜き」と関係がないと推測して選びましたが、『心理学辞典』では「これが協同作業に伴う調整ロスではないことを現実集団と疑似集団(略)を用いて実験した。」と書かれいて、結局のところ、実験の結果、調整ロスだったのかそうでなかったのかは「調整ロス」の説明がないので不明瞭です。(「集団同一視」が「社会的手抜き」とは結びつかないように思えるので「調整ロス」が誤りだとは思いますが。)

1問目以降も、誤りを選ぶ問題が多くて情報系学生にはこたえた。体系的な心理学の知識がないと中々厳しいと感じました。自信を持って答えられたのもあったけど、合格しているかは怪しいです。

「公式問題集」「基本キーワード本」『心理学辞典』以外では↓を買いました。

臨床心理士・指定大学院対策 まるおぼえ心理学一問一答
臨床心理士・指定大学院対策 まるおぼえ心理学一問一答

実力確認!!心理学検定模擬問題集
実力確認!!心理学検定模擬問題集

一問一答は同じような問題を別の問い方をしてくれていて良い。情報系出身なら、もう一冊教科書的な本を買って、心理学検定公式HPで公開されているキーワードをルーズリーフにまとめても良かったと思っています。公式のキーワード本は網羅性も詳しさもいまいちで、非心理学出身者の場合は「公式問題集」「基本キーワード」だけでは不十分であると感じました。(「調整ロス」も載っていない)『心理学辞典』は心理学出身でなければほぼ必須でしょう。

情報処理技術で飯を食べるので、来年「臨床・障害」「統計・測定・評価」を受けて心理学検定2級 合格になれば、心理学検定とはおさらばの予定です。(自然言語処理技術を駆使してテキストから感情や性格を統計的に分析するようなケースがあっても大体カバーできるので。「臨床・障害」を学ぶのは単なる趣味です。)

2015/08/17(月)心理学検定 勉強2日目

本日は4.6の「ステレオタイプ」から4.15の「感情の理論」までを『心理学辞典』を読みながらキーワードを暗記カード化していきました。

「内集団バイアス」「傍観者効果」「自尊心脅威モデル」「ゲイン-ロス効果」「条件即応モデル」「世論形成の公共化モデル」「単純接触効果」あたりは人生で役立ちそうだと感じました。

内定先企業以上にいい(目的をよく達成)できる企業なんてないだろうと思っていますが、一呼吸おいて「内集団バイアス」の影響がないか考えてみるのもいいかもしれません。「ゲイン-ロス効果」はツンデレをよく説明していて面白い。

人助けをしたい場合は、周りが要援助者を知らんぷりでも、「傍観者効果」を疑ってかからねばならないのだと思いました。(実際にそのような場面に遭遇したらかなりの勇気が必要そうですが。)

明日からは修論の準備なので、心理学検定の勉強は数日お休みです。

2015/08/15(土)心理学検定 勉強1日目

自分が心理学を学ぶ目的。

  • 自分を苦しめ続けているものの正体を知りたい。
  • Serial experiments lain(PSのゲーム)をより楽しみたい。
  • 入社したら若者が楽しめるものを業務で創らねばならない。(楽しめるものを創るには心理学の知識があるほうがいいだろう。)
  • できれば若者の心を満たすものを創りたい。 → 心が満ちている状態とは? → 心理学を学ばないと分からなそう。

他にも、騙されれにくくなるという利点もありますし、流行るか流行らないか分からない最新のIT技術よりもよっぽど長い期間役に立ってくれるのではないかと思って心理学を学ぶ所存であります。

本当は心理学検定2級を狙いたかったのですが、修論発表の3日後が試験でそれどころではないため、「社会・感情・性格」の1科目に集中することにしました。

予め読んでおいた本は「感情心理学・入門 (有斐閣アルマ)」「図説社会心理学入門」「図説 心理学入門」「主要5因子性格検査ハンドブック―性格測定の基礎から主要5因子の世界へ」「心理学マニュアル 質問紙法」「性格心理学への招待―自分を知り他者を理解するために (新心理学ライブラリ)」の6冊。その内、「心理学マニュアル 質問紙法」と「性格心理学への招待―自分を知り他者を理解するために (新心理学ライブラリ)」は必要なところのみ読んで、統計学力は統計検定2級を辛うじて合格したレベルで、論文はビッグ・ファイブの日本人向けの質問紙に関する論文をいくつか読んだというのが心理学検定勉強開始前の自分のレベルです。

心理学検定に備えて買った本は以下の3冊。(去年ぐらいから受けたいと思っていたので公式問題集は1年古いです。)

心理学検定 基本キーワード 改訂版
心理学検定 基本キーワード 改訂版

心理学検定 公式問題集 2014年度
心理学検定 公式問題集 2014年度

心理学辞典
心理学辞典

今は基本キーワード本に出てくる太い文字のキーワードを、分かりにくければを心理学辞典で参照しつつ、暗記カード化している状態です。基本キーワード本の p.125「精緻化見込みモデルによれば、前者は説得の中心ルートであり(以下略)」などは意味不明なので、心理学辞典は買う価値ありました。(心理学辞典には精緻化見込みモデルの図があって、中心ルートと周辺ルートが何を指すか分かります。)基本キーワード本は読点の使い方もいまいちです。

今日はキーワード本の4.5の「帰属過程」まで進めました。「譲歩的要請法」で「先っちょだけだから…」を連想してしまった自分が嫌だ…。人生で活かせそうなのは「社会的促進」「初頭効果」「自己説得」「スリーパー効果」。スリーパー効果の説明は心理学辞典のほうで詳細に説明されていて、それによると、低信頼性の送り手の場合と違って、高信頼性の送り手の説得は時間とともに効果が減少してしまうそうな。