计算C中两个日期之间的天数–将日期转换为天



最近,我发现自己需要知道我用C.编写的一个小程序的两个日期之间的间隔天数

我在网上搜索了一个解决方案,但没有找到C.

我该怎么做?

基本上有两种方法:

  1. 使用标准工具。使用标准库函数mktime构造对应于每个日期的time_t值。然后减去,并将差值转换为天数单位
  2. 编写你自己的小代码来构造朱利安日数的形式。再次做减法。这至少有点复杂,充满了难以把握的繁琐小细节,但这是一项很棒且令人满意的练习。(至少,如果你是一个时间迷,那就称之为"令人满意"。)

方法1如下所示:

#include <stdio.h>
#include <time.h>
#include <math.h>
int main()
{
struct tm tm1 = { 0 };
struct tm tm2 = { 0 };
/* date 1: 2022-09-25 */
tm1.tm_year = 2022 - 1900;
tm1.tm_mon = 9 - 1;
tm1.tm_mday = 25;
tm1.tm_hour = tm1.tm_min = tm1.tm_sec = 0;
tm1.tm_isdst = -1;
/* date 2: 1990-10-02 */
tm2.tm_year = 1990 - 1900;
tm2.tm_mon = 10 - 1;
tm2.tm_mday = 2;
tm2.tm_hour = tm2.tm_min = tm2.tm_sec = 0;
tm2.tm_isdst = -1;
time_t t1 = mktime(&tm1);
time_t t2 = mktime(&tm2);
double dt = difftime(t1, t2);
int days = round(dt / 86400);
printf("difference: %d daysn", days);
}

tm1tm2一样填写struct tm有点棘手。tm_year字段是从1900开始计数的,因此在填充时必须减去1900,如图所示。tm_mon字段基于0,因此您必须从月数中减去1。你必须选择一个时间;在这里,我任意选择了午夜00:00:00。最后,在tm_isdst字段中,您必须指定输入的是标准时间还是夏令时(0或1),或者您不知道或不关心(-1)。

然后函数mktime返回与您指定的日期和时间相对应的time_t值。自1970年1月1日以来,time_t通常是Unix的UTC秒计数,尽管严格来说,C标准规定它可以有任何实现定义的编码,因此减去两个time_t值最安全的方法是使用difftime函数,该函数返回保证以秒为单位的差。正如每一个书呆子都知道的那样,一天有86400秒,所以把以秒为单位的差值除以86400,你就得到了天数。

这一切看起来都很简单,尽管有一些微妙之处需要注意。如果你仔细观察你得到的difftime差异,你会发现它们通常是而不是86400的精确倍数,如果你指定的两个日期恰好跨越夏令时转换。(如果是这样的话,你至少有一天的时间是23或25小时。)这就是为什么将除以86400的结果四舍五入至关重要,如示例代码所示。通常,将tm_hour填充为12而不是0也是一个非常好的主意,因为使用中午(即在一天的中间,而不是在午夜转换一天)也有助于避免各种异常。

这就是方法1。这是方法2的开始。我们将编写一个函数CCD_;儒略日;数字一般来说,儒略日数是指每天都在增加的数字,而不考虑月和日的边界。例如,此makejd函数将计算今天(2022年9月25日)的第154035天。9月1日是第154011天,在此之前的一天,即8月31日,是第154010天。出于我们将要了解的原因,这些日子的数字可以追溯到1601年,1月1日是第一天。

(在现实世界中,不同的儒略日编号方案使用不同的基准日。官方儒略日数字可以追溯到公元前4713年;还有一个基于1858年的官方"改良儒略日"或MJD数字。)

不管怎样,这里是makejd:

#define BASEYEAR 1601      /* *not* arbitrary; see explanation in text */
long int makejd(int year, int month, int day)
{
long int jdnum = 0;
jdnum += (year - BASEYEAR) * 365L;
jdnum += (year - BASEYEAR) / 4;
jdnum -= (year - BASEYEAR) / 100;
jdnum += (year - BASEYEAR) / 400;
jdnum += monthcount(month - 1, year);
jdnum += day;
return jdnum;
}

我喜欢这个函数,因为它解决的问题一开始听起来很复杂,但这个函数本身看起来有点合理,一旦你了解了它的工作原理,它就非常简单了。

基本上,为了计算总天数,从那个基准日期到我们关心的日期,我们有三件事需要担心:

  1. 我们全年有365天的时间
  2. 然后,我们将有一些对应于从年初到我们所在月份的完整月份的天数
  3. 最后,我们在一个月里还有一些日子

(当然还有一些闰年修正,我稍后会解释。)

例如,如果基准年是2020年,而我们担心今天的日期(2022年9月25日),我们将有两年的时间365=730天,加上243天(1月至8月的长度总和),加上25天,总共998天。(这是假设2020年为基准年。如前所述,我们实际上将使用1601年作为基准年。)

因此,makejd函数只需执行这三个计算,再加上各种闰年校正。第一行,

jdnum += (year - BASEYEAR) * 365L;

按照上面的步骤1,计算我们关心的年份和基准年之间的差值,乘以365。步骤2是线路

jdnum += monthcount(month - 1, year);

它使用一个单独的函数来计算第1个月到第N个月的总天数,其中N(即month - 1)是我们关心的月份的前一个月。最后,步骤3是非常简单的

jdnum += day;

其中day是我们关心的日子。

然后我们来到闰年修正。每4年是闰年,因此线

jdnum += (year - BASEYEAR) / 4;

取我们关心的全年数,除以4,这是我们需要增加的几天。(换句话说,自基准年以来,我们每四年就必须增加一天。)

但规则并不是每四年都是闰年。我们使用的格里高利日历的实际规则是,每四年有一个闰年,除了每100年没有闰年(也就是说,1900年不是闰年),除了每400年就是闰年(毕竟,2000年是闰年)。所以这两条线

jdnum -= (year - BASEYEAR) / 100;
jdnum += (year - BASEYEAR) / 400;

注意减去每100年的非闰年,再加上每400年的闰年。

然而,请注意,这些简单表达式仅适用于某些精心选择的基准年值,如1601。如果我们使用其他基准年,就会有一定数量的尴尬±1的伪造因素。

现在我们可以回到monthcount函数。如果我们关心日期";9月25日";,monthcount的工作是统计1月至8月的所有天数。(换句话说,monthcount计算"9月0日"的部分儒略日数,使我们能够添加我们关心的确切日期,如25。)monthcount简单明了:

/* Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec */
int monthlengths[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
/* return total days from January up to the end of the given month */
int monthcount(int month, int year)
{
int r = 0;
for(int i = 1; i <= month; i++)
r += monthlengths[i];
if(isleap(year) && month >= 2)
r++;
return r;
}

此函数使用预先初始化的数组monthlengths[],其中包含每个月的长度。("三十天有九月…")由于C数组是基于0的,但我们总是认为一月是第一个月,为了简单起见,这个数组";扔掉";(废物)单元0,因此1月份的monthlengths[1]为31。

这个函数也是我们必须担心闰年的第二个地方。当然,在闰年,二月有29天。因此,如果这是闰年,如果我们被要求计算截至2月或以后的月份天数,我们必须再加一天。(这就是为什么monthcount函数也需要传入年份号的原因。)

唯一剩下的细节是monthcount在2月29日决定是否添加时使用的isleap函数。很简单:

int isleap(int year)
{
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
}

,使用C常见问题列表中问题20.32的公式。

就是这样。希望现在makejd函数的工作原理是清楚的,即使你选择不使用类似的东西。

说到这个选择,值得一问:你应该用哪种方式,方法1还是方法2?

当然,大多数时候,如果可以的话,最好使用预先编写的代码,而不是";滚动自己的";或者重新发明轮子。当涉及到处理日期和时间的代码时,这种建议("使用他人预先编写的代码")的有效性是原来的两倍甚至三倍,因为日期和时间是出了名的复杂,而且很容易(几乎可以保证)至少有一个模糊的细节出错,从而导致微妙的错误。

所以我几乎总是使用方法1。事实上,找出如何使用标准日期/时间函数通常是值得的,即使是解决它们不太适合的问题。(例如,参见这个答案,我设法使用mktime来回答"一个给定的月份从一周中的哪一天开始?"这个问题。)

然而,偶尔你可能会发现自己处于标准函数不合适的情况下,在这种情况下,你知道如何";滚你自己的";可以非常有用。只需小心,并彻底测试您的代码,针对不同日期和时间的批次!(您可能需要自己滚动的情况是,当您处理time_t类型可能无法处理的遥远过去或遥远未来的日期时,或者当您在没有完整C库的嵌入式环境中工作时。)

另一种选择方法可能是只看哪种代码更短或更简单。方法1比我想要的要麻烦一些,主要是因为逐个填充struct tm的所有字段是一件很麻烦的事情。方法2有点长,尽管实际上并没有那么长。(这里看起来有点长,但那只是因为我用了太多冗长的解释。)


脚注:我说";我喜欢这个功能";,我知道,但我承认它有一个缺陷,违反了DRY原则。公历闰年规则嵌入两次,一次在行中

jdnum += (year - BASEYEAR) / 4;
jdnum -= (year - BASEYEAR) / 100;
jdnum += (year - BASEYEAR) / 400;

makejd中,然后在isleap函数中完全单独地进行第二次。如果我们更改日历,必须有人记住更改两地的规则。


附录:我写过像makejd这样的特殊代码是";充满了难以把握的琐碎细节";,为了证明这一点,我可以承认,尽管我认为我知道如何规避这些细节,但我最初在这里发布的makejd函数恰恰成为了这个问题的牺牲品。我匆忙推出了一个快速版本,在一两个数据点上测试了它,做了一个轻微的更正,然后称它很好并发布了它;彻底测试代码"!这项任务落到了@chux身上,他做了我应该做的测试,并找到了一个数据点(实际上有很多),发布的代码给出了一个一天都没有的答案。

所以,是的,像这样的代码很难纠正,有很多机会出现严重的一点一点的错误,如果你要写一些新代码,你必须非常彻底地测试它,如果你不想这样做,你可能宁愿使用预先编写的东西。

我创建了一个函数,将日期转换为自0001年1月1日至输入日期的天数:

// Define a date data type.
struct date {
int day, month, year;
};
/*
* Function's limits (included):
* bottom: 01/01/0001
* top: 31/12/9999
* Input: date data type.
* Output: (int) number of days from 01/01/0001 to that date.
*/
unsigned long int convertDateToDays(struct date date){
unsigned long int totalDays;
int numLeap = 0;
int monthsAddFromYearStart[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
int i;
// First, calculate the number of leap year since year one (not including date's year).
for(i = 1; i < date.year; i++)
if((i % 4 == 0 && i % 100 != 0) || (i % 4 == 0 && i % 400 == 0))
numLeap++;
// If it is a leap year, as of March there has been an extra day.
if((date.year % 4 == 0 && date.year % 100 != 0) || (date.year % 4 == 0 && date.year % 400 == 0))
for(i = 2; i < 12; i++)
monthsAddFromYearStart[i]++;
// (Year - 1) * 356 + a day per leap year + days totaling the previous months + days of this month
totalDays = (date.year - 1) * 365 + numLeap + monthsAddFromYearStart[date.month - 1] + date.day;
return totalDays;
}

通过这种方式,您可以将两个日期转换为天,然后很容易地进行比较。这里有一个例子:

struct date startDate = {28, 02, 0465};
struct date endDate = {30, 06, 2020};
unsigned long int dateDifference, dateDifferenceLastDateIncluded;
dateDifference = convertDateToDays(endDate) - convertDateToDays(startDate);
dateDifferenceLastDateIncluded = convertDateToDays(endDate) - convertDateToDays(startDate) + 1;
printf("Difference in days: %lu.n", dateDifference);
printf("Difference in days, last date included: %lu.", dateDifferenceLastDateIncluded);
/*
* Output:
Difference in days: 625053.
Difference in days, last date included: 625054.
*/

只需定义一个包含日期、月份和年份的日期结构,并将其作为参数传递给convertDateToDays()函数。请注意,该函数返回一个无符号的长整型。这是因为在极端情况下,天数是巨大的(即31/12/9999)

您现在可以将两个日期转换为天,并计算它们之间的差异。如果您想在操作中包括最后一个日期,只需添加一天,如示例所示。

希望这有帮助!

使用标准函数解决此问题:

<time.h>提供了struct tmmktime()difftime()

#include <limits.h>
#include <math.h>
#include <time.h>
// y1/m1/d1 - y0/m0/d0 in days
// Return LONG_MIN on error 
long days_diff(int y1,int m1,int d1,int y0,int m0,int d0) {
// Important: Note other struct tm members are zero filled.
// This includes .tm_isdst to avoid daylight savings time issues.
struct tm date0 = { .tm_year = y0 - 1900, .tm_mon = m0 - 1, .tm_mday = d0 }; 
struct tm date1 = { .tm_year = y1 - 1900, .tm_mon = m1 - 1, .tm_mday = d1 }; 
time_t t0 = mktime(&date0);
time_t t1 = mktime(&date1);
if (t0 == -1 || t1 == -1) {
return LONG_MIN;
}
double diff = difftime(t1, t0); // Difference in seconds
const double secs_per_day = 24.0*60*60;
return lround(diff/secs_per_day);  // Form the difference in `long` days.
}


要将公历的年、月、日转换为日期,请考虑使用修改后的儒略日作为历元。这是一个定义明确的从午夜开始的天数,不同于从中午开始的朱利安日期
MJD 0.0是当地时间1858年11月17日午夜。

您很有可能找到执行命名计算的经过良好测试的代码。

除非代码已经过测试,否则要警惕它的正确性。时间函数有许多角落的情况,会绊倒看似不错的代码。

有了这个,只需减去两天的数字就可以得到日期之间的差值。


以下是几年前的一次尝试。

  • 通过使用int2x数学处理年、月、日的所有int值而不溢出,该类型的位是int的两倍。注意,int可能只有16位。

  • 一个关键特征是简化,将3月1日之前的日期改为前一年的月份,使3月为第一个月(October为第八个月,December为第十个月,…)。这让人想起罗马人在一年的最后一个月——二月增加了闰日。

请注意,格里高利历始于1582年10月,只有在20世纪初才被地球上的大多数人使用。对于更早的日期,我们可以假设是Proptic公历。


#include <limits.h>
#include <stddef.h>
#include <stdint.h>
#if LONG_MAX/2/INT_MAX - 2 == INT_MAX
typedef long int2x;
#define PRId2x "ld"
#elif LLONG_MAX/2/INT_MAX - 2 == INT_MAX
typedef long long int2x;
#define PRId2x "lld"
#elif INTMAX_MAX/2/INT_MAX - 2 == INT_MAX
typedef intmax_t int2x;
#define PRId2x "jd"
#else
#error int2x not available
#endif

static const short DaysMarch1ToBeginingOfMonth[12] = { //
0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337};
#ifndef INT32_C
#define INT32_C(x) ((int_least32_t)1*(x))
#endif
#define DaysPer400Years   (INT32_C(365)*400 + 97)
#define DaysPer100Years   (INT32_C(365)*100 + 24)
#define DaysPer4Years     (365*4    +  1)
#define DaysPer1Year      365
#define MonthsPerYear     12
#define MonthsPer400Years (12*400)
#define MonthMarch        3
#define mjdOffset         0xA5BE1
#define mjd1900Jan1       15020
// November 17, 1858
// Example: 2015 December 31 -->  ymd_to_mjd(2015, 12, 31)
int2x ymd_to_mjd(int year, int month, int day) {
int2x year2x = year;
year2x += month / MonthsPerYear;
month %= MonthsPerYear;
// Adjust for month/year to Mar ... Feb
while (month < MonthMarch) {
month += MonthsPerYear;
year2x--;
}
int2x d = (year2x / 400) * DaysPer400Years;
int y400 = (int) (year2x % 400);
d += (y400 / 100) * DaysPer100Years;
int y100 = y400 % 100;
d += (y100 / 4) * DaysPer4Years;
int y4 = y100 % 4;
d += y4 * DaysPer1Year;
d += DaysMarch1ToBeginingOfMonth[month - MonthMarch];
d += day;
// November 17, 1858 == MJD 0
d--;
d -= mjdOffset;
return d;
}

相关内容

  • 没有找到相关文章

最新更新