Hatena::Groupcprogramming

Going My C Way このページをアンテナに追加 RSSフィード

2011年07月17日(日)

2000年1月1日からの経過日数を求める

| 16:20 |  2000年1月1日からの経過日数を求める - Going My C Way を含むブックマーク はてなブックマーク -  2000年1月1日からの経過日数を求める - Going My C Way  2000年1月1日からの経過日数を求める - Going My C Way のブックマークコメント

2000年1月1日を0日目とした経過日数を求める実装です。

入力は、年(西暦)、月、日。対応範囲は 2000年1月1日から2049年12月31日まで。

まずは、年、月、日を引数として経過日数を求める関数 calc_days() です。

この関数引数チェックはしません。

#define EPOCH_YEAR    (2000)

#define MONTHS_OF_YEAR  (12)
#define DAYS_OF_4YEARS  (1 + (365 * 4))

/* 閏年の1月から3年後の12月まで、その月の1日での経過日数 */
static const int days[] = {
/*   1月   2月   3月   4月   5月   6月   7月   8月   9月  10月  11月  12月 */
/*閏*/ 0,   31,   60,   91,  121,  152,  182,  213,  244,  274,  305,  335,
     366,  397,  425,  456,  486,  517,  547,  578,  609,  639,  670,  700,
     731,  762,  790,  821,  851,  882,  912,  943,  974, 1004, 1035, 1065,
    1096, 1127, 1155, 1186, 1216, 1247, 1277, 1308, 1339, 1369, 1400, 1430,
};

int calc_days(
        int year   /* 西暦年 (2000..2049) */,
        int month  /* 月     (1..12)      */,
        int day    /* 日     (1..m) (注) m は月により異なる */)
{
    const int y =  (year - EPOCH_YEAR) / 4;
    const int m = ((year - EPOCH_YEAR) % 4) * MONTHS_OF_YEAR + (month - 1);
    const int d =  (day - 1);

    return (y * DAYS_OF_4YEARS) + days[m] + d;
}

次に、年、月、日が妥当か判定する関数 is_valid_date() です。

#define FIRST_YEAR    (EPOCH_YEAR)
#define LAST_YEAR     (FIRST_YEAR + 49)
#define FIRST_MONTH   (1)
#define LAST_MONTH    (12)
#define FIRST_DAY     (1)

/* 閏年の1月から3年後の12月まで、その月の末日 */
static const int last_day[][12] = {
/*   1月   2月   3月   4月   5月   6月   7月   8月   9月  10月  11月  12月 */
    { 31,   29,   31,   30,   31,   30,   31,   31,   30,   31,   30,   31,},
    { 31,   28,   31,   30,   31,   30,   31,   31,   30,   31,   30,   31,},
    { 31,   28,   31,   30,   31,   30,   31,   31,   30,   31,   30,   31,},
    { 31,   28,   31,   30,   31,   30,   31,   31,   30,   31,   30,   31,},
};

/* 入力の、西暦年、月、日、が妥当な場合は 1 を、それ以外の場合は 0 を返す */
int is_valid_date(int year, int month, int day)
{
    return ((year  >= FIRST_YEAR)  && (year  <= LAST_YEAR))
        && ((month >= FIRST_MONTH) && (month <= LAST_MONTH))
        && ((day   >= FIRST_DAY)   && (day   <= last_day[year%4][month-1]));
}

calc_days の前処理で is_valid_date() を実行する関数 elapsed_days() です。

入力が妥当値でない場合は ERROR_DATE(-1)を返します。

#define ERROR_DATE  (-1)

int elapsed_days(
        int year   /* 西暦年 (2000..2049) */,
        int month  /* 月     (1..12)      */,
        int day    /* 日     (1..m) (注) m は月により異なる */)
{
    const int can_calc = is_valid_date(year, month, day);

    return can_calc ? calc_days(year, month, day) : ERROR_DATE;
}

----

ついでに、曜日を求める実装です。

day_of_week() は日曜(0)、月曜(1)、...土曜(6) を返します。入力エラーの場合は ERROR_DATE を返します。

#define EPOCH_DAY_OF_WEEK  (6)  /* 2000年1月1日は土曜日(6) */
#define DAYS_OF_WEEK       (7)

int day_of_week(int year, int month, int day)
{
    const int days = elapsed_days(year, month, day);

    return (days >= 0) ? (days + EPOCH_DAY_OF_WEEK) % DAYS_OF_WEEK : ERROR_DATE;
}

さらに、曜日を文字列で返す。str_wday() です。入力エラーの場合は "???" を返します。

const char *str_wday(int year, int month, int day)
{
    const char * const wdays[] = {
        "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
    };

    const int wday = day_of_week(year, month, day);

    return (wday >= 0) ? wdays[wday] : "???";
}

ついでに、main() もつくります。main() をコンパイルするときは TEST_MAIN を define してください。

#if defined(TEST_MAIN)
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int year  = (argc > 1) ? atoi(argv[1]) : 2000;
    int month = (argc > 2) ? atoi(argv[2]) : 1;
    int day   = (argc > 3) ? atoi(argv[3]) : 1;

    const int days   = elapsed_days(year, month, day);
    const char *wday = str_wday(year, month, day);

    printf("%04d/%02d/%02d = %5d (%s)\n", year, month, day, days, wday);

    return EXIT_SUCCESS;
}
#endif

コンパイル。ソースファイル名は datetime.c

$ make CFLAGS=-DTEST_MAIN datetime
gcc -DTEST_MAIN    datetime.c   -o datetime

実行例。2000年1月1日から2003年12月31日まで表示する。

$ seq -w 2000 2003 | while read yyyy
> do
>     seq 1 12 | while read mm
>     do
>         seq 1 31 | while read dd
>         do
>             ./datetime $yyyy $mm $dd
>         done
>     done
> done
2000/01/01 =     0 (Sat)
2000/01/02 =     1 (Sun)
2000/01/03 =     2 (Mon)
2000/01/04 =     3 (Tue)
2000/01/05 =     4 (Wed)
  :
2000/02/29 =    59 (Tue)
2000/02/30 =    -1 (???)
2000/02/31 =    -1 (???)
2000/03/01 =    60 (Wed)
2000/03/02 =    61 (Thu)
  :
2000/12/30 =   364 (Sat)
2000/12/31 =   365 (Sun)
2001/01/01 =   366 (Mon)
2001/01/02 =   367 (Tue)
  :
2001/02/27 =   423 (Tue)
2001/02/28 =   424 (Wed)
2001/02/29 =    -1 (???)
2001/02/30 =    -1 (???)
2001/02/31 =    -1 (???)
2001/03/01 =   425 (Thu)
2001/03/02 =   426 (Fri)
   :
2003/12/28 =  1457 (Sun)
2003/12/29 =  1458 (Mon)
2003/12/30 =  1459 (Tue)
2003/12/31 =  1460 (Wed)

OK です。

----

うるう年は、

  • 4 で割り切れる年は閏年
  • ただし、100 で割り切れる年は平年
  • ただし、400 で割り切れる年は閏年

となってるので、2100年を含めると上のロジックでは対応できません。

なので、対応範囲は 2049年までとしてます。

(符号付き32ビット整数で秒を表すと68年くらい表わせるので、2000年 + (きりのいい)50年間 = 2049年です)

----

おまけ。

/* 閏年の1月から3年後の12月まで、その月の1日での経過日数 */ のテーブルを出力する ruby スクリプト

#!/usr/bin/env ruby

days = [
  [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
  [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
  [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
  [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
]

d = 0
4.times do |y|
  12.times do |m|
    print " %4d," % d
    d += days[y][m]
  end 
  puts ""
end

# vi:set ts=2 sw=2 et fenc=UTF-8: