Ask Learn
Preview
Please sign in to use this experience.
Sign inThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
このポストは、2 月 2 日に投稿された Is your code ready for the leap year? の翻訳です。
2016 年も 2 月に入りました。そして今年はうるう年です。多くの人にとっては仕事や遊ぶ日が 1 日増えたイレギュラーな年なだけかも知れませんが、ソフトウェア開発者にとってはこれがたいへん大きな問題になる可能性があります。
もしも今、うるう年のバグが潜んでいるコードがあるかもと気になっている方は、すぐにチェックしてみてください。もしかしたら、既に影響が出始めているのに気付いていないだけかもしれません。では、どんなバグがコードに潜んでいることが考えられるのでしょうか?
ここまで言っても、「まさか。私のコードは問題ないですよ。ちゃんと単体テストをしてますから」と、無関心な方もいるでしょう。
そのような方に、私はいつも次のような質問をします。「そうですか。ではテストではきちんと時計をモックしていますか? 2 月 29 日や 12 月 31 日を含めたエッジ ケースをテストしていますか? C++ の低レベル コードやシステムの残りの部分もテストしましたか? そもそも、うるう年のバグがどのようなものか、本当にご存じですか?」
すると、多くの人は一様にぽかんとした顔を見せます。
この問題が Azure に影響する理由
うるう年は開発者が書くほとんどのコードに関係する問題であり、多くの処理が Azure クラウドでの実行に影響します。Azure ではうるう年だった 2012 年にサービス停止 (英語) を経験したため、今度こそは何事も起きないよう十分に取り組んできました。そして、このとき私たちが行った調査や経験から学んだことを、ぜひ皆様にもお役立ていただきたいと考えています。
開発者が知っておくべきこと
いろいろありますが、一番重要なことからお伝えしていきましょう。
これらはメディアで大きく取り上げられたケースですが、影響や認知度の差こそあれ、ほかにも多くの問題が発生したに違いありません。たとえば、あまり知られてはいませんが 1996 年のうるう年のバグ (英語) の影響で、ニュージーランドやオーストラリアのタスマニア州のアルミニウム製錬工場のプロセス制御システムが 12 月 31 日 (366 日目) に突然シャットダウンするという事態が発生しました。
これにより溶融金属の温度管理ができなくなり、数百万ドル相当の設備被害が発生しました。こうした過去の事例は、ソフトウェアの不具合が現実にもたらすリスクを再認識させてくれます。最近の IoT の普及や IoT とクラウド コンピューティングの融合を考えると、開発者のだれもが十分注意すべき問題なのだということがわかります。
特に危険な 2 つのうるう年バグ
1. C/C++ での年数の加算または減算
Win32 API を使用する C/C++ コードでは、SYSTEMTIME (英語) 構造体で常用時を扱うのが一般的です。日付の要素ごとにフィールドが用意されており、年、月、日の値 (およびその他の値) が分離されています。よく次のように使用されます。
SYSTEMTIME st; // SYSTEMTIME 変数を宣言
GetSystemTime(&st); // 現在の日時を設定
st.wYear++; // 1 年分増やす
このコードではエラーは発生しませんが、コードが 2 月 29 日に呼び出されると、うるう年でない年の 2 月 29 日が返されます。たとえば、2016-02-29 + 1 年 = 2017-02-29 は存在しない日付です。
この値がいたるところの処理に渡されると、いずれは SystemTimeToFileTime (英語) などの関数のパラメーターに指定され、関数が正しく実行されずに戻り値 0 が返されることになります。戻り値をチェックせずにこのメソッドを使用しているコードは非常に多く、FILETIME の値が初期化されていない状態のままだと予期しない結果が生じる可能性があります。
SYSTEMTIME st; // SYSTEMTIME 変数を宣言
GetSystemTime(&st); // 現在の日時を設定
st.wYear++; // 1 年分増やす
// うるう年かどうかチェック
bool leap = st.wYear % 4 == 0 && (st.wYear % 100 != 0 || st.wYear % 400 == 0);
// 2 月 29 日だがうるう年でない場合、2 月 28 日にする
st.wDay = st.wMonth == 2 && st.wDay == 29 && !leap ? 28 : st.wDay;
類似のバグが標準的な C++ (非 Windows) コードでも発生する可能性があることにご注意ください。SYSTEMTIME の代わりに tm 構造体を使用できますが、動作が若干異なります。各月は 1 ~ 12 ではなく 0 ~ 11 で表されるため、2 月は 1 となります。SystemTimeToFileTime の代わりに _mkgmtime を呼び出して time_t 構造体を生成してもよいでしょう。重要な相違点としては、うるう年ではない年の 2 月 29 日を渡すと、実行に失敗せず 3 月 1 日を示す値が生成されます。2 月 28 日という戻り値を想定している場合は調整が必要です。
2. 1 年の 1 日ごとに値を格納する配列の宣言
int items[365];
items[dayOfYear - 1] = x;
上記の C コードは C# や別の言語で書き直してある可能性もあります。あるいは、整数の代わりに文字列やその他のデータ型を使用しているかもしれません。重要なのは、固定サイズの配列を宣言し、1 年の 1 日ごとにデータを格納するようになっている点です。うるう年の 366 日目の 12 月 31 日を格納する場所が用意されていないことが問題なのです。
この影響は言語により大きく異なります。C# では IndexOutOfRangeException が発生します。C では、コンパイラの境界チェック オプションが有効になっていないと、バッファー オーバーフローが発生します。その影響は無視できる場合もあれば、重大なものになることもあります。ただし、JavaScript では 366 番目の要素が自動的に追加されるので、この点はそれほど心配はないと考えられます。
データのフィルタリングに関する問題
うるう年のバグは、前年の 2 月 28 日から翌年の 3 月 1 日の間のデータに影響を及ぼす可能性があります。影響を受けるのはデータのフィルタリングで、1 年の日数が常に 365 日または 2 月の日数が常に 28 日だと想定していて、1 日増える場合に対応していないケースです。次の SQL 文を見てください。
SELECT AVG(Total) as AverageOrder, SUM(Total) as GrandTotal
FROM Orders WHERE OrderDate >= @startdate AND OrderDate < @enddate
もし @enddate が今日に設定され、@startdate が今日から 365 日前に設定されたとしたらどうでしょう。指定範囲内にうるう日の 2 月 29 日が含まれていると、1 年間を指定したことにはならなくなります。開始日が 1 年前の日付の 1 日後になってしまうため、1 年分のデータを表示することが目的の場合は値が不正確になります。
このようなバグを評価する際は、バグの影響度を考える必要があります。このケースで言えば、「値はどこに表示されるのか」ということです。たとえば、ダッシュボードで毎日更新される平均発注額のグラフであれば、米国 SEC 提出書類などの企業財務報告書に記載される年間総売上ほど重要ではないかもしれません。こうした評価を行うには当然、アプリケーションの機能やその用途をよく知っている人物の助けが必要です。画一的な対応策はありません。
こうした問題は、次のような手法で解決したくなるものです。
TimeSpan oneYear = TimeSpan.FromDays(isLeapYear(endDate.Year) ? 366 : 365);
DateTime startDate = endDate - oneYear;
しかし、この手法には欠陥があります。その年だけを見て追加する日数を判定することはできません。endDate が 2016-01-01 の場合 2016 年はうるう年ですが、2015-01-01 を得るには、引くのは 365 日です。このような手法ではなく、指定の範囲にうるう日の 2 月 29 日が含まれるかどうかを考えます。単年だけでなく複数年にまたがるケースに手作業で対応しようとするとかなり複雑なコードになるでしょう。
つまるところ、.NET (および他の言語の類似の型) の TimeSpan は絶対時間を表し、「年」も「月」も常用時の単位であるという点が重要となります。1 年または 1 か月の絶対時間は、どの年、または月かによって変化するのです。同じことが夏時間の「日」についてもあてはまりますが、本題から逸れるため今回は取り上げません。
.NET での正しい解決策は次のとおりです。
DateTime startDate = endDate.AddYears(-1);
AddYears メソッドは、何日先に進めるか、負の値の場合に何日前に戻すかを判定するのに必要なロジックをすべて正しく実装しています。
JavaScript で 1 年を加算する場合
JavaScript の開発者は、この場合 moment.js (英語) を使用すべきです。次のように非常にシンプルに処理できます。
var m = moment();
m.add(1, 'years');
未だに従来の面倒な方法で対処することを好む人もいるようです。よく次のようなコードを目にします。
var d = new Date();
d.setFullYear(d.getFullYear() + 1);
これは先に指摘したのと同じ問題です。今日がうるう年の 2 月 29 日の場合、値は 3 月 1 日となります。これで良しとするか悪いとするかは使う側しだいです。その他の日付については、元の値と同じ月の日付が返されます。月初ではなく月末の日付を取得したい場合についてもよく検討してください。
フル ライブラリを使用せずに JavaScript で正しく年数を加算する関数は次の通りです。
function addYears(d, n) {
var m = d.getMonth();
d.setFullYear(d.getFullYear() + n);
if (d.getMonth() !== m)
d.setDate(d.getDate() - 1);
}
// 使用例
var d = new Date();
addYears(d, 1);
上記のコードでは年数を追加した後、3 月にずらされたどうかを確認し、3 月にずらされた場合は修正しています。自分のやろうとしていることが完全に理解できていないなら、追加する正確な日数を割り出そうとするのは控えてください。
その他のよくあるミス
開発者がうるう年について誤解している点はほかにもたくさんあります。
うるう年のバグを見つけるには
このほかに次の 2 つのアプローチについてよく質問を受けます。
静的コード分析
コードをチェックしてうるう年のバグを指摘してくれるツールがあれば便利なのですが、残念ながらそのようなツールの存在は聞いたことがありません。シンプルな文字列検索や正規表現検索も、それ以上のことはできません。
.NET に必要なのは、うるう年、タイム ゾーン、夏時間、構文解析など日時に関する一般的なバグを特定できる包括的な Roslyn アナライザー一式なのですが、私には自作する時間的余裕がありません。将来いつか着手するかもしれませんが、現時点でこうしたツールは存在していません。
C++、JavaScript、他の言語でも類似のツールがあればと便利ですが、耳にしたことはありません。
タイム ワープ
問題を特定するために、時計の針を進めてみるのはどうでしょうか? 一部のシステムには効果的かもしれませんが、このアイデアには次のような問題があります。
上記の理由から、一般にはこのアプローチはお勧めしません。少なくとも誤検知や、コード パスがテストから漏れる原因となる可能性がある外部リソースへの依存関係を必ず考慮してください。
時計をモックする
日付によって動作が異なるコードをテストするには時計をモックします。
これは多くの信頼性の高いシステムでよく採用されているパターンです (「Virtual Clock」と呼ばれることもあります)。重要なのは、現在時刻を知らせるシステム時計は行き当たりばったりに使用すべきではないということです。アプリケーション ロジックで DateTime.Now、DateTime.UtcNow、new Date()、GetSystemTime、または使用する言語の同等のコードを直接呼び出して現在の日時を取得しないでください。
代わりに時計を「サービス」として扱います (ドメイン駆動設計の意味で)。他のサービスと同様、「モック」できるからです。
たとえば、.NET では直接 DateTimeOffset.UtcNow (または類似の API) をアプリケーション ロジックから直接呼び出す代わりに、次を行います。
手間がかかりそうに思えますが、実際に動かしてみると利点がわかります。現在の日時が依存関係にある場合にすべてのコードを確実にテストするにはこの方法しかありません。
他の言語でもパターンは同じはずなので、あえてコードは紹介しませんでした。この優れた実装が Noda Time (英語) に組み込まれています。IClock と SystemClock がメイン アセンブリに、FakeClock が NodaTime.Testing アセンブリにあります。Noda Time は日付関連のテストの点やその他の目的に活用することができます。
JavaScript 開発者は Sinon.JS (英語) や MockDate (英語) などのライブラリや、moment.js のビルトイン モッキング サポート (英語) も検討することをお勧めします。
他の言語のライブラリにも同様の機能があるかもしれません。自力で実装しようとする前に調査してみてください。
まとめ
今年はついにうるう年です。2000 年問題でも 2038 年問題でもありませんが、定期的に対応しなければならない重要な問題です。皆様はこの 4 年の間にどのくらいのコードを作成しましたか? すべてが順調でしたでしょうか? ぜひこの機会にご自身のコードをチェックして、テストを行ってみてください。今まで見逃していた何かが見えてくるかもしれません。
ご不明な点はコメント欄までお寄せいただければ、喜んで回答させていただきます。
この記事の内容の一部は codeofmatt.com (英語) で公開されたものであり、了承のもと再掲載しています。
Please sign in to use this experience.
Sign in