Bug #2546

Daylight Saving Time が反映されない

Added by Susumu Yata over 4 years ago. Updated over 4 years ago.

Status:完了Start date:05/07/2014
Priority:NormalDue date:
Assignee:Susumu Yata% Done:

100%

Category:-
Target version:単発

Description

概要

時刻を入力したときに Daylight Saving Time が反映されません.

Groonga では時刻の表現に UTC Epoch からの経過秒数を使っているのですが,タイムゾーンが Pacific Daylight Time(太平洋夏時間,PDT)になるように時刻を入力したとき, Daylight Saving Time への補正がおこなわれていない値(一時間ずれた値)が格納されます.

再現方法

この問題は,システムのタイムゾーンを PDT にすれば,以下の Groonga コマンドで確認できます.

$ src/groonga -n /tmp/groonga/tmp.db
> table_create Table TABLE_NO_KEY
[[0,1399448258.85777,0.00617003440856934],true]
> column_create Table Column COLUMN_SCALAR Time
[[0,1399448260.83597,0.00195145606994629],true]
> load --table Table --columns Column --values '
  [["2012-11-04 00:12:34"],
   ["2012-11-04 01:12:34"],
   ["2012-11-04 02:12:34"],
   ["2012-11-04 03:12:34"],
   ["2012-11-04 04:12:34"]]'
[[0,1399448262.88971,0.000402212142944336],5]
> select Table
[
 [0,1399448264.73128,0.000463724136352539],
 [[[5],
   [["_id","UInt32"],["Column","Time"]],
   [1,1352016754.0],
   [2,1352020354.0],
   [3,1352023954.0],
   [4,1352027554.0],
   [5,1352031154.0]]]]

2007 年以降は 11 月の第 1 日曜日午前 2 時まで Daylight Saving Time が適用されるため,本来であれば "2012-11-04 01:12:34" と "2012-11-04 02:12:34" の間は 2 時間(7,200 秒)となるはずですが,そのようになっていません.

History

#1 Updated by Susumu Yata over 4 years ago

  • Status changed from 新規 to 担当者作業中
  • Target version set to 単発
  • % Done changed from 0 to 30

原因と修正方法

ctx.c の grn_str2timeval() において, mktime() を呼び出すとき,引数 tm のメンバ tm_isdst を 0 にしているのが原因でした.

mktime() のマニュアル(on Ubuntu 14.04)では,以下のように説明されています.

The  mktime() function converts a broken-down time structure, expressed
as local time, to calendar time representation.  The  function  ignores
the  values  supplied  by the caller in the tm_wday and tm_yday fields.
The value specified in the tm_isdst field informs mktime()  whether  or
not  daylight  saving  time (DST) is in effect for the time supplied in
the tm structure: a positive value means DST is in effect;  zero  means
that  DST  is  not  in effect; and a negative value means that mktime()
should (use timezone information and system databases  to)  attempt  to
determine whether DST is in effect at the specified time.

マニュアルに従って tm_isdst を -1 にすることにより,手元では Daylight Saving Time が反映されるようになりました.

修正後の実行結果を以下に示します. "2012-11-04 01:12:34" と "2012-11-04 02:12:34" の間隔が 2 時間(7,200 秒)になっています. また, "2012-11-04 02:12:34" 以降は修正前と同じ値になっていて,夏時間の終わりが正しく反映されていることが確認できます.

## 修正後(tm_isdst = -1)
$ src/groonga -n /tmp/gonga/tmp.db
> table_create Table TABLE_NO_KEY
[[0,1399448083.48934,0.00113201141357422],true]
> column_create Table Column COLUMN_SCALAR Time
[[0,1399448085.62635,0.00165057182312012],true]
> load --table Table --columns Column --values '
  [["2012-11-04 00:12:34"],
   ["2012-11-04 01:12:34"],
   ["2012-11-04 02:12:34"],
   ["2012-11-04 03:12:34"],
   ["2012-11-04 04:12:34"]]'
[[0,1399448087.8668,0.000379323959350586],5]
> select Table
[[0,1399448090.63619,0.000773191452026367],
 [[[5],
   [["_id","UInt32"],["Column","Time"]],
   [1,1352013154.0],
   [2,1352016754.0],
   [3,1352023954.0],
   [4,1352027554.0],
   [5,1352031154.0]]]]

念のため,ほかの環境でもテストしてみます.

#2 Updated by Kouhei Sutou over 4 years ago

いいんじゃないでしょうか!

#3 Updated by Susumu Yata over 4 years ago

  • % Done changed from 30 to 50

ありがとうございます!

FreeBSD でも tm_isdst の扱いは(少なくとも仕様上)同じなことが確認できました.

後は, Daylight Saving Time が開始するタイミングのテストと, Daylight Saving Time による空白時間を入力したときの動作確認をしておこうと思います.

Daylight Saving Time の終了時に発生する重複した時間帯はどうやって入力すればいいのかも少し気になりますが,それはまた別の問題と考えるべきかと思います.

#4 Updated by Susumu Yata over 4 years ago

夏時間の開始前後におけるテスト結果

夏時間の開始(3 月第 2 日曜日午前 2 時)前後の時間を入力してみたところ,修正によって夏時間が反映されるようになることが確認できました.

実行結果は以下の通りで,修正前は "2012-03-11 01:12:34" と "2012-03-11 03:12:34" の間が 2 時間(7,200 秒)となっているのに対し,修正後は 1 時間(3,600 秒)となっています.

## 修正前(tm_isdst = 0)
$ rm -rf /tmp/groonga && mkdir /tmp/groonga && s/groonga -n /tmp/groonga/db.grn
> table_create Table TABLE_NO_KEY
[[0,1399512131.53385,0.00451922416687012],true]
> column_create Table Column COLUMN_SCALAR Time
[[0,1399512133.23844,0.00220727920532227],true]
> load --table Table --columns Column --values '\
  [["2012-03-11 00:12:34"],\
   ["2012-03-11 01:12:34"],\
   ["2012-03-11 03:12:34"],\
   ["2012-03-11 04:12:34"]]'
[[0,1399512135.33773,0.000335931777954102],4]
> select Table
[[0,1399512137.2137,0.000440359115600586],
 [[[4], [["_id","UInt32"],["Column","Time"]],
   [1,1331453554.0],
   [2,1331457154.0],
   [3,1331464354.0],[4,1331467954.0]]]]
> select Table --filter 'Column == "2012-03-11 00:12:34"'
[[0,1399512139.14406,0.0012359619140625],
 [[[1],[["_id","UInt32"],["Column","Time"]],[1,1331453554.0]]]]
> select Table --filter 'Column == "2012-03-11 01:12:34"'
[[0,1399512141.19773,0.0023343563079834],
 [[[1],[["_id","UInt32"],["Column","Time"]],[2,1331457154.0]]]]
> select Table --filter 'Column == "2012-03-11 03:12:34"'
[[0,1399512143.25643,0.000901937484741211],
 [[[1],[["_id","UInt32"],["Column","Time"]],[3,1331464354.0]]]]
> select Table --filter 'Column == "2012-03-11 04:12:34"'
[[0,1399512145.26644,0.00174832344055176],
 [[[1],[["_id","UInt32"],["Column","Time"]],[4,1331467954.0]]]]
## 修正後(tm_isdst = -1)
$ rm -rf /tmp/groonga && mkdir /tmp/groonga && s/groonga -n /tmp/groonga/db.grn
> table_create Table TABLE_NO_KEY
[[0,1399511896.3879,0.00105452537536621],true]
> column_create Table Column COLUMN_SCALAR Time
[[0,1399511898.65405,0.00103878974914551],true]
> load --table Table --columns Column --values '\
  [["2012-03-11 00:12:34"],\
   ["2012-03-11 01:12:34"],\
   ["2012-03-11 03:12:34"],\
   ["2012-03-11 04:12:34"]]'
[[0,1399511900.90269,0.000252246856689453],4]
> select Table
[[0,1399511902.93196,0.000457525253295898],
 [[[4],[["_id","UInt32"],["Column","Time"]],
 [1,1331453554.0],
 [2,1331457154.0],
 [3,1331460754.0],
 [4,1331464354.0]]]]
> select Table --filter 'Column == "2012-03-11 00:12:34"'
[[0,1399511905.83202,0.0020294189453125],
 [[[1],[["_id","UInt32"],["Column","Time"]],[1,1331453554.0]]]]
> select Table --filter 'Column == "2012-03-11 01:12:34"'
[[0,1399511908.04711,0.000685691833496094],
 [[[1],[["_id","UInt32"],["Column","Time"]],[2,1331457154.0]]]]
> select Table --filter 'Column == "2012-03-11 03:12:34"'
[[0,1399511910.09793,0.000780344009399414],
 [[[1],[["_id","UInt32"],["Column","Time"]],[3,1331460754.0]]]]
> select Table --filter 'Column == "2012-03-11 04:12:34"'
[[0,1399511911.96214,0.000684738159179688],
 [[[1],[["_id","UInt32"],["Column","Time"]],[4,1331464354.0]]]]

#5 Updated by Susumu Yata over 4 years ago

存在しない時刻のテスト

mktime() のマニュアルには存在しない時刻の扱いが記述されていないため,動作としては未定義と考えるべき(※)だと思いますが,念のために実環境(Ubuntu 14.04)で実験してみました.

※ Daylight Saving Time を適用しない場合の結果を返したり -1 を返したりする可能性が考えられます.

実験結果は以下の通りで, Ubuntu 14.04 では Daylight Saving Time が無視されるようです. 本来であれば "2012-03-11 02:12:34" は存在しないはずの時刻ですが, "2012-03-11 03:12:34" と同じ値になっています.

## 修正後(tm_isdst = -1)
$ rm -rf /tmp/groonga && mkdir /tmp/groonga && s/groonga -n /tmp/groonga/db.grn
> table_create Table TABLE_NO_KEY
[[0,1399512384.73122,0.00298738479614258],true]
> column_create Table Column COLUMN_SCALAR Time
[[0,1399512388.82841,0.000969886779785156],true]
> load --table Table --columns Column --values '\
  [["2012-03-11 01:12:34"],\
   ["2012-03-11 02:12:34"],\
   ["2012-03-11 03:12:34"]]'
[[0,1399512391.32411,0.00034332275390625],3]
> select Table
[[0,1399512394.43395,0.000571489334106445],
 [[[3],[["_id","UInt32"],["Column","Time"]],
   [1,1331457154.0],
   [2,1331460754.0],
   [3,1331460754.0]]]]
> select Table --filter 'Column == "2012-03-11 01:12:34"'
[[0,1399512398.77267,0.00111770629882812],
 [[[1],[["_id","UInt32"],["Column","Time"]],[1,1331457154.0]]]]
> select Table --filter 'Column == "2012-03-11 02:12:34"'
[[0,1399512400.92204,0.000619888305664062],
 [[[2],[["_id","UInt32"],["Column","Time"]],[2,1331460754.0],[3,1331460754.0]]]]
> select Table --filter 'Column == "2012-03-11 03:12:34"'
[[0,1399512403.35867,0.00192451477050781],
 [[[2],[["_id","UInt32"],["Column","Time"]],[2,1331460754.0],[3,1331460754.0]]]]

少し気になるところではありますが,特に問題のある動作ではないでしょう.

#6 Updated by Susumu Yata over 4 years ago

mktime() による自動判定のコストを確認

tm_isdst = -1 とすれば mktime() は夏時間を自動判定してくれるわけですが,この判定にどのくらいかかるのか気になったのでテストしてみました.

テストに使ったソースコードは以下のとおりです. Daylight Saving Time が有効な年(PDT の 2012 年)のランダムな時刻を 1,000,000 個生成して配列に格納しておき,それらを mktime() に渡します.

#include <cstdlib>
#include <cstring>
#include <ctime>
#include <iostream>
#include <random>
#include <vector>

class Timer {
 public:
  Timer() : base_(now()) {}
  double elapsed() const {
    return now() - base_;
  }
 private:
  double base_;
  static double now() {
    struct timespec current_time;
    ::clock_gettime(CLOCK_MONOTONIC, &current_time);
    return current_time.tv_sec + (current_time.tv_nsec / 1000000000.0);
  }
};

int main(int argc, char *argv[]) {
  constexpr std::size_t NUM_VALUES = 1000000;
  std::vector<struct tm> values(NUM_VALUES);
  std::mt19937 rng32;
  for (struct tm &tm : values) {
    std::memset(&tm, 0, sizeof(tm));
    // 2012-??-?? ??:??:??.
    tm.tm_year = 112;
    tm.tm_mon = rng32() % 12;
    tm.tm_mday = 1 + (rng32() % 29);
    tm.tm_hour = rng32() % 24;
    tm.tm_min = rng32() % 60;
    tm.tm_sec = rng32() % 60;
    tm.tm_isdst = std::atoi(argv[1]);
  }
  Timer timer;
  for (int i = 0; i < NUM_VALUES; ++i) {
    std::mktime(&values[i]);
  }
  std::cout << "elapsed [s] = " << timer.elapsed() << std::endl;
  return 0;
}

実行結果は以下のようになりました. 実行環境は Ubuntu 14.04 64-bit on VMWare Fusion (Core i7-4558U 2.8GHz) です. 自動判定を有効にした方が速いという結果になりました. ひょっとすると, tm_isdst が 0 以上のときは,自動判定した結果から逆算しているのかもしれません.

$ clang++ -Wall -O2 -std=c++11 a.cpp -lrt

## tm_isdst = 1
$ ./a.out 1
elapsed [s] = 1.97856
$ ./a.out 1
elapsed [s] = 1.97285

## tm_isdst = 0
$ ./a.out 0
elapsed [s] = 2.7475
$ ./a.out 0
elapsed [s] = 2.72544

## tm_isdst = -1
$ ./a.out -1
elapsed [s] = 1.67461
$ ./a.out -1
elapsed [s] = 1.68575

これにより, mktime() による自動判定には特にデメリットがなさそうなことがわかりました.

ちなみに,同じ時刻を繰り返し渡したときは, tm_isdst を変更してもほぼ同じ時間で終了しました. 分岐のミスなどが発生しないからでしょうか.

#7 Updated by Susumu Yata over 4 years ago

付属のテスト

手元の環境(Ubuntu 14.04)では, Groonga 付属のテストを問題なく通過しました. タイムゾーンは JST と PDT の両方で確認しました.

~/git/groonga/test/unit$ ./run-test.sh
...
Finished in 11.626737 seconds (total: 7.528428 seconds)

1404 test(s), 602530 assertion(s), 0 failure(s), 0 error(s), 0 pending(s), 3 omission(s), 0 notification(s)
100% passed
~/git/groonga/test/command$ ./run-test.sh
...
|-----------------------------------------------------------------------| [100%]

  tests/sec |    tests |   passes | failures |   leaked |  omitted | !checked |
      31.34 |      915 |      893 |        0 |        0 |       22 |        0 |
97.6% passed in 29.1998s.

#8 Updated by Susumu Yata over 4 years ago

  • Status changed from 担当者作業中 to 完了チェック待ち
  • % Done changed from 50 to 80

Applied in changeset commit:466eff87ecb9de0acf9f104ab6ce1d8f18eada38.

#9 Updated by Susumu Yata over 4 years ago

Travis CI のエラーについて

Travis CI にてエラーが発生したのですが,見たところ今回の修正とは関係なさそうです.

以前にも同様のエラーが発生しているらしく,再現性がないのではないかと考えられます.

そういうわけで,このエラーについては別件と判断します.

そのほか,特に問題が見つからなければ,明日には完了にします.

#10 Updated by Susumu Yata over 4 years ago

  • Status changed from 完了チェック待ち to 完了
  • % Done changed from 80 to 100

解決したということで完了にします.

Also available in: Atom PDF