うるう年のバグとは、通常 うるう年 のコンテキスト内で実行されると、問題のある意図しない結果を引き起こすコードの欠陥であり、通常は発作的なグレゴリオ暦システム内で発生します。
最後のうるう年は2016年でした。次のうるう年は2020年と2024年です。
うるう年に固有の2つの属性があります。
この投稿は、うるう年のバグの性質、さまざまな言語でのそれらの外観、およびそれらの修正方法を他の人が理解できるようにすることを目的としています。
うるう年のバグは通常、2つの影響カテゴリに分類されます。
それぞれの回答で、プログラミング言語および/またはプラットフォーム、および上記で定義された影響カテゴリを示してください。 (既存の回答で使用されているテンプレートに従ってください。)
言語と欠陥のタイプごとに1つの個別の回答を作成し、お気に入り、特に個人的に遭遇したものに投票してください(可能な場合は逸話を付けてコメントを残してください)。
始めにいくつかの答えをシードし、時間をかけて追加の例で更新します。
影響カテゴリ1
不良コード
DateTime dt = DateTime.Now;
DateTime result = new DateTime(dt.Year + 1, dt.Month, dt.Day);
このコードは、dt
が2月29日になるまで正しく機能します。次に、存在しない一般的な年の2月29日を作成しようとします。 DateTime
コンストラクターはArgumentOutOfRangeException
をスローします。
バリエーションには、年、月、日のパラメーターを受け入れるDateTime
またはDateTimeOffset
コンストラクターの任意の形式が含まれます。これらの値は、さまざまなソースから派生するか、全体としての有効性に関係なく操作されます。
修正コード
DateTime dt = DateTime.Now;
DateTime result = dt.AddYears(1);
1つのバリエーションは、ハンチングを考慮せずにユーザーの現在の誕生日を決定する場合です(2月29日に生まれた人)。また、入社日、勤務日、請求日など、その他の種類の記念日にも適用されます。
不良コード
DateTime birthdayThisYear = new DateTime(DateTime.Now.Year, dob.Month, dob.Day);
このアプローチは、一般的な年に2月28日を使用する次のような調整が必要です。 (ただし、ユースケースによっては3月1日が望ましい場合があります。)
修正コード
int year = DateTime.Now.Year;
int month = dob.Month;
int day = dob.Day;
if (month == 2 && day == 29 && !DateTime.IsLeapYear(year))
day--;
DateTime birthdayThisYear = new DateTime(year, month, day);
修正されたコード(代替実装)
DateTime birthdayThisYear = dob.AddYears(DateTime.Now.Year - dob.Year);
SYSTEMTIME
構造体操作影響カテゴリ1
不良コード
SYSTEMTIME st;
FILETIME ft;
GetSystemTime(&st);
st.wYear++;
SystemTimeToFileTime(&st, &ft);
このコードは、st
が2月29日になるまで正しく機能します。次に、存在しない一般的な年の2月29日を作成しようとします。これをSYSTEMTIME
構造体を受け入れる関数に渡すと失敗する可能性があります。
たとえば、ここに示すSystemTimeToFileTime
呼び出しはエラーコードを返します。その戻り値はチェックされていないため(これは非常に一般的です)、ft
が初期化されないままになります。
修正されたコード
SYSTEMTIME st;
FILETIME ft;
GetSystemTime(&st);
st.wYear++;
bool isLeapYear = st.wYear % 4 == 0 && (st.wYear % 100 != 0 || st.wYear % 400 == 0);
st.wDay = st.wMonth == 2 && st.wDay == 29 && !isLeapYear ? 28 : st.wDay;
bool ok = SystemTimeToFileTime(&st, &ft);
if (!ok)
{
// handle error
}
この修正により、一般的な年の2月29日がチェックされ、2月28日に修正されます。
影響カテゴリ1
コードの欠陥
from datetime import date
today = date.today()
later = today.replace(year = today.year + 1)
このコードは、today
が2月29日になるまで正しく機能します。次に、存在しない一般的な年の2月29日を作成しようとします。 date
コンストラクターは、メッセージ"day is out of range for month"
とともにValueError
を発生させます。
バリエーション:
from datetime import date
today = date.today()
later = date(today.year + 1, today.month, today.day)
修正コード
追加のライブラリを使用せずに、エラーをトラップできます。
from datetime import date
today = date.today()
try:
later = today.replace(year = today.year + 1)
except ValueError:
later = date(today.year + 1, 2, 28)
ただし、通常は dateutil などのライブラリを使用することをお勧めします。
from datetime import date
from dateutil.relativedelta import relativedelta
today = date.today()
later = today + relativedelta(years=1)
例ではC#を使用して説明していますが、パターンはすべての言語に共通です。
影響カテゴリ2
コードの欠陥
bool isLeapYear = year % 4 == 0;
このコードは、うるう年が正確に4年ごとに発生すると誤って想定しています。これは、ビジネスやコンピューティングで最も一般的に使用されているグレゴリオ暦システムには当てはまりません。
修正コード
完全なアルゴリズム( Wikipediaから )は次のとおりです。
if(yearは4で割り切れない)then(それは一般的な年です)
else if(yearは100で割り切れません)その後(うるう年です)
else if(yearは400で割り切れない)その後(それは一般的な年です)
else(うるう年です)
このアルゴリズムの1つの実装は次のとおりです。
bool isLeapYear = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
多くのプラットフォームでは、この関数が組み込まれています。たとえば、.Netでは、以下が推奨されます。
bool isLeapYear = DateTime.IsLeapyear(year);
これは私がたまたま見つけた興味深いものです。可能な限りUTCを使用するためのもう1つの議論。
// **** Appears to "Leap" forward **** //
moment('2020-02-29T00:00:00Z').toISOString();
// or
moment('2020-02-29T00:00:00Z').toJSON();
// 2020-02-29T00:00:00.000Z
moment('2020-02-29T00:00:00Z').add(1, 'year').toISOString();
// or
moment('2020-02-29T00:00:00Z').add(1, 'year').toJSON();
// 2021-03-01T00:00:00.000Z
// **** Falls back **** //
moment.utc('2020-02-29T00:00:00Z').toISOString();
// or
moment.utc('2020-02-29T00:00:00Z').toJSON();
// 2020-02-29T00:00:00.000Z
moment.utc('2020-02-29T00:00:00Z').add(1, 'year').toISOString();
// or
moment.utc('2020-02-29T00:00:00Z').add(1, 'year').toJSON();
// 2021-02-28T00:00:00.000Z
与えられたC#のデフォルトの動作はフォールバックです...
DateTime dt = new DateTime(2020, 02, 29, 0, 0, 0, DateTimeKind.Utc);
DateTime result = dt.AddYears(1);
// 2021-02-28T00:00:00.0000000Z
これは、フロントエンドとバックエンドがフォールバックするか、1日進むかを確実に合意するために重要です。
影響カテゴリ2
コードの欠陥
var dt = new Date();
dt.setFullYear(dt.getFullYear() + 1);
このコードは、dt
が2020-02-29
などの2月29日になるまで正しく機能します。次に、年を2021に設定しようとします。2021-02-29
が存在しないため、Date
オブジェクトは次の有効な日付である2020-03-01
にロールフォワードします。
これは、意図された動作である場合があります。それ以外の場合、1日休んでも影響はごくわずかです(有効期限など)。
ただし、多くの場合、年を進めるときの意図は、月と日のほぼ同じ位置に留まることです。言い換えると、2月の終わり(2020-02-29
)から開始して1年進んだ場合、結果は2月の終わり(2021-02-28
)でもあり、2月の終わりではなく、 3月(2021-03-01
)。
修正コード
2月末の動作を維持しながらJavaScriptに年を追加するには、次の関数を使用します。
function addYears(dt, n) {
var m = dt.getMonth();
dt.setFullYear(dt.getFullYear() + n);
if (dt.getMonth() !== m)
dt.setDate(dt.getDate() - 1);
}
// example usage
addYears(dt, 1);
次のように、元のオブジェクトを変更しないコードが含まれている場合があります。
コードの欠陥
var dt = new Date();
var result = new Date(dt.getFullYear() + 1, dt.getMonth(), dt.getDate());
2月末の動作を保持する不変の形式は次のとおりです。
修正コード
function addYears(dt, n) {
var result = new Date(dt);
result.setFullYear(result.getFullYear() + n);
if (result.getMonth() !== dt.getMonth())
result.setDate(result.getDate() - 1);
return result;
}
// example usage
var result = addYears(dt, 1);
JavaScriptには、 Luxon 、 Date-Fns 、 Moment 、および js-Joda など、多くの日付/時刻ライブラリがあります。 。これらのライブラリはすべて、年の追加関数に2月末の動作をすでに使用しています。 3月の初めの動作を希望しない限り、追加の調整は必要ありません。