Search code examples
cdateformulaleap-year

Calculate number of days elapsed from epoch - formula based


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

Solution

  • 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)