15

I am building a embedded project which displays the time retrieved from a GPS module on a display, but I would also like to display the current date. I currently have the time as a unix time stamp and the progject is written in C.

I am looking for a way to calculate the current UTC date from the timestamp, taking leap years into account? Remember, this is for an embedded project where there is no FPU, so floating point math is emulated, avoiding it as much as possible for performance is required.

EDIT

After looking at @R...'s code, I decided to have a go a writing this myself and came up with the following.

void calcDate(struct tm *tm)
{
  uint32_t seconds, minutes, hours, days, year, month;
  uint32_t dayOfWeek;
  seconds = gpsGetEpoch();

  /* calculate minutes */
  minutes  = seconds / 60;
  seconds -= minutes * 60;
  /* calculate hours */
  hours    = minutes / 60;
  minutes -= hours   * 60;
  /* calculate days */
  days     = hours   / 24;
  hours   -= days    * 24;

  /* Unix time starts in 1970 on a Thursday */
  year      = 1970;
  dayOfWeek = 4;

  while(1)
  {
    bool     leapYear   = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
    uint16_t daysInYear = leapYear ? 366 : 365;
    if (days >= daysInYear)
    {
      dayOfWeek += leapYear ? 2 : 1;
      days      -= daysInYear;
      if (dayOfWeek >= 7)
        dayOfWeek -= 7;
      ++year;
    }
    else
    {
      tm->tm_yday = days;
      dayOfWeek  += days;
      dayOfWeek  %= 7;

      /* calculate the month and day */
      static const uint8_t daysInMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
      for(month = 0; month < 12; ++month)
      {
        uint8_t dim = daysInMonth[month];

        /* add a day to feburary if this is a leap year */
        if (month == 1 && leapYear)
          ++dim;

        if (days >= dim)
          days -= dim;
        else
          break;
      }
      break;
    }
  }

  tm->tm_sec  = seconds;
  tm->tm_min  = minutes;
  tm->tm_hour = hours;
  tm->tm_mday = days + 1;
  tm->tm_mon  = month;
  tm->tm_year = year;
  tm->tm_wday = dayOfWeek;
}
auselen
  • 27,577
  • 7
  • 73
  • 114
Geoffrey
  • 10,843
  • 3
  • 33
  • 46
  • What about leap seconds? – wonce Feb 06 '14 at 04:00
  • Only if the absence of their calculation will cause a large margin of error (which I doubt). – Geoffrey Feb 06 '14 at 04:02
  • UTC and GPS time are 16 seconds different right now due to leap seconds. This will not vary quickly. In other words, if you're just displaying the current date, you can ignore leap seconds. – kmort Feb 06 '14 at 04:21
  • If the epoch time is derived from the GPS module, it will probably have leap seconds accounted for - GPS transmits the difference between GPS and UTC time every 13.5 minutes, the standard NMEA0183 RMC sentence time is defined as UTC time, not GPS time, the module should apply the offset before output. GLONASS (Russian GNSS) tracks leap seconds and transmits UTC time directly from the constellation. – Clifford Feb 06 '14 at 19:48
  • @Clifford - I am not using NMEA, but the SiRF binary protocol. It seems that the 16 seconds are also accounted for here as the timestamp I derive from the module is only 1-2 seconds out with my NTP synced time on my PC. – Geoffrey Feb 06 '14 at 22:27

2 Answers2

21

First divide by 86400; the remainder can be used trivially to get the HH:MM:SS part of your result. Now, you're left with a number of days since Jan 1 1970. I would then adjust that by a constant to be the number of days (possibly negative) since Mar 1 2000; this is because 2000 is a multiple of 400, the leap year cycle, making it easy (or at least easier) to count how many leap years have passed using division.

Rather than trying to explain this in more detail, I'll refer you to my implementation:

http://git.musl-libc.org/cgit/musl/tree/src/time/__secs_to_tm.c?h=v0.9.15

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • 2
    +1 for shift the the beginning of the year to March 1. That makes **Oct** ober the **8th** month and **Dec** ember the **10th** month - as it was long ago. – chux - Reinstate Monica Feb 06 '14 at 16:31
  • 2
    I never even thought of that part. I just think it makes handling leap years in units of days easier, since the extra day is exactly at the end of the year rather than in the middle. – R.. GitHub STOP HELPING ICE Feb 06 '14 at 20:27
  • The Romans put Leap day in the traditional last month (February). They also shifted the 1st day of the year from sometime in March to January 1st. – chux - Reinstate Monica Feb 06 '14 at 20:58
  • First of all thank you very much for linking your musl example! However running the linked code `__secs_to_tm` with the current Unix timestamp `1619222466` gives me `Year 121` instead of `2021` and `Month 3` instead of `4`, for everything else ( day, hour, minute, second etc.) I get the correct values (for UTC). Is there some adjustment that happens for Year and Month afterwards outside of this function or am I missing something? – Jessica Nowak Apr 24 '21 at 00:12
  • 1
    @Jessica: C `struct tm` represents years as offsets from 1900. Add 1900 and you should be good. Also months are zero based. Read the docs for the C time functions to find all the gotchas. – R.. GitHub STOP HELPING ICE Apr 24 '21 at 14:00
1

Here's a portable implementation of mktime(). It includes support for DST that you might remove in order reduce the size somewhat for UTC only. It also normalizes the data (so if for example you had 65 seconds, it would increment the minute and set the seconds to 5, so perhaps has some overhead that you don't need.

It seems somewhat more complex than the solution you have arrived at already; you may want to consider whether there is a reason for that? I would perhaps implement both as a test (on a PC rather than embedded) and iterate through a large range of epoch time values and compare the results with the PC compiler's own std::mktime (using C++ will avoid the name clash without having to rename). If they all produce identical results, then use the fastest/smallest implementation as required, otherwise use the one that is correct!

I think that the typical library mktime performs a binary convergence comparing the return of localtime() with the target. This is less efficient than a direct calendrical calculation, but I presume is done to ensure that a round-trip conversion from struct tm to time_t (or vice versa) and back produces the same result. The portable implementation I suggested above uses the same convergence technique but replaces localtime() to remove library dependencies. On reflection therefore, I suspect that the direct calculation method is preferable in your case since you don't need reversibility - so long as it is correct of course.

Clifford
  • 88,407
  • 13
  • 85
  • 165