師匠の散歩

暦あれこれ

内部時計関数に対する考察

カレンダーを作成する場合、Perlではtime関数、timelocal関数、localtime関数を使うのが普通です。 これらを使った日記CGIスクリプトを採用して実際に使ってきましたが、これまでは特に問題はありませんでした。

しかし、ほんの少し過去や未来を計算すると値がどうもおかしくなります。 何がどのようにエラーになるのかは他の人のページを見ていただくとして、ここでは 内部関数を使わずに、時間関数を作成する 方法を紹介します。

time関数

現在UNIXシステムでは、時刻を求める関数としてtime関数があります。これは、1970/1/1 0:0:0 からの秒数=epockもしくはエポック秒を出力するものです。

ここで、UNIXシステムはシステムに登録したタイムゾーンを考慮したepockを出力してくれる。つまり、 グリニッジ時刻が1970/1/1 0:0:0のとき、エポック秒=0なので、日本製パソコンでは、時差9時間(JST-9)があるため、 ローカルエポック秒=-60*60*9=-32400となるのだ。

結論を言うと、内部関数timeを使わずに現在時刻を調べる方法を見つけることはできなかった。

エクセルのVBAにも当然ながら内部時計関数timeがあるがUNIXのtime関数とは異なる。1900年1月1日からの日数を出力するのだが、驚いたのはタイムゾーンが設定されていないことである。

timelocal関数

内部関数timeLocalを使わず、getTimeLocal2関数を作成する。

1/1/1 0:0:0 から起算した秒数を求め、1/1/1 0:0:0から1970/1/1 0:0:0までの秒数を減算する方法とした。日付はグレゴリオ暦を使い、うるう秒は考慮しない。

# -------------------------------------------
# 1..12月の「前月までの累積日数」を配列で返す
# -------------------------------------------
sub TotalDays {
  my $year = shift;
  if (checkLeapYear($year)) {
    return (0,31,60,91,121,152,182,213,244,274,305,335); # うるう年の場合
  } else {
    return (0,31,59,90,120,151,181,212,243,273,304,334); # 平年の場合
  }
  return "";
}
# ------------------------------------------------------------------------------
# 1/1/1 0:0:0 から 所定の年月日までの秒数を数え、定数あわせでエポック秒を求める
# そのため、紀元前は正しい解は出ない
# 目標とする解:# getTimeLocal2(0,0,0,1,0,70) = 60*60*(-9) = -32400
# 引数の年月日が存在するかどうかは確認しない
# ------------------------------------------------------------------------------
sub getTimeLocal2 {
  my ($sec,$min,$hour,$day,$month,$year,$yday,$ydays,$summer) = @_;
  $year+=1900;
  $month++;
  my @monDays = TotalDays($year);
  my $epock =(365*($year-1) + int(($year-1)/4) - int(($year-1)/100) + int(($year-1)/400) )*86400;
  $epock -= 62135629200 ;               # 62135596800 + 32400
  $epock += $monDays[$month-1] * 86400; # 前月までの日数
  $epock += ($day-1) * 86400;           # 当月における当日までの日数
  $epock += ($hour) * 3600;             # 時(0から始まるので1は引かない)
  $epock += ($min) * 60;                # 分(0から始まるので1は引かない)
  $epock += ($sec);                     # 秒(0から始まるので1は引かない)
  return $epock;
}

localtime

エポック秒から現在時刻を求めるgetLocalTime2関数関数を求める。

当初は簡単かと思っていたが、なかなか難しい。具体的には、春分点周期でエポック秒を割れ 現在の年が判別できるものと考えていたが、1月1日0時0分0秒と前年の12月31日23時59分59秒との判別がうまくできない。

そこで、グレゴリオ暦が400年周期であることを使えば、定式化できるのではないかと考えた。

開始年月日時刻エポック秒100年の秒数100年の日数
1/1/1 0:0:0-62135629200 3155673600 36524
101/1/1 0:0:0-58979955600 3155673600 36524
201/1/1 0:0:0-55824282000 3155673600 36524
301/1/1 0:0:0-52668608400 3155760000 36525
401/1/1 0:0:0-49512848400 3155673600 36524
501/1/1 0:0:0-46357174800 3155673600 36524
601/1/1 0:0:0-43201501200 3155673600 36524
701/1/1 0:0:0-40045827600 3155760000 36525
801/1/1 0:0:0-36890067600 3155673600 36524

いろいろと定式を考えたが、いいのが思いつかなかったので、ゾーンごとに年を計算することにした。(ゾーン分けしないとエラーが発生した。検証はできていない。)

なんとか$epockがある年さえわかれば、あとは元日からの秒数を力ずくで計算できると考えた。ゾーンに分けた$epockを太陽周期(365.242194)で割りintをとれば結果が得られると思ったが、次の結果になった。

年月日単純計算int後判定1秒後単純結果int後判定
398年大晦日398.9989234398 399元旦398.9989234398NG
399年大晦日399.9982603399 400元旦399.9982603399NG
400年大晦日401.0003351401NG401元旦401.0000000401 
401年大晦日401.9993369401 402元旦401.9993369401NG
402年大晦日402.9986738402 403元旦402.9986738402NG
403年大晦日403.9980107403 404元旦403.9980107403NG
404年大晦日405.0000855405NG405元旦405.0000855405 

しかたがないので、(int後+1)の年の元日のエポック秒を求め、$epockと比較して無理やり年を求めることにした。

# ---------------------------------------------
# エポック秒が含まれる年を求める
# 0以上の実数を対象にする
# ---------------------------------------------
sub epock2year {
  my $time = shift;
  my $loop = 0;                     # 1/1/1
  my $eraTime  = -62135629200 ;     # 1/1/1 0:0:0 のローカルエポック秒
  my $loop400  = 12622780800  ;     # 400年周期の秒数
  my $loop100  = 3155673600   ;     # 100年周期の秒数
  my $kaiki    = 365.242194   ;     # 太陽周期として用いた値
 # if ($time < $eraTime ){
 #   print "紀元前の日時には対応していません";
 #   exit;
 # }
  while ( $time >= $eraTime ) {
    $eraTime  += $loop400;                               # 400年グループにわける
    $loop ++;
  }
  $eraTime  -= $loop400;                                 # 足しすぎたので戻す
  $loop--;                                               # 同上
  my $loopYear = 1 + 400*($loop) ; 
  my $newTime = $time - $eraTime ;  
  my $plusYear = $newTime/$kaiki/86400;                  # 400年ループの中で年計算
  my $getYear = round(($loopYear+$plusYear),0);          # 小数点1位四捨五入で一旦年を求める
  if ( $time < getTimeLocal2(0,0,0,1,0,$getYear-1900) ){ # 検算
    $getYear--;                                          # 大きければ1減らす
  }
  return $getYear;                                       # 結果を返す
}                                      # 結果を返す

$epockの年がわかったので、その年の元日(1/1 0:0:0)から$epockまでの秒数を求め、あとは力ずくで計算すれば終了となる。

# ---------------------------------------------
# エポック秒から年月日時刻を求める
# 西暦1/1/1からの秒数を考慮するため、紀元前には対応しない
# データ出力は、localtime関数と同じにする
# ---------------------------------------------
sub getLocalTime2 {
  my $epock = shift;
  my ($year,$month,$day,$hour,$min,$sec,$week,$summer);
  $year = epock2year($epock);                     # $epockのある年を求める
  my $kai = getTimeLocal2(0,0,0,1,0,$year-1900);  # その年の元日のエポック秒を求める
  $kai = ($epock -$kai)/86400;                    # 元日からの秒数を86400で割る
  if ($kai ==0 ) {                                # 1/1 0:0:0 は特別扱いで処理した
    $week=Zeller($year,1,1);
    return ( 0,0,0,1,0,$year-1900,$week,0,0) ;
  }
  my $yday=int($kai);                             # 元日からの日数-1
  my @monDays = TotalDays($year);                 # 配列読み込み
  foreach(@monDays){
    $month++ if ($kai>$_) ;                       # 月の計算
  }
  $kai=$kai-$monDays[$month-1];
  $day=int($kai);                                 # 日の計算
  $kai=($kai-$day)*24;
  $hour = int($kai);                              # 時の計算
  $kai=($kai-$hour)*60;
  $min=int($kai);                                 # 分の計算
  $sec=($kai-$min)*60;
  $sec=round($sec,0);                             # 秒の計算
  $day++;
  $week=Zeller($year,$month,$day);                # 曜日の計算
  $summer=0;                                      # サマータイム=0
  return  ($sec,$min,$hour,$day,$month-1,$year-1900,$week,$yday,$summer) ;
}

Topに戻る // 戻る
Copyright(C) Grandmaster since 2010最終更新:2015/5/15