Convert unix timestamp to date without system libs

2019-02-13 18:48发布

问题:

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

回答1:

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



回答2:

It is not clear why you cannot use the standard library; if size is an issue, only the library components used by your code will be linked, and time_t to struct tm conversion will be pretty small, but more importantly correct and eficient.

However a better question is why you would not use the data directly from the GPS module? It is in the eighth field of the standard RMC sentence that is almost certainly output by the device.

Example for 25 November 2011:

$GPRMC,001225,A,2832.1834,N,08101.0536,W,12,25,251211,1.2,E,A*03



回答3:

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.