Search code examples
cdatetimetimetimezonetime-t

Incorrect result after adding days to date


Using below function, which simply adds days to date (yyyymmdd), works fine throughout years.

int dateplusdays(int datein, int days) {
    int year, month, day;
    int dateout;
    struct tm date;
    time_t secs;

    year = (int)floor(datein / 10000.0);
    month = (int)floor(datein / 100.0) - year * 100;
    day = datein - month * 100 - year * 10000;

    date.tm_sec = 0;
    date.tm_min = 0;
    date.tm_hour = 12;
    date.tm_year = year - 1900;
    date.tm_mon = month - 1;
    date.tm_mday = day;
    date.tm_isdst = -1;
    
    secs = mktime(&date) + days * 86400;
    date = *localtime(&secs);

    dateout = (date.tm_year + 1900) * 10000 + (date.tm_mon + 1) * 100 + date.tm_mday;
    
    return dateout;
}

I stress-tested from 1900 to 2100 using this test code. No errors!

for (i = 19000101; i < 21001231; i++) {
    int a = dateplusdays(i, 0); // make date out of i
    if (i == a) { // check for valid date
        int b = dateplusdays(a, 1);
        int c = dateplusdays(b, 1);
        if (b == c)
            printf("i:%d a:%d b:%d c:%d\n", i, a, b, c);
    }
}

Now ... when changing date.tm_hour from 12 to 0, I get exactly 184 errors on very specific dates, spread completely irregularly throughout the range of years 1900-2100 (e.g. 30.10.2022 adding 1 day results in 30.10.2022).

i:19160930 a:19160930 b:19161001 c:19161001
i:19161001 a:19161001 b:19161001 c:19161001
...
i:20221029 a:20221029 b:20221030 c:20221030
i:20221030 a:20221030 b:20221030 c:20221030
...
i:20381030 a:20381030 b:20381031 c:20381031
i:20381031 a:20381031 b:20381031 c:20381031

On top, only months September-December are concerned.

geohei@vm92:~/Devel$ ./dateplusdays | cut -c7-8 | sort | uniq -c
     47 09
    131 10
      6 11

What am I missing?


Solution

  • You are missing switches between winter and summer time — standard and daylight saving time, or whatever other jargon is used. The choice of 12:00:00 for the time of day was not accidental.

    You should use date.tm_day = day + days; to get the right answer (instead of adding days * 86400). The mktime() function normalizes dates. Note that there are 82800 and 90000 seconds on some days because of the switch in time zone offset from UTC.

    Side note: why on earth are you using floating point arithmetic instead of plain integer division?

    Hmmm: I tried your code with the bare minimum extras (headers, an actual main() function) and only got output when I changed the 12 in the code to 0 — running on a MacBook Pro with macOS Big Sur 11.7.2.

    The dates you quote are near the end of October, when the clocks "fall back". Daylight saving time was first introduced in 1918 in the USA, then stopped after 1921. It was reintroduced for one year in 1945, then 'permanently' in 1965. The rules changed in 1991 so the autumnal time switch changed from "last Sunday in October" to "first Sunday in November", so I see different values from you. (The rules for the springtime time switch also changed from "first Sunday in April" to "second Sunday in March" at the same time.) I'd guess you are living somewhere outside North America (or at least, outside the USA).

    Here's some revised code, exploiting mktime()'s abilities, using midnight. It also formats the data using the ISO 8601 notation. However, it produces no output unless it is compiled with -DUSE_BROKEN_CODE as a compile-time option.

    #include <stdio.h>
    #include <time.h>
    
    static int dateplusdays(int datein, int days)
    {
        struct tm date = { 0 };
    
        int year = datein / 10000;
        int month = (datein / 100) % 100;
        int day = datein % 100;
    
        date.tm_sec = 0;
        date.tm_min = 0;
        date.tm_hour = 0;
        date.tm_year = year - 1900;
        date.tm_mon = month - 1;
        date.tm_isdst = -1;
    
    #ifdef USE_BROKEN_CODE
        date.tm_mday = day;
        time_t secs = mktime(&date) + days * 86400;
    #else
        date.tm_mday = day + days;
        time_t secs = mktime(&date);
    #endif /* USE_BROKEN_CODE */
    
        date = *localtime(&secs);
    
        int dateout = (date.tm_year + 1900) * 10000 + (date.tm_mon + 1) * 100 + date.tm_mday;
    
        return dateout;
    }
    
    static void print_date(int date)
    {
        printf("%.4d-%.2d-%.2d", date / 10000, (date / 100) % 100, date % 100);
    }
    
    int main(void)
    {
        for (int i = 19000101; i < 21001231; i++)
        {
            int a = dateplusdays(i, 0); // make date out of i
            if (i == a)   // check for valid date
            {
                int b = dateplusdays(a, 1);
                int c = dateplusdays(b, 1);
                if (b == c)
                {
                    printf("i: %d  a: ", i);
                    print_date(a);
                    printf("  b: ");
                    print_date(b);
                    printf("  c: ");
                    print_date(c);
                    putchar('\n');
                }
            }
        }
    
        return 0;
    }
    

    I live in Colorado, USA, so I get results like this from the program when compiled with the broken code active:

    i: 19181026  a: 1918-10-26  b: 1918-10-27  c: 1918-10-27
    i: 19181027  a: 1918-10-27  b: 1918-10-27  c: 1918-10-27
    i: 19191025  a: 1919-10-25  b: 1919-10-26  c: 1919-10-26
    i: 19201030  a: 1920-10-30  b: 1920-10-31  c: 1920-10-31
    i: 19201031  a: 1920-10-31  b: 1920-10-31  c: 1920-10-31
    i: 19210521  a: 1921-05-21  b: 1921-05-22  c: 1921-05-22
    i: 19210522  a: 1921-05-22  b: 1921-05-22  c: 1921-05-22
    i: 19450929  a: 1945-09-29  b: 1945-09-30  c: 1945-09-30
    i: 19450930  a: 1945-09-30  b: 1945-09-30  c: 1945-09-30
    i: 19651030  a: 1965-10-30  b: 1965-10-31  c: 1965-10-31
    i: 19651031  a: 1965-10-31  b: 1965-10-31  c: 1965-10-31
    i: 19661029  a: 1966-10-29  b: 1966-10-30  c: 1966-10-30
    i: 19661030  a: 1966-10-30  b: 1966-10-30  c: 1966-10-30
    …
    i: 19881029  a: 1988-10-29  b: 1988-10-30  c: 1988-10-30
    i: 19881030  a: 1988-10-30  b: 1988-10-30  c: 1988-10-30
    i: 19891028  a: 1989-10-28  b: 1989-10-29  c: 1989-10-29
    i: 19891029  a: 1989-10-29  b: 1989-10-29  c: 1989-10-29
    i: 19901027  a: 1990-10-27  b: 1990-10-28  c: 1990-10-28
    i: 19901028  a: 1990-10-28  b: 1990-10-28  c: 1990-10-28
    i: 19911026  a: 1991-10-26  b: 1991-10-27  c: 1991-10-27
    i: 19911027  a: 1991-10-27  b: 1991-10-27  c: 1991-10-27
    i: 19921024  a: 1992-10-24  b: 1992-10-25  c: 1992-10-25
    i: 19921025  a: 1992-10-25  b: 1992-10-25  c: 1992-10-25
    …
    i: 20201031  a: 2020-10-31  b: 2020-11-01  c: 2020-11-01
    i: 20201101  a: 2020-11-01  b: 2020-11-01  c: 2020-11-01
    i: 20211106  a: 2021-11-06  b: 2021-11-07  c: 2021-11-07
    i: 20211107  a: 2021-11-07  b: 2021-11-07  c: 2021-11-07
    i: 20221105  a: 2022-11-05  b: 2022-11-06  c: 2022-11-06
    i: 20221106  a: 2022-11-06  b: 2022-11-06  c: 2022-11-06
    …
    i: 20371031  a: 2037-10-31  b: 2037-11-01  c: 2037-11-01
    i: 20371101  a: 2037-11-01  b: 2037-11-01  c: 2037-11-01
    i: 20381106  a: 2038-11-06  b: 2038-11-07  c: 2038-11-07
    i: 20381107  a: 2038-11-07  b: 2038-11-07  c: 2038-11-07
    i: 20391105  a: 2039-11-05  b: 2039-11-06  c: 2039-11-06
    i: 20391106  a: 2039-11-06  b: 2039-11-06  c: 2039-11-06
    …
    i: 20981101  a: 2098-11-01  b: 2098-11-02  c: 2098-11-02
    i: 20981102  a: 2098-11-02  b: 2098-11-02  c: 2098-11-02
    i: 20991031  a: 2099-10-31  b: 2099-11-01  c: 2099-11-01
    i: 20991101  a: 2099-11-01  b: 2099-11-01  c: 2099-11-01
    i: 21001106  a: 2100-11-06  b: 2100-11-07  c: 2100-11-07
    i: 21001107  a: 2100-11-07  b: 2100-11-07  c: 2100-11-07