日付を「保存」するには 2 つの方法があります。年、月、日を個別に保存する方法と、「0 ポイント」からの合計日数 (または時間、分、秒、ミリ秒 ... ここで測定単位を選択) を保存する方法です。 . DateTime
たとえば、.NET の は として 100 ナノ秒を使用し、Tick
0001 年 1 月 1 日を「0 ポイント」として使用します。Unix は 1970 年 1 月 1 日から秒を使用します。明らかに、.NET と Unix の方法はメモリ内でよりコンパクトであり (保存する単一の値)、数量を加算/減算する場合 (単純に加算/減算する) に非常に役立ちます。問題は、この内部数値を年/月/日に変換したり、年/月/日をこの数値に変換したりするのがより複雑なことです。
年/月/日から内部番号への変換方法の簡単な例:
public class MyDate
{
public int TotalDaysFrom00010101 { get; private set; }
private const int DaysIn400YearCycle = 365 * 400 + 97;
private const int DaysIn100YearCycleNotDivisibleBy400 = 365 * 100 + 24;
private const int DaysIn4YearCycle = 365 * 4 + 1;
private static readonly int[] DaysPerMonthNonLeap = new[]
{
31,
31 + 28,
31 + 28 + 31,
31 + 28 + 31 + 30,
31 + 28 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31 // Useless
};
private static readonly int[] DaysPerMonthLeap = new[]
{
31,
31 + 29,
31 + 29 + 31,
31 + 29 + 31 + 30,
31 + 29 + 31 + 30 + 31,
31 + 29 + 31 + 30 + 31 + 30,
31 + 29 + 31 + 30 + 31 + 30 + 31,
31 + 29 + 31 + 30 + 31 + 30 + 31 + 31,
31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30,
31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31,
31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,
31 + 29 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30 + 31 // Useless
};
public static bool IsLeap(int year)
{
if (year % 400 == 0) return true;
return (year % 4 == 0) && (year % 100 != 0);
}
public void SetDate(int year, int month, int day)
{
TotalDaysFrom00010101 = 0;
{
int year2 = year - 1;
// Full 400 year cycles
TotalDaysFrom00010101 += (year2 / 400) * DaysIn400YearCycle;
year2 %= 400;
// Remaining 100 year cycles (0...3)
if (year2 >= 100)
{
year2 -= 100;
TotalDaysFrom00010101 += DaysIn100YearCycleNotDivisibleBy400;
if (year2 >= 100)
{
year2 -= 100;
TotalDaysFrom00010101 += DaysIn100YearCycleNotDivisibleBy400;
if (year2 >= 100)
{
year2 -= 100;
TotalDaysFrom00010101 += DaysIn100YearCycleNotDivisibleBy400;
}
}
}
// Full 4 year cycles
TotalDaysFrom00010101 += (year2 / 4) * DaysIn4YearCycle;
year2 %= 4;
// Remaining non-leap years
TotalDaysFrom00010101 += year2 * 365;
}
// Days from the previous month
if (month > 1)
{
// -2 is because -1 is for the 1 January == 0 index, plus -1
// because we must add only the previous full months here.
// So if the date is 1 March 2016, we must add the days of
// January + February, so month 3 becomes index 1.
TotalDaysFrom00010101 += DaysPerMonthNonLeap[month - 2];
if (month > 2 && IsLeap(year))
{
TotalDaysFrom00010101 += 1;
}
}
// Days (note that the "instant 0" in this class is day 1, so
// we must add day - 1)
TotalDaysFrom00010101 += day - 1;
}
public void GetDate(out int year, out int month, out int day)
{
int days = TotalDaysFrom00010101;
// year
{
year = days / DaysIn400YearCycle * 400;
days %= DaysIn400YearCycle;
if (days >= DaysIn100YearCycleNotDivisibleBy400)
{
year += 100;
days -= DaysIn100YearCycleNotDivisibleBy400;
if (days >= DaysIn100YearCycleNotDivisibleBy400)
{
year += 100;
days -= DaysIn100YearCycleNotDivisibleBy400;
if (days >= DaysIn100YearCycleNotDivisibleBy400)
{
year += 100;
days -= DaysIn100YearCycleNotDivisibleBy400;
}
}
}
year += days / DaysIn4YearCycle * 4;
days %= DaysIn4YearCycle;
// Special case: 31 dec of a leap year
if (days != 1460)
{
year += days / 365;
days %= 365;
}
else
{
year += 3;
days = 365;
}
year++;
}
// month
{
bool isLeap = IsLeap(year);
int[] daysPerMonth = isLeap ? DaysPerMonthLeap : DaysPerMonthNonLeap;
for (month = 0; month < daysPerMonth.Length; month++)
{
if (daysPerMonth[month] > days)
{
if (month > 0)
{
days -= daysPerMonth[month - 1];
}
break;
}
}
month++;
}
// day
{
day = days;
day++;
}
}
public void AddDays(int days)
{
TotalDaysFrom00010101 += days;
}
}
ここでのポイントは、400 年の「期間」があり、これらの「サイクル」のそれぞれに日があることを知っているということです365 * 400 + 97
。これらの「サイクル」を差し引いた後、100 年の小さな「サイクル」があり、それぞれに365 * 100 + 24
日数があります。次に、4 年の「サイクル」があり、それぞれに365 * 4 + 3
日数があり、さらに残りの年数 (0...3) がそれぞれ 365 日です。
すべての「前年度全体」の日数を追加した後、「前月全体」の日数を追加できます。ここで、その年がうるう年である可能性を考慮する必要があります。
最後に、選択した日を追加します。
の書き方はGetDate()
練習問題として残します。
今...結果が正しいかどうかを確認するにはどうすればよいですか? 実装に基づいていくつかの単体テストを作成できますDateTime
...次のようなもの:
var date = default(DateTime);
var endDate = new DateTime(2017, 1, 1);
while (date < endDate)
{
int year = date.Year;
int month = date.Month;
int day = date.Day;
int totalDays = (int)(date - default(DateTime)).TotalDays;
var md = new MyDate();
md.SetDate(year, month, day);
if (totalDays != md.TotalDaysFrom00010101)
{
Console.WriteLine("{0:d}: {1} vs {2}", date, totalDays, md.TotalDaysFrom00010101);
}
int year2, month2, day2;
md.GetDate(out year2, out month2, out day2);
if (year != year2 || month != month2 || day != day2)
{
Console.WriteLine("{0:d}: {1:D4}-{2:D2}-{3:D2} vs {4:D4}-{5:D2}-{6:D2}", date, year, month, day, year2, month2, day2);
}
date = date.AddDays(1);
}
(これは単体テストではないことは知っていますがDateTime
、比較を行うための使用方法を示しています)
これは実装方法の例であることに注意してください。私はこのように実装しません。の代わりにコンストラクターを使用して、不変struct
のようなものを作成します。しかし、あなたのものは演習のようであり、小さなステップで進める必要があると思います: 最初に何かを正しく構築し、次にそれを「形式的に正しい」ものにします。したがって、最初のステップは正しい/を構築することです。次に、より形式的に正しいデータ構造に正しくカプセル化できます。この小さなサンプルには、いくつかのパラメーター検証が欠けています: できます:-)DateTime
SetDate()
GetDate()
SetDate()
SetDate(-1, 13, 32)
のGetDate()
構築は非常に複雑でした。思ったよりずっと。やっと書き方が理解できました。最終的には、Microsoft の実装と非常に似ていることに注意してください ( GetDatePart(int part)
.