和時計を作ってみた

DateTime::Spanを使ったcronプログラミング

今回は和時計を作ってみました.日の出,日の入りの時間を計算していて,それぞれ明け六ツ,暮れ六ツとなるすぐれものです.正午(しょううま)が12時じゃなかったり,丑三つ時がホントに午前2時ごろだったり,followしていると不思議な感じがします.それはともかく,今回はこのスクリプトの構成について…

繰り返し実行するスクリプト

以前,hinetmaniaを作った時にも思ったんですが,何かのアルゴリズムに従って不定期な時間間隔で処理を繰り返すしくみってのがなかなか見つかりません.定期的な実行ならcronでいいんだけどな,と思いながらPoCo::Cronを見ていたら,「"DateTime::Set iteretor"で定義されたスケジュールに基づいてクライアントにイベントを送ると書いてある」のに気がつきました.そこでDateTime::Setを読んでみるとfrom_recurrenceというメソッドを使えば時間間隔をプログラミングできそうだということがわかりました.ってことで,まず作ってみたのがこんなスクリプト

#!/usr/local/bin/perl
use strict;
use warnings;
use POE;
use POE::Component::Cron;
use DateTime::Set;

my $s1 = POE::Session->create(
    inline_states => {
        _start => sub {
            $_[KERNEL]->delay( '_die_', 120 );
        },
        Tick => sub {
            print 'Tick ', scalar localtime, "\n";
        },
        _die_ => sub {
            print "exit\n";
            exit;
          }
    }
);

POE::Component::Cron->add(
    $s1 => Tick => DateTime::Set->from_recurrence(
        span => DateTime::Span->from_datetimes(
            start => DateTime->now,
            end   => DateTime::Infinite::Future->new
        ),
        recurrence => \&every_30_seconds,
    )
);

sub every_30_seconds {
    my $dt = shift;
    if ( $dt->second < 30 ) {
        return $dt->truncate( to => 'minute' )->add( seconds => 30 );
    }
    else {
        return $dt->truncate( to => 'minute' )->add( minutes => 1 );
    }
}

POE::Kernel->run();

このスクリプトは,起動してから120秒後にexitと表示して終了するまで,毎0秒と30秒に時刻を出力します.120秒間の指定は_start中にdelayで指定していますが,

    inline_states => {
        _start => sub {
            $_[KERNEL]->delay( '_die_', 120 );
        },

この_startの部分がなくなるとスクリプト自体が動かなくなります.cronのようにずっと動かしたいスクリプトとしては冗長な表現なのでなくしたいのですが,今のところそのやり方がわかりません.繰り返し実行する時間の設定は

    $s1 => Tick => DateTime::Set->from_recurrence(
        span => DateTime::Span->from_datetimes(
            start => DateTime->now,
            end   => DateTime::Infinite::Future->new
        ),
        recurrence => \&every_30_seconds,
    )

で行っていて,この場合,今(DateTime->now)から未来永劫(DateTime::Infinite::Future->new)の区間で実行することを宣言しています.イベントを実行する時刻は,最後にイベントが発生した時刻を引数とした,関数の返り値として取得します.つまりevery_30_secondsという関数の場合,

sub every_30_seconds {
    my $dt = shift;
    if ( $dt->second < 30 ) {
        return $dt->truncate( to => 'minute' )->add( seconds => 30 );
    }
    else {
        return $dt->truncate( to => 'minute' )->add( minutes => 1 );
    }
}

引数$dtが最後にイベントが起こった時間で,その次の0秒か30秒の時刻を返しています.

和時計

今回の話には,前置きがあります.ある会合でDMAKIさんとしゃべっていて,「これからは夏時間なんて古い,時代は江戸の時刻制度じゃ」と私がぶち上げた時,DMAKIさんが「それ僕,書きました」とのたまわったのです.それがDateTime::Calendar::Japaneseでした.このモジュールは時刻を与えると,年号とか江戸の時刻制度での時間を返してくれる優れものです.このモジュールをしばらく使った後,必要なのは,今が何時だか返してくれるモジュールではなくて,次に鐘を打つ時間が何時なのかを返してくれるモジュール(というかアルゴリズム)だと気がついたのでした.それがこんな感じ.

use DateTime::Set;
use DateTime::Duration;
use DateTime::Event::Sunrise;

my %location = ( longitude => 139.45, latitude => 35.40, altitude => -6 );
my $sunrise  = DateTime::Event::Sunrise->sunrise(%location);
my $sunset   = DateTime::Event::Sunrise->sunset(%location);
my $day_set =
  DateTime::SpanSet->from_sets( start_set => $sunrise, end_set => $sunset );
my $delta = DateTime::Duration->new( minutes => 1 );

sub tick {
    my $dt = shift;
    my ( $ret, $quarter );
    if ( $dt->is_infinite ) {
        $ret = $dt;
    }
    elsif ( $day_set->contains($dt) ) {
        my $i;
        $quarter =
          ( $sunset->next($dt) - $sunrise->current($dt) )->multiply( 1 / 24 );
        $ret = $sunrise->current($dt);
        if ( ( $dt + $delta > $ret ) && ( $dt - $delta < $ret ) ) {
            $ret = $ret->add($quarter);
        }
        else {
            while ( $dt >= $ret ) {
                $ret = $ret->add($quarter);
                $i++;
            }
            $ret = $sunset->next($dt) if ( $i == 24 );
        }
    }
    else {
        my $i;
        $quarter =
          ( $sunrise->next($dt) - $sunset->current($dt) )->multiply( 1 / 24 );
        $ret = $sunset->current($dt);
        if ( ( $dt + $delta > $ret ) && ( $dt - $delta < $ret ) ) {
            $ret = $ret->add($quarter);
        }
        else {
            while ( $dt >= $ret ) {
                $ret = $ret->add($quarter);
                $i++;
            }
            $ret = $sunrise->next($dt) if ( $i == 24 );
        }
    }
    return $ret;
}

やってることは,江戸時代の時刻制度のサイトに載っていた方法をそのまま使っただけで,

my %location = ( longitude => 139.45, latitude => 35.40, altitude => -6 );
my $sunrise  = DateTime::Event::Sunrise->sunrise(%location);
my $sunset   = DateTime::Event::Sunrise->sunset(%location);

東京の緯度経度(139.45, 35.40)で,日の出,日没からそれぞれ30分ほど暗くなった頃(Civil twilight, 太陽は1時間で15度動くから6度なら30分弱ってところかな,と)の値を使っていて,引数で与えた時間が日中かどうかで処理を分割して

    elsif ( $day_set->contains($dt) ) {

計算しています.24で割っている

        $quarter =
          ( $sunset->next($dt) - $sunrise->current($dt) )->multiply( 1 / 24 );

のは,子,丑,寅,卯,辰,巳,午,羊,申,酉,戌,亥の刻限がそれぞれ4つのパートに分かれる(12 x 4 / 2 = 24)からで,妙に足し算で答えを出してたり,境界条件にきびしいことをやっているのは,日の出日没の計算で数値誤差がでてるっぽかったからです.もしかしたらまだバグがあるかもしれません.それはそれとして,最終的にこれらを組み合わせて,鐘を鳴らす代わりにtwitterに書き込みをするスクリプトがこれ

#!/usr/local/bin/perl
use strict;
use warnings;
use POE;
use POE::Component::Cron;
use POE::Component::Client::Twitter;
use DateTime::Set;
use DateTime::Duration;
use DateTime::Event::Sunrise;

my %location = ( longitude => 139.45, latitude => 35.40, altitude => -6 );
my $sunrise  = DateTime::Event::Sunrise->sunrise(%location);
my $sunset   = DateTime::Event::Sunrise->sunset(%location);
my $day_set =
  DateTime::SpanSet->from_sets( start_set => $sunrise, end_set => $sunset );
my $delta = DateTime::Duration->new( minutes => 1 );

my @daytime_name = qw(
  明け六ツ
  卯の刻二つ時
  卯の刻三つ時
  卯の刻四つ時
  朝五ツ
  辰の刻二つ時
  辰の刻三つ時
  辰の刻四つ時
  昼四ツ
  巳の刻二つ時
  巳の刻三つ時
  巳の刻四つ時
  真昼九ツ
  午の刻二つ時
  正午
  午の刻四つ時
  昼八ツ
  羊の刻二つ時
  羊の刻三つ時
  羊の刻四つ時
  夕七ツ
  申の刻二つ時
  申の刻三つ時
  申の刻四つ時
);
my @nighttime_name = qw(
  暮れ六ツ
  酉の刻二つ時
  酉の刻三つ時
  酉の刻四つ時
  宵五ツ
  戌の刻二つ時
  戌の刻三つ時
  戌の刻四つ時
  夜四ツ
  亥の刻二つ時
  亥の刻三つ時
  亥の刻四つ時
  真夜九ツ
  子の刻二つ時
  子の刻三つ時
  子の刻四つ時
  夜八ツ
  丑の刻二つ時
  丑の刻三つ時
  丑の刻四つ時
  暁七ツ
  寅の刻二つ時
  寅の刻三つ時
  寅の刻四つ時
);

my $twitter = POE::Component::Client::Twitter->spawn(
    username => "wadokei",
    password => "password",
);

my $s1 = POE::Session->create(
    inline_states => {
        _start => sub {
            $_[KERNEL]->delay( 'check', 300 );
        },
        Tick => sub {
            $twitter->yield(
                update => sprintf "%s: %s",
                scalar localtime,
                nandoki( DateTime->now->set_time_zone('Asia/Tokyo') )
            );
        },
        check => sub {
            $_[KERNEL]->delay( 'check', 300 );
          }
    }
);

POE::Component::Cron->add(
    $s1 => Tick => DateTime::Set->from_recurrence(
        span => DateTime::Span->from_datetimes(
            start => DateTime->now->set_time_zone('Asia/Tokyo'),
            end   => DateTime::Infinite::Future->new
        ),
        recurrence => \&tick,
    )
);

POE::Kernel->run();

sub tick {
    my $dt = shift;
    my ( $ret, $quarter );
    if ( $dt->is_infinite ) {
        $ret = $dt;
    }
    elsif ( $day_set->contains($dt) ) {
        my $i;
        $quarter =
          ( $sunset->next($dt) - $sunrise->current($dt) )->multiply( 1 / 24 );
        $ret = $sunrise->current($dt);
        if ( ( $dt + $delta > $ret ) && ( $dt - $delta < $ret ) ) {
            $ret = $ret->add($quarter);
        }
        else {
            while ( $dt >= $ret ) {
                $ret = $ret->add($quarter);
                $i++;
            }
            $ret = $sunset->next($dt) if ( $i == 24 );
        }
    }
    else {
        my $i;
        $quarter =
          ( $sunrise->next($dt) - $sunset->current($dt) )->multiply( 1 / 24 );
        $ret = $sunset->current($dt);
        if ( ( $dt + $delta > $ret ) && ( $dt - $delta < $ret ) ) {
            $ret = $ret->add($quarter);
        }
        else {
            while ( $dt >= $ret ) {
                $ret = $ret->add($quarter);
                $i++;
            }
            $ret = $sunrise->next($dt) if ( $i == 24 );
        }
    }
    return $ret;
}

sub nandoki {
    my $dt = shift;
    my ( $quarter, $tmp, $times, $ret );
    if ( $day_set->contains($dt) ) {
        $quarter =
          ( $sunset->next($dt) - $sunrise->current($dt) )->multiply( 1 / 24 );
        $times = 0;
        $tmp   = $sunrise->current($dt) + $quarter * $times;
        while ( !( ( $dt + $delta > $tmp ) && ( $dt - $delta < $tmp ) ) ) {
            $times++;
            $tmp = $sunrise->current($dt) + $quarter * $times;
        }
        $ret = $daytime_name[$times];
    }
    else {
        $quarter =
          ( $sunrise->next($dt) - $sunset->current($dt) )->multiply( 1 / 24 );
        $tmp   = $sunset->current($dt) + $quarter * $times;
        $times = 0;
        while ( !( ( $dt + $delta > $tmp ) && ( $dt - $delta < $tmp ) ) ) {
            $times++;
            $tmp = $sunset->current($dt) + $quarter * $times;
        }
        $ret = $nighttime_name[$times];
    }
    return $ret;
}

twitterで音を出す仕組みがあれば使ってみるんだけど,今のところその手段がないので表示だけ.今日も地道に時刻をきざみます.落ちてなければね(笑