トップC-Tips > 日付時刻に関する処理

日付時刻に関する処理

1.現在日時の表示

Windows APIGetLocalTime関数を使うと、 現在時刻の取得、表示が簡単に行える。
#include <windows.h>
// 出力例:2011.08.31 20:35:50
int main() {
    SYSTEMTIME st;
    GetLocalTime(&st);
    printf("%d.%02d.%02d %2d:%02d:%02d", st.wYear, 
	st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
}

C言語の標準ライブラリによるプログラムを以下に示す
#include <stdio.h>
#include <time.h>
// 出力例:2011.08.31 20:35:50
int main() {
    time_t timer = time(NULL);
    struct tm *date = localtime(&timer); 
    printf("%d.%02d.%02d %2d:%02d:%02d", 
	date->tm_year+1900, date->tm_mon+1, date->tm_mday, 
	date->tm_hour, date->tm_min, date->tm_sec);
}
構造体 tm はtime.h の中で次のように宣言されている。
struct tm {
  int tm_sec;      /* 秒 [0-61] 最大2秒までのうるう秒を考慮 */
  int tm_min;      /* 分 [0-59] */
  int tm_hour;     /* 時 [0-23] */
  int tm_mday;     /* 日 [1-31] */
  int tm_mon;      /* 月 [0-11] 0から始まることに注意 */
  int tm_year;     /* 年 [1900からの経過年数] */
  int tm_wday;     /* 曜日 [0:日 1:月 ... 6:土] */
  int tm_yday;     /* 年内の通し日数 [0-365] 0から始まることに注意*/
  int tm_isdst;    /* 夏時間が無効であれば 0 */
};

2.日付から曜日を求める[2013.02.02]

例えば "2013-02-02" のように、 文字列表記の日付からその曜日を求めるプログラムを次に示す。
datetime3.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(int argc, char *argv[]) {
    char *weeks[] = { "日", "月", "火", "水", "木", "金", "土" };

    char *date = argc > 1 ? argv[1] : "2013-02-02";
    struct tm tm, *ltime;
    time_t time;

    memset(&tm, 0, sizeof(tm));
    sscanf(date, "%d-%d-%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday);
    tm.tm_year -= 1900;
    tm.tm_mon -= 1;

    time = mktime(&tm);
    ltime = localtime(&time);
    printf("%s %s曜日\n", date, weeks[ltime->tm_wday]);
}

実行例を下に示す。

c:\mh\mcc>datetime3
2013-02-02 土曜日

c:\mh\mcc>datetime3 2013-01-01
2013-01-01 火曜日

この場合も Windows APIを使う方がプログラムはシンプルになる。 struct tm に対応するのが SYSTEMTIME構造体であり、time_t を64ビット化したのが FILETIME構造体である。 下のプログラムでは、SYSTEMTIMEをFILETIMEに変換して、 再びFILETIMEをSYSTEMTIMEに戻している。 一見無意味に思えるかもしれないが、そうではない。 最初の段階では wDayOfWeek要素には値がセットされていない(ゼロ)。 SystemTimeToFileTime関数ではwDayOfWeek要素の値は無視されるので、 問題はない。次のFileTimeToSystemTime関数の実行により、 wDayOfWeek要素の値が正しくセットされる。 つまり、この実現のために、SYSTEMTIME → FILETIME → SYSTEMTIME 変換を行うわけである。
datetime4.c

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

int main(int argc, char *argv[]) {
    char *weeks[] = { "日", "月", "火", "水", "木", "金", "土" };
    SYSTEMTIME  st;
    FILETIME    ft;

    char *date = argc > 1 ? argv[1] : "2013-02-02";
    memset(&st, 0, sizeof(st));
    sscanf(date, "%d-%d-%d", &st.wYear, &st.wMonth, &st.wDay);
    SystemTimeToFileTime(&st, &ft);
    FileTimeToSystemTime(&ft, &st);
    printf("%s %s曜日\n", date, weeks[st.wDayOfWeek]);
}

実行例を下に示す。

c:\mh\mcc>datetime4
2013-02-02 土曜日

c:\mh\mcc>datetime4 2013-01-01
2013-01-01 火曜日

また、Zellerの公式を使って求める方法もある。
datetime5.c

#include <stdio.h>

/* Zellerの公式 */
int zeller(int y, int m, int d){
    if (m == 1 || m == 2){
        y--;
        m += 12;
    }
    return ((y + (int)(y/4) - (int)(y/100) + (int)(y/400) + (int)(2.6*m + 1.6) + d) % 7);
}

int main(int argc, char *argv[]) {
    int year, month, day;
    char *weeks[] = { "日", "月", "火", "水", "木", "金", "土" };

    char *date = argc > 1 ? argv[1] : "2013-02-02";
    sscanf(date, "%d-%d-%d", &year, &month, &day);
    printf("%s %s曜日\n", date, weeks[zeller(year,month,day)]);
}

実行例を下に示す。

c:\mh\mcc>datetime5
2013-02-02 土曜日

c:\mh\mcc>datetime5 2013-01-01
2013-01-01 火曜日

3.基準日時からの通算日数(または秒数など)を求める[2013.02.03]

ある時点からの経過日数や経過秒数などを求める方法は基本的には曜日を求める方法と同じである。

FILETIME構造体は次のように定義された 1601 年 1 月 1 日午前 12 時からの 100 ナノ秒間隔の数 (UTC)を表す64ビット整数である。

typedef struct _FILETIME {
    DWORD dwLowDateTime;   // 下位32ビット
    DWORD dwHighDateTime;  // 上位32ビット
} FILETIME;

一方、time_t で表示される時間は、万国標準時 (UCT)の1970年1月1日の00:00:00からの 経過時間を秒単位で表した32ビット数値である。

従って、ミリ秒単位の短い経過時間ではなく、秒単位以上の経過時間であれば、time_t で十分である。 time_t の値を 86400 ( = 60 * 60 * 24 ) で割れば、1970年1月1日からの通算日数となる。
datetime6.c

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(int argc, char *argv[]) {
    char *date = argc > 1 ? argv[1] : "2013-02-03";
    struct tm tm;

    memset(&tm, 0, sizeof(tm));
    sscanf(date, "%d-%d-%d", &tm.tm_year, &tm.tm_mon, &tm.tm_mday);
    tm.tm_year -= 1900;
    tm.tm_mon -= 1;
    time_t time = mktime(&tm);

    printf("%s %d\n", date, time/86400);
}

実行例を下に示す。

c:\mh\mcc>datetime6
2013-02-03 15738

c:\mh\mcc>datetime6 1970-1-1
1970-1-1 0

c:\mh\mcc>datetime6 1980-1-1
1980-1-1 3651

c:\mh\mcc>datetime6 2000-1-1
2000-1-1 10956

[2013.3.5] なるべくスリムな関数にするには、 次のようにすればよい。

int days(char *date) {
    int y, m, d;
    sscanf(date, "%d-%d-%d", &y, &m, &d);
    struct tm tm = { 0, 0, 0, d, m-1, y-1900 }; 
    return mktime(&tm)/86400;
}

Win32 API を使う場合には64ビット演算が必要となる。 プログラム例を下に示す。
datetime7.c

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

int main(int argc, char *argv[]) {
    SYSTEMTIME  st;
    FILETIME    ft;
    ULONGLONG   t64;

    char *date = argc > 1 ? argv[1] : "2013-02-02";
    memset(&st, 0, sizeof(st));
    sscanf(date, "%d-%d-%d", &st.wYear, &st.wMonth, &st.wDay);
    SystemTimeToFileTime(&st, &ft);
    t64 = (((ULONGLONG)ft.dwHighDateTime) << 32) + ft.dwLowDateTime;
    printf("%s %lld\n", date, (t64/86400/10000000));
}

実行例を下に示す。

c:\mh\mcc>gcc ../www/c-tips/c/datetime7.c -o datetime7.exe

c:\mh\mcc>datetime7
2013-02-02 150512

c:\mh\mcc>datetime7 1601-1-1
1601-1-1 0

c:\mh\mcc>datetime7 1970-1-1
1970-1-1 134774

4.ある日から何日か後(または前)の日付を求める[2013.3.6]

ある日から何日か後(または前)の日付を求めるには次のようにすればよい。 次のプログラムでは、 文字列表記の日付を通算秒に変えて、日数を秒数に変えた値を足す(または引く)。 それを文字列表記する。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void getdate(char *newdate, char *date, int days) {
    int y, m, d;
    sscanf(date, "%d-%d-%d", &y, &m, &d);
    struct tm tm = { 0, 0, 0, d, m-1, y-1900 }; 
    time_t time = mktime(&tm) + 86400 * days;
    struct tm *ltime = localtime(&time);
    sprintf(newdate, "%d-%02d-%02d", 
	ltime->tm_year+1900, ltime->tm_mon+1, ltime->tm_mday);
}
int main(int argc, char *argv[]) {
    char buf[11];
    getdate(buf, argv[1], atoi(argv[2]));
    printf("%sから%s日後は%sです\n", argv[1], argv[2], buf);
}

実行例を下に示す。

c:\mh\mcc>datetime9 2013-03-06 5
2013-03-06から5日後は2013-03-11です

c:\mh\mcc>datetime9 2013-03-06 -10
2013-03-06から-10日後は2013-02-24です

c:\mh\mcc>datetime9 2013-02-24 10
2013-02-24から10日後は2013-03-06です

4.高い精度でプログラムの時間を計測する[2014.08.28]

C言語の伝統的な時間関数clock()の単位はミリ秒である。 C言語が誕生した頃はコンピュータの性能が低いため、これで十分であったのであろう。 現在では、マイクロ秒単位程度の精度が欲しいことがある。

コンパイラやOSなどに依存する可能性があり、移植性が劣るが、次のような方法がある。

GCCの場合、gettimeofday関数を使うと、プログラムの実行時間をマイクロ秒単位で計測できる。 TCCやCL(VCのコマンドラインコンパイラ)ではこの方法は使えなかった。

ただし、clock関数はプロセッサが動いた時間を返すのに対して、 gettimeofday関数は time関数などと同じく、カレンダー時間を返す。

#include <sys/time.h>

struct timeval {
  long tv_sec;
  long tv_usec;
};

struct timeval tv;
gettimeofday(&tv, NULL);
double time = tv.tv_sec + tv.tv_usec/10000000.0;

次のプログラムが CL, GCC でコンパイルできた。

[time01.c]
#include <stdio.h>
#include <windows.h>

int main() {
    ULONGLONG t64;
    FILETIME ft;
    double time;
    GetSystemTimeAsFileTime(&ft);
    t64 = (((ULONGLONG)ft.dwHighDateTime) << 32) + ft.dwLowDateTime;	// 単位 100ナノ秒
    time = (ft.dwHighDateTime/10000000.0)*0x10000*0x10000 + ft.dwLowDateTime/10000000.0;	// 単位 秒
    printf("%.10f, time=%.10f", t64/10000000.0, time);
}
c:\sys>time01
13053792521.8189740000, time=13053792521.8189750000

t64とtimeに僅かな差が出るのは計算誤差である。 TCC では ULONGLONG 型をサポートしていないようだ。 invalid operands for binary operation というコンパイルエラーがでた。

64ビット演算をやめて、timeの計算式を使えば、TCC, GCC, CL のいずれでもコンパイルできる。