1

I am trying to calculate number of days elapsed from a given GMT time.

Well, I am able to make it work with iterative approach of calculation (finding number of normal years and leap years)

The function get_number_of_leap_years_from_base_year is iterating over all the years from 1970 till the given date and checking every year whether its a leap or not and finally add all days.

Is there any other way (formula) based to calculating number normal & leap years elapsed.

/* so-prg-2: Calculating number normal & leap years passed */

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

#define BASE_YEAR 1970

void print_time_readable_format(struct tm tm);
int convert_gmt_date_time_to_tm_format(char* gmt_time_fmt);
int get_number_of_leap_years_from_base_year(int start_year, int end_year);
int calculate_days_elapsed_from_epoch(struct tm tm);

int main()
{
    int days = 0;
    char gmt_time_fmt[] = "Dec 28 18:40:01 2020 GMT";
    //char gmt_time_fmt[] = "Jan 20 19:00:01 2019 GMT";
    //char gmt_time_fmt[] = "Dec 27 14:52:30 2020 GMT";
    //char gmt_time_fmt[] = "Jan 01 00:00:01 1970 GMT";
    days = convert_gmt_date_time_to_tm_format(gmt_time_fmt);

    printf("GMT = %s and days are %d\n", gmt_time_fmt, days);
    return 0;
}

int convert_gmt_date_time_to_tm_format(char* gmt_time_fmt)
{
    struct tm tm;
    char tm_time_fmt[255];

    //set tm struture to 0
    memset(&tm, 0, sizeof(struct tm));
    // convert gmt_time_fmt to format required by 'tm' structure
    strptime(gmt_time_fmt, "%B %d %H:%M:%S %Y GMT", &tm);

    strftime(tm_time_fmt, sizeof(tm_time_fmt), "%s", &tm);
    printf("tm_time_fmt = %s\n", tm_time_fmt);

    print_time_readable_format(tm);
    return calculate_days_elapsed_from_epoch(tm);
}

int calculate_days_elapsed_from_epoch(struct tm tm)
{
    int days_by_month [2][12] = {
        /* normal years */
        { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334},
        /* leap years */
        { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335}
    };

    int current_year = tm.tm_year+1900;
    int total_years_passed = current_year - BASE_YEAR;
    /* -1, to skip the current year */
    int nleap_years_passed = get_number_of_leap_years_from_base_year(BASE_YEAR, current_year-1); 
    int normal_years = total_years_passed - nleap_years_passed;
    int total_days_passed = (normal_years*365 + nleap_years_passed*366 );
    
    printf(" **Years total_days_passed =%d\n", total_days_passed);
    
    total_days_passed += days_by_month[(current_year%4 == 0) - (current_year%100 == 0) + (current_year%400 == 0)][tm.tm_mon];
    total_days_passed += tm.tm_mday - 1; /* to skip the current day */
    
    printf(" **total_days_passed =%d\n", total_days_passed);
    return total_days_passed;
}

int get_number_of_leap_years_from_base_year(int start_year, int end_year)
{
    int leap_year_count = 0;
    int year = start_year;
    
    while( year <= end_year)
    {
        if( (year%4 == 0) - (year%100 == 0) + (year%400 == 0) )
            leap_year_count++;
        year++;
    }
    printf("leap_year_count = %d\n", leap_year_count);
    return leap_year_count;
}

void print_time_readable_format(struct tm tm)
{
    printf("tm.tm_year = %d ", tm.tm_year);
    printf("tm.tm_mon = %d ", tm.tm_mon);
    printf("tm.tm_mday = %d ",tm.tm_mday);
    printf("tm.tm_hour = %d ", tm.tm_hour); 
    printf("tm.tm_min = %d ", tm.tm_min );
    printf("tm.tm_sec = %d\n", tm.tm_sec );
}
Cheppy
  • 25
  • 5
  • Are you allowed to use standard functions? Or must you do it yourself? – Jonathan Leffler Dec 28 '20 at 14:21
  • Thanks @JonathanLeffler for reply, as long as they are portable anything is fine, but good to have formula based to avoid function over heads and loops, and i want to use this to finally calculate the number of seconds elapsed from epoch. – Cheppy Dec 28 '20 at 14:33
  • @JonathanLeffler - Are you aware of a standard C function to do this? – ryyker Dec 28 '20 at 15:07
  • 1
    @ryyker — As in my answer, [`mktime()`](https://pubs.opengroup.org/onlinepubs/9699919799/functions/mktime.html) converts the data in a `struct tm` value into a `time_t` value. – Jonathan Leffler Dec 28 '20 at 15:18

2 Answers2

2

Use mktime()

Since your code is allowed to use both Standard C strftime() and POSIX strptime(), there's no reason not to use Standard C mktime() either.

It gives you a time_t value which is the number of seconds since The Epoch.

int calculate_days_elapsed_from_epoch(struct tm tm)
{
    time_t t = mktime(&tm);
    return t / 86400;   // 24 * 60 * 60 = 86400
}

But if the goal is to calculate the seconds since The Epoch, you have the answer immediately from mktime().

Note that mktime() is passed a struct tm pointer, and it accepts values that are 'out of range' and normalizes the result. See also the example code in the section 'Demonstrating mktime()'.

Calculating leap days

I have a function jl_dmy_conversion() lurking in my library which converts a combination of year, month, day to a number of days since 1899-12-31 (so in this system, day 1 was 1900-01-01). But it includes a calculation for number of leap days. This code is internal to a package where the parameters are already validated as valid within the date range 0001-01-01 .. 9999-12-31, so it does not do much to protect itself from invalid data. There is another function that invokes this that does the data validation. Some of the information shown here comes from a header, most from the source file containing the implementation.

typedef int Date;

enum { DATE_NULL = -2147483648 };   /* Informix NULL DATE */

#define LEAPYEAR(y) (((y) % 4) == 0 && (((y) % 100) != 0 || ((y) % 400) == 0))

#define PRId_Date "d"

/*
** In 400 years, there are 97 leap years (because the three years
** divisible by 100 but not by 400 are not leap years).  This also
** happens to be exactly 20871 weeks.
*/
#define DAYS_IN_400_YEARS   (400*365+97)
#define DAYS_IN_2000_YEARS  (5*DAYS_IN_400_YEARS)

enum
{
    DAYS_IN_JANUARY   = 31,
    DAYS_IN_FEBRUARY  = 28,
    DAYS_IN_MARCH     = 31,
    DAYS_IN_APRIL     = 30,
    DAYS_IN_MAY       = 31,
    DAYS_IN_JUNE      = 30,
    DAYS_IN_JULY      = 31,
    DAYS_IN_AUGUST    = 31,
    DAYS_IN_SEPTEMBER = 30,
    DAYS_IN_OCTOBER   = 31,
    DAYS_IN_NOVEMBER  = 30,
    DAYS_IN_DECEMBER  = 31
};

static const int days_in_month[][2] =
{
    { 0,                 0                  },
    { DAYS_IN_JANUARY,   DAYS_IN_JANUARY    },
    { DAYS_IN_FEBRUARY,  DAYS_IN_FEBRUARY+1 },
    { DAYS_IN_MARCH,     DAYS_IN_MARCH      },
    { DAYS_IN_APRIL,     DAYS_IN_APRIL      },
    { DAYS_IN_MAY,       DAYS_IN_MAY        },
    { DAYS_IN_JUNE,      DAYS_IN_JUNE       },
    { DAYS_IN_JULY,      DAYS_IN_JULY       },
    { DAYS_IN_AUGUST,    DAYS_IN_AUGUST     },
    { DAYS_IN_SEPTEMBER, DAYS_IN_SEPTEMBER  },
    { DAYS_IN_OCTOBER,   DAYS_IN_OCTOBER    },
    { DAYS_IN_NOVEMBER,  DAYS_IN_NOVEMBER   },
    { DAYS_IN_DECEMBER,  DAYS_IN_DECEMBER   }
};

/* Return date as number of days since 31st December 1899 - no range check */
static Date jl_dmy_conversion(int d, int m, int y)
{
    int             leap;
    int             i;
    Date            daynum;

    /* No need to assert here - calling functions have checked basics */
    DB_TRACE(1, "[[-- jl_dmy_conversion (d = %2d, m = %2d, y = %4d) ", d, m, y);

    leap = LEAPYEAR(y);
    if (d > days_in_month[m][leap])
    {
        DB_TRACE(1, "<<-- NULL (invalid day of month)\n");
        return(DATE_NULL);
    }

    /* Number of days so far this month */
    daynum = d;

    /* Days so far this year prior to this month */
    for (i = 1; i < m; i++)
        daynum += days_in_month[i][leap];
    DB_TRACE(4, "YDAY = %3ld ", daynum);

    /*
    ** Now compute number of days to 1st of January of this year.  Add
    ** 2000 years (5 periods of 400 years) to ensure that numbers
    ** resulting from subtraction are positive, even when dates back to
    ** 0001-01-01 are allowed, and then remove the number of days found
    ** in 2000 years.  This assumes int is 32-bit or longer.
    **
    ** NB: Things begin to go haywire when (y - 1901) yields -4, which
    ** is when y == 1897.  Things get worse before 1601.  The result is
    ** usually, but not always, off by one.  Adding 2000 years and then
    ** subtracting the appropriate number of days sidesteps the
    ** problems.
    */
    y += 2000;
    daynum += 365 * (y - 1900); /* Ignoring leap years */
    DB_TRACE(4, "INC1 = %7d ", 365 * (y - 1900));
    daynum += (y - 1901) / 4;   /* Allowing for leap years */
    DB_TRACE(4, "INC2 = %4d ", (y - 1901) / 4);
    daynum -= (y - 1901) / 100; /* Allowing for non-leap years */
    DB_TRACE(4, "INC3 = %3d ", -(y - 1901) / 100);
    daynum += (y - 1601) / 400; /* Allowing for leap years */
    DB_TRACE(4, "INC4 = %2d ", (y - 1601) / 400);
    daynum -= DAYS_IN_2000_YEARS;
    DB_TRACE(1, " (r = %7" PRId_Date ") --]]\n", daynum);

    return(daynum);
}

The DB_TRACE macro is derived from the code shown in #define a macro for debug printing in C?. The DB_TRACE macro is available in my SOQ (Stack Overflow Questions) repository on GitHub as files debug.c and debug.h in the src/libsoq sub-directory. The formatting gives a single line showing the calculation steps.

The code above compiles with the debug.h header and <stdio.h> included, and a minimal main(), plus linking with the code from debug.c:


int main(void)
{
    int dd = 28;
    int mm = 12;
    int yyyy = 2020;
    Date dt = jl_dmy_conversion(dd, mm, yyyy);
    printf("%.4d-%.2d-%.2d = %d\n", yyyy, mm, dd, dt);
    return 0;
}

Demonstrating mktime()

As mentioned above, mktime() is passed a struct tm pointer, and it is documented that it accepts values that are 'out of range' and normalizes the result — modifying the structure it is passed. It also sets the tm_wday and tm_yday fields — it ignores them as inputs.

If you have a struct tm value for 2020-12-28 08:20:26 and you want to know the time_t value for 6 days, 18 hours, 43 minutes, 32 seconds later, you can use code like this:

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

static void print_time(time_t t, const struct tm *tm)
{
    char buffer[32];
    strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S %A", tm);
    printf("%lld: %s (%d)\n", (long long)t, buffer, tm->tm_yday);
}

int main(void)
{
   struct tm tm = { .tm_year = 2020 - 1900, .tm_mon = 12 - 1, .tm_mday = 28,
                    .tm_hour = 8, .tm_min = 20, .tm_sec = 26 };
   time_t t0 = mktime(&tm);
   print_time(t0, &tm);
   tm.tm_mday += 6;
   tm.tm_hour += 18;
   tm.tm_min += 43;
   tm.tm_sec += 32;
   time_t t1 = mktime(&tm);
   print_time(t1, &tm);
   return 0;
}

When run (in US/Mountain standard time zone — UTC-7), it produces:

1609168826: 2020-12-28 08:20:26 Monday (362)
1609754638: 2021-01-04 03:03:58 Monday (3)
Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • I had been looking at `strptime/strftime` at potentials, but you apparently have already done the work! Nice answer. I am curious about the validation code used in the calling function though. – ryyker Dec 28 '20 at 15:17
  • `return t / 86400;` does ignore leap seconds. See https://www.ietf.org/timezones/data/leap-seconds.list – Andrew Henle Dec 28 '20 at 16:07
  • @AndrewHenle: of course — all the Standard C and POSIX functions ignore leap seconds, so it goes without saying … and I didn't say it. – Jonathan Leffler Dec 28 '20 at 16:08
  • Thanks a lot @Jonathan , It was very nice explanation and most of my doubts are cleared, but 1) what is the meaning of _mktime() accepts values that are 'out of range'_, do you mean passing day value as more than 31 for any given day for example 2) Suppose if my system start time is 1970, can i blindly depend on `mktime` function to return correct number of seconds since epoch, with out going for complex calculations for 2000 years? – Cheppy Dec 28 '20 at 17:18
  • For ‘out of range’ see the example. – Jonathan Leffler Dec 28 '20 at 17:52
  • I made a blunder as `mktime` modifies the times internally as per the `TZ`, when i ran your program with `mktime` to get the seconds since epoch it worked correctly on an online compiler ( I mean the TZ is considered as GMT) , but when i ran the same code on my system i see a difference of 5.5 hrs( I am running on IST), which is exactly the reason i dint use `mktime` in first place **but i forgot to mention that point, its been very long i tried that** – Cheppy Dec 28 '20 at 18:54
  • For item (2) you can rely on `mktime()` for dates from 1970-01-01 forwards, subject to your system using a 64-bit `time_t`. You run into problems with dates from 2038-01-01 onwards with 32-bit `time_t`. It gets a bit more tricky if you want to go backwards in time. Usually it works for negative numbers, but it isn't guaranteed to do so by the standards (and dates prior 1902-01-01 become problematic with 32-bit `time_t`). There are also issues going backwards in time with the so-called 'proleptic Gregorian calendar' and the switch from Julian to Gregorian calendar, etc. Lots of fun. – Jonathan Leffler Dec 28 '20 at 18:56
  • It's an important point — `mktime()` works in local time, not UTC (aka GMT). One option is to use `tzset("UTC0")` before calling `mktime()`; that sets the time zone to UTC. You just need to capture the prior value of the `$TZ` variable if you want to set it back afterwards. That can be problematic if your system doesn't set `TZ` explicitly (macOS, for example) — then you need to know how to recover the default value, which varies per system; or you do `unsetenv("TZ")`. – Jonathan Leffler Dec 28 '20 at 19:00
  • @JonathanLeffler, the thing is we can rely on `mktime` provided we add the `TZ` difference to make it work for epoch conversion. Otherwise we may see some difference, am i right? same with my example, i need to convert GMT into epoch, but if i am running on `TZ` other than `GMT`, then the output yeild by `mktime` is not correct. – Cheppy Dec 28 '20 at 19:09
  • Instead of playing with `TZ` , its better to add the time difference is my opinion. – Cheppy Dec 28 '20 at 19:11
  • 1
    There are endless ways to deal with it — probably about as many different ways as there are programmers who've tackled the problem. One way is to use `settz()`. Another way is to know/determine your time zone's offset from UTC and apply a 'correction' to the `time_t` value passed to `gmtime()`, if that's what you're going to use. I've used both in different contexts. With IST, you don't have winter/summer (standard/daylight saving) time to confuse the issue. It comes down to specifying the problem you want to solve carefully, and then deciding how to make the system respond appropriately. – Jonathan Leffler Dec 28 '20 at 19:15
  • Some systems have extra fields in the `struct tm` compared to what POSIX and Standard C guarantee. For example, macOS has `char *tm_zone` (a pointer to an abbreviation of the timezone name) and `long tm_gmtoff` (offset from UTC in seconds). But using those immediately limits portablity. – Jonathan Leffler Dec 28 '20 at 19:19
  • All in all, i hope i never forget this again, thanks a lot for the information. – Cheppy Dec 28 '20 at 19:35
0

The conditions for leap year are summarized as:

  • leap year if perfectly visible by 400
  • not a leap year if visible by 100 but not divisible by 400
  • leap year if not divisible by 100 but divisible by 4
  • all other years are not leap year

So the logic could be expressed as:

if (((year%4 == 0) && (year%100 != 0)) || (year%400 == 0))
{
    leap_year_count++;
}
year++;

But not sure if re-factoring your existing logic will add any speed advantage.

ryyker
  • 22,849
  • 3
  • 43
  • 87
  • do you see any problem in my logic, it also achieves the same and works – Cheppy Dec 28 '20 at 14:57
  • @Cheppy - No. your logic is sound from what I observed, although It was not intuitive to me :) But I verified it against a test using the statement I have above from 1700 to 2020, (78 leaps years.) And I do not believe there is a standard C function to do this, so without bench-marking to compare with other forms, I would just use it. – ryyker Dec 28 '20 at 15:00
  • I am using date `Dec 28 18:40:01 2020 GMT` and getting 12 leap years, since i am doing -1 from `current_year`. (actually there are 13 leap years from 1970 till 2020 from [here](https://miniwebtool.com/leap-years-list/?start_year=1970&end_year=2020)) – Cheppy Dec 28 '20 at 15:11
  • @Cheppy - in feeding in the years only, i.e. 1970 - 2020, I am seeing 13 years, for both sets of logic. I believe Jonathan's answer addresses the exceptions existing in sequential time that need to be handled for best accuracy. – ryyker Dec 28 '20 at 15:24