UNIX時間と日付時刻の相互変換

フォーラム(掲示板)ルール
フォーラム(掲示板)ルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
アバター
みけCAT
記事: 6273
登録日時: 9年前
住所: 千葉県
連絡を取る:

UNIX時間と日付時刻の相互変換

#1

投稿記事 by みけCAT » 8年前

Windows Vista SP2 Home Premium 32ビット
Dev-C++ 4.9.9.2
gcc version 3.4.2 (mingw-special)
です。

UNIX時間と普通の日付時刻(西暦a年b月c日 d時e分f秒)とを相互変換するプログラムを作っています。
現在のソースコードはこれです。
utconvert.h

コード:

/*utconvert.h*/
#ifndef UTCONVERT_H_GUARD
#define UTCONVERT_H_GUARD

typedef struct {
	int year;
	int month;
	int date;
	int hour;
	int minute;
	int second;
} ut2date_date_t;

long long date2ut(int year,int month,int date,
		int hour,int minute,int second);
void ut2date(ut2date_date_t* result,long long unixTime);

#endif
utconvert.c

コード:

#include "utconvert.h"

/*1日の秒数(=24*60*60)*/
#define SECONDS_PER_DAY 86400

/*1年からyear年まで(両端を含む)の日数を求める*/
static long long getDays(int year) {
	long long result;
	if(year<0)return -getDays(-year)-366+365;
	else if(year==0)return 0;
	result=year*365;/*1年は基本365日*/
	result+=year/4;/*4で割り切れたら閏年*/
	result-=year/100;/*100で割り切れたら閏年ではない*/
	result+=year/400;/*400で割り切れたら閏年*/
	return result;
}

/*1970年1月1日 00:00:00からの経過秒数を返す*/
long long date2ut(int year,int month,int date,
		int hour,int minute,int second) {
	const int monthDays[13]={
			0,31,59,90,120,151,181,212,243,273,304,334,365
	};/*その月までの日数の和(累積和)*/
	long long result;
	/*アクセス違反になる不正入力なら0を返す*/
	if(month<1 || month>12)return 0;
	/* year年1月1日 00:00:00までの日数を求める*/
	result=getDays(year-1)-getDays(1969);
	/*秒に変換*/
	result*=SECONDS_PER_DAY;
	/*月の日数を秒に変換して足す*/
	result+=monthDays[month-1]*SECONDS_PER_DAY;
	/*閏年かつ3月以降なら1日足す*/
	if(year%400==0 || (year%4==0 && year%100!=0)) {
		if(month>=3)result+=SECONDS_PER_DAY;
	}
	/*その月の日数を秒に変換して足す*/
	result+=(date-1)*SECONDS_PER_DAY;
	/*時間を足す*/
	result+=hour*60*60;
	/*分を足す*/
	result+=minute*60;
	/*秒を足す*/
	result+=second;
	/*結果を返す*/
	return result;
}

/*1年からyear年までの閏年の個数を求める*/
static int getLeapYearNum(int year) {
	int result;
	if(year<0)return -getLeapYearNum(-year)-1;
	else if(year==0)return 0;
	result=year/4;/*4で割り切れたら閏年*/
	result-=year/100;/*100で割り切れたら閏年ではない*/
	result+=year/400;/*400で割り切れたら閏年*/
	return result;
}

/*その年が閏年であるかを求める*/
static int isLeapYear(int year) {
	if(year%400==0 || (year%4==0 && year%100!=0))return 1;
	return 0;
}

/*1970年1月1日 00:00:00からの経過秒数を日付にする*/
void ut2date(ut2date_date_t* result,long long unixTime) {
	int monthDays[13]={
			0,31,59,90,120,151,181,212,243,273,304,334,365
	};/*その月までの日数の和(累積和)*/
	int i;
	long long days;
	int yearNum;
	int amariDays;
	int daySeconds;
	int hoseiYear;
	if(!result)return;
	if(unixTime>=0) {
		days=unixTime/SECONDS_PER_DAY;
		daySeconds=(int)(unixTime%SECONDS_PER_DAY);
		yearNum=1970+(int)(days/365);
		amariDays=(int)(days%365);
	} else {
		days=(-unixTime)/SECONDS_PER_DAY;
		daySeconds=(int)(SECONDS_PER_DAY-((-unixTime)%SECONDS_PER_DAY));
		if(daySeconds==SECONDS_PER_DAY) {
			daySeconds=0;
		} else days++;
		yearNum=1970-(int)(days/365)-1;
		amariDays=365-(int)(days%365);
	}
	amariDays-=getLeapYearNum(yearNum-1)-getLeapYearNum(1969);
	while(1) {
		if((amariDays>=0 && amariDays<365) ||
			(amariDays==365 && isLeapYear(yearNum)))break;
		if(amariDays<0) {
			hoseiYear=(-amariDays)/365+1;
			amariDays+=hoseiYear*365;
			amariDays+=getLeapYearNum(yearNum-1)-
				getLeapYearNum(yearNum-hoseiYear-1);
			yearNum-=hoseiYear;
		} else if(amariDays>=365) {
			hoseiYear=amariDays/365;
			amariDays=amariDays%365;
			amariDays-=getLeapYearNum(yearNum+hoseiYear-1)-
				getLeapYearNum(yearNum-1);
			yearNum+=hoseiYear;
		} else break;
	}
	if(isLeapYear(yearNum)) {
		for(i=2;i<=12;i++)monthDays[i]++;
	}
	for(i=1;i<=12;i++) {
		if(amariDays>=monthDays[i-1] && amariDays<monthDays[i])break;
	}
	amariDays-=monthDays[i-1];
	result->year=yearNum;
	result->month=i;
	result->date=amariDays+1;
	result->hour=daySeconds/3600;
	result->minute=(daySeconds/60)%60;
	result->second=daySeconds%60;
}
実行できるexeとソースコードも添付します。(若干書き方が違いますが、内容は同じです)

現状のプログラムで、このJavaScriptのコードといくつかの入力に対する出力を比較したところ、
見た範囲では一致しているようです。
► スポイラーを表示
しかし、本当にこのプログラムでいいのでしょうか?
また、もっといい(見た目が美しい、わかりやすい、効率が良いなど)書き方はありますでしょうか?
お教えていただければ幸いです。
よろしくお願いします。

C言語で、標準ライブラリ(time.hの関数を含む)は使用禁止でお願いします。
添付ファイル
utconvert.zip
プログラムです。
(14.4 KiB) ダウンロード数: 80 回
複雑な問題?マシンの性能を上げてOpenMPで殴ればいい!(死亡フラグ)

アバター
a5ua
記事: 199
登録日時: 9年前

Re: UNIX時間と日付時刻の相互変換

#2

投稿記事 by a5ua » 8年前

コードの外見を見て、気づいた点をいくつか指摘したいと思います。
  1. isLeapYearのように真偽値を返す関数は、単純に論理式の結果を返せば良いと思います。

    コード:

    static int isLeapYear(int year) {
        return year%400==0 || (year%4==0 && year%100!=0);
    }
    
  2. date2utで閏年判定をしているが、せっかく作ったisLeapYearが使われていません。

    コード:

    /*閏年かつ3月以降なら1日足す*/
    if(year%400==0 || (year%4==0 && year%100!=0)) {
    	if(month>=3)result+=SECONDS_PER_DAY;
    }
    
  3. 日数のテーブル monthDaysはグローバル(かつ static const)にしたほうが重複がなくなって良いと思います。
  4. getLeapYearNum(~) - getLeapYearNum(~)というコードがありますが、
    引き算まで含めた、X年からY年までの閏年の個数を求める関数も作ったほうが、
    可読性が上がると思います。

かずま

Re: UNIX時間と日付時刻の相互変換

#3

投稿記事 by かずま » 8年前

みけCAT さんが書きました:また、もっといい(見た目が美しい、わかりやすい、効率が良いなど)書き方はありますでしょうか?
お教えていただければ幸いです。
とりあえず、date2ut() のほうだけ。

コード:

static long long getSec(int y, int m, int d, int hour, int min, int sec)
{
    int t[] = {306,337,0,31,61,92,122,153,184,214,245,275};
    long long s;
    y -= m < 3;
    s = y*365 + y/4 - y/100 + y/400 + t[m-1] + d;
    return ((s * 24 + hour) * 60 + min) * 60 + sec;
}

#define getSec_1970_1_1  62162121600

long long date2ut(int y,int m,int d, int hour, int min, int sec)
{
    return getSec(y, m, d, hour, min, sec) - getSec_1970_1_1;
}

アバター
みけCAT
記事: 6273
登録日時: 9年前
住所: 千葉県
連絡を取る:

Re: UNIX時間と日付時刻の相互変換

#4

投稿記事 by みけCAT » 8年前

>>a5uaさん
ありがとうございます。
アドバイス通りに修正してみました。
どうでしょうか?

コード:

#include "utconvert.h"

/*その月までの日数の和(累積和)*/
static const int monthDays[13]={
	0,31,59,90,120,151,181,212,243,273,304,334,365
};

/*1日の秒数(=24*60*60)*/
#define SECONDS_PER_DAY 86400

/*1年からyear年まで(両端を含む)の日数を求める*/
static long long getDays(int year) {
	long long result;
	if(year<0)return -getDays(-year)-366+365;
	else if(year==0)return 0;
	result=year*365;/*1年は基本365日*/
	result+=year/4;/*4で割り切れたら閏年*/
	result-=year/100;/*100で割り切れたら閏年ではない*/
	result+=year/400;/*400で割り切れたら閏年*/
	return result;
}

/*fromYear+1年からtoYear年までの日数を求める*/
static long long getDays2(int fromYear,int toYear) {
	return getDays(toYear)-getDays(fromYear);
}

/*1年からyear年までの閏年の個数を求める*/
static int getLeapYearNum(int year) {
	int result;
	if(year<0)return -getLeapYearNum(-year)-1;
	else if(year==0)return 0;
	result=year/4;/*4で割り切れたら閏年*/
	result-=year/100;/*100で割り切れたら閏年ではない*/
	result+=year/400;/*400で割り切れたら閏年*/
	return result;
}

/*fromYear+1年からtoYear年までの閏年の個数を求める*/
static int getLeapYearNum2(int fromYear,int toYear) {
	return getLeapYearNum(toYear)-getLeapYearNum(fromYear);
}

/*その年が閏年であるかを求める*/
static int isLeapYear(int year) {
	return (year%400==0 || (year%4==0 && year%100!=0));
}

/*1970年1月1日 00:00:00からの経過秒数を返す*/
long long date2ut(int year,int month,int date,
		int hour,int minute,int second) {
	long long result;
	/*アクセス違反になる不正入力なら0を返す*/
	if(month<1 || month>12)return 0;
	/* year年1月1日 00:00:00までの日数を求める*/
	result=getDays2(1969,year-1);
	/*秒に変換*/
	result*=SECONDS_PER_DAY;
	/*月の日数を秒に変換して足す*/
	result+=monthDays[month-1]*SECONDS_PER_DAY;
	/*閏年かつ3月以降なら1日足す*/
	if(isLeapYear(year)) {
		if(month>=3)result+=SECONDS_PER_DAY;
	}
	/*その月の日数を秒に変換して足す*/
	result+=(date-1)*SECONDS_PER_DAY;
	/*時間を足す*/
	result+=hour*60*60;
	/*分を足す*/
	result+=minute*60;
	/*秒を足す*/
	result+=second;
	/*結果を返す*/
	return result;
}

/*1970年1月1日 00:00:00からの経過秒数を日付にする*/
void ut2date(ut2date_date_t* result,long long unixTime) {
	int i;
	long long days;
	int yearNum;
	int amariDays;
	int daySeconds;
	int hoseiYear;
	int is2_29;
	if(!result)return;
	if(unixTime>=0) {
		days=unixTime/SECONDS_PER_DAY;
		daySeconds=(int)(unixTime%SECONDS_PER_DAY);
		yearNum=1970+(int)(days/365);
		amariDays=(int)(days%365);
	} else {
		days=(-unixTime)/SECONDS_PER_DAY;
		daySeconds=(int)(SECONDS_PER_DAY-((-unixTime)%SECONDS_PER_DAY));
		if(daySeconds==SECONDS_PER_DAY) {
			daySeconds=0;
		} else days++;
		yearNum=1970-(int)(days/365)-1;
		amariDays=365-(int)(days%365);
	}
	amariDays-=getLeapYearNum2(1969,yearNum-1);
	while(1) {
		if((amariDays>=0 && amariDays<365) ||
			(amariDays==365 && isLeapYear(yearNum)))break;
		if(amariDays<0) {
			hoseiYear=(-amariDays)/365+1;
			amariDays+=hoseiYear*365;
			amariDays+=getLeapYearNum(yearNum-1)-
				getLeapYearNum(yearNum-hoseiYear-1);
			yearNum-=hoseiYear;
		} else if(amariDays>=365) {
			hoseiYear=amariDays/365;
			amariDays=amariDays%365;
			amariDays-=getLeapYearNum(yearNum+hoseiYear-1)-
				getLeapYearNum(yearNum-1);
			yearNum+=hoseiYear;
		} else break;
	}
	is2_29=0;
	if(isLeapYear(yearNum) && amariDays>=59) {
		if(amariDays==59)is2_29=1;
		amariDays--;
	}
	for(i=1;i<=12;i++) {
		if(amariDays>=monthDays[i-1] && amariDays<monthDays[i])break;
	}
	amariDays-=monthDays[i-1];
	if(is2_29)amariDays++;
	result->year=yearNum;
	result->month=i;
	result->date=amariDays+1;
	result->hour=daySeconds/3600;
	result->minute=(daySeconds/60)%60;
	result->second=daySeconds%60;
}
>>かずまさん
なるほど。この形はツェラーの公式の応用っぽいですね。

コード:

y -= m < 3;
は環境依存になりそうで怖いので、素直に

コード:

if(m<3)y--;
でどうでしょうか?
添付ファイル
utconvert.zip
修正したプログラムです。
(13.71 KiB) ダウンロード数: 78 回
複雑な問題?マシンの性能を上げてOpenMPで殴ればいい!(死亡フラグ)

かずま

Re: UNIX時間と日付時刻の相互変換

#5

投稿記事 by かずま » 8年前

構造体名と関数名を変えてありますが、こんなふうにしてみました。

コード:

typedef struct DateTime {
    int year, month, day, hour, min, sec;
} DateTime;

static int t[] = { 306,337,0,31,61,92,122,153,184,214,245,275 };

long long date2sec(int y, int m, int d, int hour, int min, int sec)
{
    const long base = 1969*365L + 1969/4 - 1969/100 + 1969/400 + 306 + 1;
    long long days;
    if (m < 3) y--;
    days = y*365L + y/4 - y/100 + y/400 + t[m-1] + d - base;
    return days * (24*60*60) + ((hour * 60 + min) * 60 + sec);
}

void sec2date(DateTime* date, long long sec)
{
    long y, m, d, n, leap;
    const long long base = 1969*365L + 1969/4 - 1969/100 + 1969/400 + 306;
    sec += base * (24 * 60 * 60);
    date->sec = sec % 60; sec /= 60;
    date->min = sec % 60; sec /= 60;
    date->hour   = sec % 24; d = sec / 24;
    n = d / (365L * 400 + 97); d %= 365L * 400 + 97; y = n * 400;
    n = d / (365L * 100 + 24); d %= 365L * 100 + 24; y += n * 100;
    leap = (n == 4);
    n = d / (365 * 4 + 1); d %= 365 * 4 + 1; y += n * 4;
    n = d / 365; d %= 365; y += n;
#if 0
    if (n == 4) leap = 1;
    n = d / (31 + 30 + 31 + 30 + 31); d %= (31 + 30 + 31 + 30 + 31); m = n * 5;
    n = d / (31 + 30); d %= 31 + 30; m += n * 2;
    n = d / 31; d %= 31; m += n;
    if (leap) m = 2, d = 29;
    else if (m <= 9) m += 3, d++;
    else y++, m -= 9, d++;
#else
    if (leap || n == 4) m = 2, d = 29;
    else {
        m = (d * 5 + 2) / 153 + 3;
        if (m > 12) m -= 12, y++;
        d -= t[m-1] - 1;
    }
#endif
    date->year = y; date->month = m; date->day = d;
}

#include <stdio.h>

void test(int y, int m, int d)
{
    DateTime date;
    long long s = date2sec(y, m, d, 0, 0, 0);
    sec2date(&date, s);
    printf("%d/%02d/%02d %12lld  %d/%02d/%02d\n",
            y, m, d, s, date.year, date.month, date.day);
}

int main(void)
{
    test(1900, 2, 28);
    test(1900, 2, 29); // 100年に一度は平年だからありえない
    test(1900, 3, 1);

    test(1969, 12, 31);
    test(1970, 1, 1);  // 基準日
    test(1970, 1, 2);

    test(2000, 2, 28);
    test(2000, 2, 29); // 400年に一度の閏年
    test(2000, 3, 1);

    test(2011, 2, 28);
    test(2011, 2, 29); // 平年だからありえない
    test(2011, 3, 1);

    test(2120, 2, 28);
    test(2120, 2, 29); // 4年に一度の閏年
    test(2120, 3, 1);

    return 0;
}

アバター
みけCAT
記事: 6273
登録日時: 9年前
住所: 千葉県
連絡を取る:

Re: UNIX時間と日付時刻の相互変換

#6

投稿記事 by みけCAT » 8年前

>>かずまさん
すごく・・・複雑です・・・
なんでだろう?
マジックナンバーだらけで、しかもコメントが無いからかな?
複雑な問題?マシンの性能を上げてOpenMPで殴ればいい!(死亡フラグ)

かずま

Re: UNIX時間と日付時刻の相互変換

#7

投稿記事 by かずま » 8年前

みけCAT さんが書きました:マジックナンバーだらけで、しかもコメントが無いからかな?
n = d / (365L * 400 + 97); d %= 365L * 400 + 97; y = n * 400; を
n = d / 146097; d % = 14697; y = n * 400; と書けば、146097 はマジックナンバーです。しかし、
閏年の計算をしようとする者にとって、365, 400, 100, 4, 31, 30 はマジックナンバーではありません。
まあ、97 はちょっとマジックかもしれませんが。

では、コメントつけてみます。

コード:

void sec2date(DateTime* date, long long sec)
{
    long y, m, d, n, leap;
    const long long BASE = 1969*365L + 1969/4 - 1969/100 + 1969/400 + 306;
	    // BASE は 0年3月1日から 1970年1月1日までの日数
	const long Y400 = 365L * 400 + 97;      // 400年間の日数、閏年は97回
	const long Y100 = 365L * 100 + 24;      // 100年間の日数、閏年は24回
	const long Y4   = 365L *   4 +  1;      //   4年間の日数、閏年は 1回
	const long Y1   = 365;                  //   1年間の日数
	const long M5 = 31 + 30 + 31 + 30 + 31; // 3月~7月、8月~12月の5か月ずつの日数
	const long M2 = 32 + 30;  // 3月4月、5月6月、8月9月、10月11月、1月2月 の 2か月ずつの日数
	const long M1 = 31;       // 3月、5月、7月、8月、10月、12月、1月 の 1か月の日数

    sec += BASE * (24 * 60 * 60);           // sec は 0年3月1日からの秒数
    date->sec  = sec % 60; sec /= 60;       // sec は 0年3月1日からの分数
    date->min  = sec % 60; sec /= 60;       // sec は 0年3月1日からの時間数
    date->hour = sec % 24; d = sec / 24;    // d は 0年3月1日からの日数

    n = d / Y400; d %= Y400; y  = n * 400;  // y は 400年単位の年数、400年内の日数
    n = d / Y100; d %= Y100; y += n * 100;  // y は 100年単位の年数を加えたもの、d は 100年内の日数
    leap = (n == 4);                        // 400年に一度の閏年が見つかったかどうか
    n = d / Y4;   d %= Y4;   y += n * 4;    // y は 4年単位の年数を加えたもの、d は 4年内の日数
    n = d / Y1;   d %= Y1;   y += n;        // y は 年数
#if 0
    if (n == 4) leap = 1;                   // 4年に一度の閏年が見つかったかどうか
    n = d / M5;   d %= M5;   m  = n * 5;    // m は 5か月単位の月数、d は 5か月内の日数
	n = d / M2;   d %= M2;   m += n * 2;    // m は 2か月単位の月数を加えたもの、d は 2か月内の日数
    n = d / M1;   d %= M1;   m += n + 3;    // m は 3月から始まる月、d は 月内の日数
    if (leap) m = 2, d = 29;                // 閏年
    else if (m <= 12) d++;                  // 3月~12月は、日を 1 からに始まるようにする
    else y++, m -= 12, d++;                 // 13月14月は 1月2月しに、年を翌年にし、日を 1 からにする
#else
    if (leap || n == 4) m = 2, d = 29;      // 閏年
    else {
        m = (d * 5 + 2) / 153 + 3;          // 年内の日数から、月を求める式、3月から始まる
        if (m > 12) m -= 12, y++;           // 13月14月は 1月2月にし、年を翌年にする
        d -= t[m-1] - 1;                    // 年内の日数から、前月までの日数を引き、当月の日数にする
    }
#endif
    date->year = y; date->month = m; date->day = d;
}
Y0 は 0年3月1日~1年2月28日の日数
Y4 は Y0 * 4 + 1 で、0年3月1日~4年2月29日の日数
Y100 は Y4 * 25 - 1 で、0年3月1日~100年2月28日の日数
Y400 は Y100 * 4 + 1 で、0年3月1日~400年2月29日の日数

0年3月1日からの通算日は Y400 の繰り返しだから、その通算日を Y400 で割って、
例えば商が 4 なら、余りは 1600年 3月1日からの日数になります。
その日数は Y100 の繰り返しだから、それを Y100 で割って、
例えば商が 3 なら、余りは 1900年 3月1日からの日数になります。
その日数は Y4 の繰り返しだから、それを Y4 で割って、
例えば商が 20 なら、余りは 1980年 3月1日からの日数になります。
その日数は Y1 の繰り返しだから、それをY1 で割って、
例えば商が 3 なら、余りは 1983年 3月1日からの日数になります。

月は、3月~7月と 8月~12月が 31 + 30+ 31 + 30 + 31 でパターンが一緒です。
1月~2月も 31 から始まるので同じパターンの最初の部分といえるでしょう。
ここまで書けば、あとはプログラムから分かるのではありませんか?

ただ、月を求めるのに 3回も割り算をするのは効率が悪いので、
ツェラーの公式の逆のようなマジックエクスプレッションを使ってみました。
1900年3月1日からの日数になります。

アバター
みけCAT
記事: 6273
登録日時: 9年前
住所: 千葉県
連絡を取る:

Re: UNIX時間と日付時刻の相互変換

#8

投稿記事 by みけCAT » 8年前

なんかすごいです・・・
またじっくり考えてみます。
複雑な問題?マシンの性能を上げてOpenMPで殴ればいい!(死亡フラグ)

かずま

Re: UNIX時間と日付時刻の相互変換

#9

投稿記事 by かずま » 8年前

間違いがいくつかありました。

const long M2 = 31 + 30; // 3月4月、5月6月、8月9月、10月11月、1月2月 の 2か月ずつの日数

n = d / Y400; d %= Y400; y = n * 400; // y は 400年単位の年数、d は 400年内の日数

n = d / Y1; d %= Y1; y += n; // y は 年数、d は 1年内の日数

ただ、月を求めるのに 3回も割り算をするのは効率が悪いので、
ツェラーの公式の逆のようなマジックエクスプレッションを使ってみました。
1900年3月1日からの日数になります。

アバター
みけCAT
記事: 6273
登録日時: 9年前
住所: 千葉県
連絡を取る:

Re: UNIX時間と日付時刻の相互変換

#10

投稿記事 by みけCAT » 8年前

けっこう時間が経ったので、解決にします。
複雑な問題?マシンの性能を上げてOpenMPで殴ればいい!(死亡フラグ)

閉鎖

“C言語何でも質問掲示板” へ戻る