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

nqwrtyyt  于 2023-03-12  发布在  其他
关注(0)|答案(4)|浏览(244)

最近,我发现自己需要知道两个日期之间相隔的天数,因为我正在用C编写一个小程序。
我在网上搜索了一个解决方案,但没有找到C。
我怎么能这么做?

eaf3rand

eaf3rand1#

基本上有两种方法可以做到这一点:
1.使用标准工具,使用标准库函数mktime构造每个日期对应的time_t值,然后相减,并将差值转换为天的单位。
1.编写你自己的小代码来构造Julian day number的形式。同样,减法。这至少有点复杂,充满了很难正确处理的繁琐细节,但这是一个很好的、令人满意的练习。(至少,如果你是一个时间迷,这是“令人满意的”。)
方法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 days\n", 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 count of UTC seconds,尽管严格地说C标准规定它可以具有任何实现定义的编码,所以减去两个time_t值的最安全方法是使用difftime函数,它返回一个以秒为单位的差值,正如每个书呆子都知道的,一天有86400秒,所以用秒为单位的差值除以86400,你就得到了天数。
这一切看起来都很简单,尽管有一些微妙之处需要注意。如果你仔细观察你得到的difftime的差异,你会发现它们通常不是86400的整数倍,如果你指定的两个日期碰巧跨越了夏令时转换。(如果是这样,那么您至少有一天是23或25小时长的。)这就是为什么对除以86400的结果进行四舍五入是至关重要的,如示例代码所示。通常,将tm_hour填充为12而不是0也是一个非常好的主意。因为使用中午(即,在一天的“中间”,而不是正好在午夜转换时)也可以帮助避免各种异常。
这就是方法1,这里是方法2的开始,我们将编写一个函数makejd,它计算一个修改后的“Julian day“数,一般来说,儒略日数是一个从一天到另一天递增的数,不考虑月份和日期的界限,例如,这个makejd函数将计算今天的日期154035,2022年9月25日,9月1日是第154011天,再前一天,8月31日,是第154010天,出于我们将要讨论的原因,这些日期可以追溯到1601年,当时1月1日是第1天。
(In在真实的世界中,不同的儒略日编号方案使用不同的基准日。官方的儒略日编号可以追溯到公元前4713年;还有一个官方的“修正儒略历日”(Modified Julian Day,简称MJD),是基于1858年的。)
不管怎样,这里是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天。
1.然后我们会有一些天数对应于从年初到我们所在月份的完整月份。
1.最后,我们在一个月里有了一些天。
(And当然还有一些闰年的修正,我稍后会解释。)
例如,如果基准年是2020年,而我们担心的是今天的日期(2022年9月25日),我们有两年乘以365 = 730天,加上243天(即一月到八月的长度总和),加上25,总计为998。(假设的基准年是2020年,如前所述,我们实际上将使用1601年作为基准年。)
所以makejd函数只执行这三个计算,加上各种闰年校正。

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

实际上就是上面的第一步,计算我们关心的年份和基准年之间的差值乘以365。第二步是

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

它使用一个单独的函数来计算月份1到N中的总天数,其中N(即month - 1)是我们所关心的月份的前一个月。

jdnum += day;

其中day是我们关心的日子。
接下来是闰年修正。每4年是一个闰年,所以这条线

jdnum += (year - BASEYEAR) / 4;

用我们关心的整年数除以4,我们还需要加上几天(换句话说,我们必须为基准年之后的每四年加上一天)。

但是规则并不是每四年就有一个闰年,我们使用的Gregorian calendar的实际规则是每四年有一个闰年,除了每一百年没有闰年(也就是说,1900年不是闰年),除了每400年有一个闰年,毕竟(也就是说,2000年是闰年)。

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

注意减去每隔100年的非闰年,并加上每隔400年的闰年。
然而,请注意,这些简单的表达式 * 仅 * 适用于某些精心选择的基准年值,如1601年。如果我们使用其他基准年,就会有一定数量的尴尬的±1个蒙混因素。
现在我们可以回到monthcount函数,如果我们关心日期“September 25”,那么monthcount的工作就是计算从1月到8月的所有天数。(换句话说,X1 M23 N1 X计算“九月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[],包含每个月的长度。(“Thirty days has September...”)由于C数组是从0开始的,但是我们总是认为一月是月份号1,为了简单起见,这个数组“丢弃”(浪费)了单元格0,所以monthlengths[1]是31代表一月。
这个函数也是第二个需要考虑闰年的地方。当然,在闰年中,二月有29天。因此,如果这是一个闰年,并且如果我们被要求计算到二月或以后的月份中的天数,我们必须再加上一天。(这就是为什么monthcount函数也需要传入年份号的原因。)
剩下的唯一细节是isleap的一个小函数,monthcount在2月29日决定是否添加时使用该函数。

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

使用来自X1 E4 F1 X的X1 E5 F1 X的公式。
...就是这样。希望现在makejd函数的工作原理已经很清楚了,即使您选择不使用类似的东西。
说到这个选择,我们有必要问:您应该使用哪种方法,方法1还是方法2?
当然,大多数时候,如果可能的话,最好使用预先编写的代码,而不是“自己动手”或重新发明轮子。当涉及到处理日期和时间的代码时,建议(“使用别人预先编写的代码”)是双倍甚至三倍有效的,因为日期和时间是出了名的复杂,而且很容易(几乎可以保证)至少弄错一个模糊的细节,导致细微的bug。
因此,我几乎总是使用方法1。实际上,通常值得花点心思弄清楚如何使用标准日期/时间函数,即使是为了解决一个它们并不立即适合的问题。(例如,请参见下面的答案,在这里我设法使用mktime来回答问题:“给定月份从一周中的哪一天开始?")
但是,有时候,您可能会发现自己处于标准函数不适用的情况下,在这种情况下,知道如何“滚动自己的”可能会非常有用。只要小心,并彻底测试您的代码,在不同的日期和时间!(您可能需要自己滚动的情况是,当您处理time_t类型可能无法处理的遥远过去或遥远未来的日期时,或者在没有完整C库的嵌入式环境中工作时。)
另一种选择方法可能是只看哪种代码更短或更简单。方法1比我想的要麻烦一点,主要是因为一个接一个地填充struct tm的所有字段是一件很麻烦的事情。方法2稍微长一点,尽管实际上并没有长那么多。(这里看起来有点长,但那只是因为我用了太多冗长的解释。)
脚注:我说过“我喜欢这个函数”,我确实喜欢,但我承认它有一个缺陷,违反了DRY原则。公历闰年规则嵌入了两次,一次在行中

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

makejd中,然后第二次,完全单独地,在isleap函数中。如果我们改变日历,某人将不得不记住改变这两个地方的规则。(我不是在开玩笑!“干”是一个极好的原则,我确实喜欢尽可能地遵守它。这肯定是违反了,但探讨在这里应用这一原理的可能性将是另一天的主题,因为我在这里已经写了我打算写的大约3倍。)

增编:我曾写道,像makejd这样的临时代码“充满了繁琐的小细节,很难正确处理”,为了证明这一点,我可以承认,尽管我认为我知道如何绕过这些细节,但我最初发布的makejd函数 * 正是 * 这个问题的牺牲品。我做了一个小小的修正,然后说它很好并发布了它。但是我 * 没有 * 遵循我自己的建议“彻底测试代码”!这项任务落在了@chux身上,他做了我应该做的测试,发现了一个数据点(实际上有很多),发布的代码给出的答案偏离了一天。
所以,是的,像这样的代码是很难做对的,有很多机会出现讨厌的小错误,如果你要写一些新代码,你必须非常彻底地测试它,如果你不想这样做,你可能宁愿使用预先写好的东西。

pgx2nnw8

pgx2nnw82#

要使用标准函数解决此问题:
<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.
}
azpvetkf

azpvetkf3#

我创建了一个函数,它可以将日期转换为从01/01/0001到输入日期所经过的天数:

// 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()函数。* 注意,该函数返回一个unsigned long int。这是因为在极端情况下,日计数非常大(例如31/12/9999)。*
现在,您可以将两个日期转换为天数,并计算它们之间的差值。如果您希望在操作中包括最后一个日期,只需添加一天,如示例所示。
希望这对你有帮助!

toiithl6

toiithl64#

要将年、月、日Gregorian calendar转换为 * 日数 *,请考虑使用Modified Julian Day作为历元。它是从午夜开始的明确定义的天数计数,不像 Julian date 从中午开始。
MJD 0.0是当地时间1858年11月17日午夜。
您很有可能找到经过良好测试的代码来执行指定的计算。
除非代码已经过测试,否则要警惕它的正确性。时间函数有许多看似好的代码会出错的情况。
有了这些,简单地减去两个日期数字就可以得到日期之间的差值。
以下是几年前的一次尝试。

  • 使用int2x math(位数是int的两倍)处理所有int的年、月、日值而不溢出。注意int只能是16位。
  • 一个主要特点是简化,将3月1日之前的日期转换为前一年的月份,使3月成为第一个月(October是第八个月,December是第十个月,......)。这让人想起罗马人在一年的最后一个月--二月--增加闰日。

请注意,公历始于1582年10月,只有在20世纪初的某些时候才被地球上的大多数人使用,对于更早的日期,我们可以假设Proleptic Gregorian calendar

#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;
}

相关问题