ESPNTPServer/TimeUtils.cpp

492 lines
13 KiB
C++

/*
* TimeUtils.cpp
*
* Copyright 2017 Christopher B. Liebman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*
* Created on: Jun 7, 2017
* Author: liebman
*/
#include "TimeUtils.h"
//#define DEBUG
#include "Logger.h"
uint8_t TimeUtils::parseSmallDuration(const char* value)
{
int i = atoi(value);
if (i < 0 || i > 255)
{
dbprintf("TimeUtils::parseSmallDuration: invalid value %s: using 32 instead!\n", value);
i = 32;
}
return (uint8_t) i;
}
uint8_t TimeUtils::parseOccurrence(const char* occurrence_string)
{
int i = atoi(occurrence_string);
if (i < -5 || i == 0 || i > 5)
{
dbprintf("TimeUtils::parseOccurrence: invalid value %s: using 1 instead!\n", occurrence_string);
i = 1;
}
return (uint8_t) i;
}
uint8_t TimeUtils::parseDayOfWeek(const char* dow_string)
{
int i = atoi(dow_string);
if (i < 0 || i > 6)
{
dbprintf("TimeUtils::parseDayOfWeek: invalid value %s: using 0 (Sunday) instead!\n", dow_string);
i = 1;
}
return (uint8_t) i;
}
uint8_t TimeUtils::parseMonth(const char* month_string)
{
int i = atoi(month_string);
if (i < 0 || i > 12)
{
dbprintf("TimeUtils::parseMonth: invalid value '%s': using 3 (Mar) instead!\n", month_string);
i = 1;
}
return (uint8_t) i;
}
uint8_t TimeUtils::parseHour(const char* hour_string)
{
int i = atoi(hour_string);
if (i < 0 || i > 23)
{
dbprintf("TimeUtils::parseMonth: invalid value '%s': using 2 instead!\n", hour_string);
i = 1;
}
return (uint8_t) i;
}
int TimeUtils::parseOffset(const char* offset_string)
{
int result = 0;
char value[11];
strncpy(value, offset_string, 10);
if (strchr(value, ':') != NULL)
{
int sign = 1;
char* s;
if (value[0] == '-')
{
sign = -1;
s = strtok(&(value[1]), ":");
}
else
{
s = strtok(value, ":");
}
if (s != NULL)
{
int h = atoi(s);
while (h > 11)
{
h -= 12;
}
result += h * 3600; // hours to seconds
s = strtok(NULL, ":");
}
if (s != NULL)
{
result += atoi(s) * 60; // minutes to seconds
s = strtok(NULL, ":");
}
if (s != NULL)
{
result += atoi(s);
}
// apply sign
result *= sign;
}
else
{
result = atoi(value);
if (result < -43199 || result > 43199)
{
result = 0;
}
}
return result;
}
uint16_t TimeUtils::parsePosition(const char* position_string)
{
int result = 0;
char value[10];
strncpy(value, position_string, 9);
if (strchr(value, ':') != NULL)
{
char* s = strtok(value, ":");
if (s != NULL)
{
int h = atoi(s);
while (h > 11)
{
h -= 12;
}
result += h * 3600; // hours to seconds
s = strtok(NULL, ":");
}
if (s != NULL)
{
result += atoi(s) * 60; // minutes to seconds
s = strtok(NULL, ":");
}
if (s != NULL)
{
result += atoi(s);
}
}
else
{
result = atoi(value);
if (result < 0 || result > 43199)
{
result = 0;
}
}
return result;
}
//
// Modified code from: http://www.jbox.dk/sanos/source/lib/time.c.html
//
#define YEAR0 1900
#define EPOCH_YR 1970
#define SECS_DAY (24L * 60L * 60L)
#define LEAPYEAR(year) (!((year) % 4) && (((year) % 100) || !((year) % 400)))
#define YEARSIZE(year) (LEAPYEAR(year) ? 366 : 365)
#define TIME_MAX 2147483647L
const int _ytab[2][12] =
{
{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } };
// esp version is broken :-(
time_t TimeUtils::mktime(struct tm *tmbuf)
{
long day, year;
int tm_year;
int yday, month;
/*unsigned*/long seconds;
int overflow;
tmbuf->tm_min += tmbuf->tm_sec / 60;
tmbuf->tm_sec %= 60;
if (tmbuf->tm_sec < 0)
{
tmbuf->tm_sec += 60;
tmbuf->tm_min--;
}
tmbuf->tm_hour += tmbuf->tm_min / 60;
tmbuf->tm_min = tmbuf->tm_min % 60;
if (tmbuf->tm_min < 0)
{
tmbuf->tm_min += 60;
tmbuf->tm_hour--;
}
day = tmbuf->tm_hour / 24;
tmbuf->tm_hour = tmbuf->tm_hour % 24;
if (tmbuf->tm_hour < 0)
{
tmbuf->tm_hour += 24;
day--;
}
tmbuf->tm_year += tmbuf->tm_mon / 12;
tmbuf->tm_mon %= 12;
if (tmbuf->tm_mon < 0)
{
tmbuf->tm_mon += 12;
tmbuf->tm_year--;
}
day += (tmbuf->tm_mday - 1);
while (day < 0)
{
if (--tmbuf->tm_mon < 0)
{
tmbuf->tm_year--;
tmbuf->tm_mon = 11;
}
day += _ytab[LEAPYEAR(YEAR0 + tmbuf->tm_year)][tmbuf->tm_mon];
}
while (day >= _ytab[LEAPYEAR(YEAR0 + tmbuf->tm_year)][tmbuf->tm_mon])
{
day -= _ytab[LEAPYEAR(YEAR0 + tmbuf->tm_year)][tmbuf->tm_mon];
if (++(tmbuf->tm_mon) == 12)
{
tmbuf->tm_mon = 0;
tmbuf->tm_year++;
}
}
tmbuf->tm_mday = day + 1;
year = EPOCH_YR;
if (tmbuf->tm_year < year - YEAR0)
return (time_t) -1;
seconds = 0;
day = 0; // Means days since day 0 now
overflow = 0;
// Assume that when day becomes negative, there will certainly
// be overflow on seconds.
// The check for overflow needs not to be done for leapyears
// divisible by 400.
// The code only works when year (1970) is not a leapyear.
tm_year = tmbuf->tm_year + YEAR0;
if (TIME_MAX / 365 < tm_year - year)
overflow++;
day = (tm_year - year) * 365;
if (TIME_MAX - day < (tm_year - year) / 4 + 1)
overflow++;
day += (tm_year - year) / 4 + ((tm_year % 4) && tm_year % 4 < year % 4);
day -= (tm_year - year) / 100
+ ((tm_year % 100) && tm_year % 100 < year % 100);
day += (tm_year - year) / 400
+ ((tm_year % 400) && tm_year % 400 < year % 400);
yday = month = 0;
while (month < tmbuf->tm_mon)
{
yday += _ytab[LEAPYEAR(tm_year)][month];
month++;
}
yday += (tmbuf->tm_mday - 1);
if (day + yday < 0)
overflow++;
day += yday;
tmbuf->tm_yday = yday;
tmbuf->tm_wday = (day + 4) % 7; // Day 0 was thursday (4)
seconds = ((tmbuf->tm_hour * 60L) + tmbuf->tm_min) * 60L + tmbuf->tm_sec;
if ((TIME_MAX - seconds) / SECS_DAY < day)
overflow++;
seconds += day * SECS_DAY;
if (overflow)
return (time_t) -1;
if ((time_t) seconds != seconds)
return (time_t) -1;
return (time_t) seconds;
}
struct tm *TimeUtils::gmtime_r(const time_t *timer, struct tm *tmbuf)
{
time_t time = *timer;
unsigned long dayclock, dayno;
int year = EPOCH_YR;
dayclock = (unsigned long) time % SECS_DAY;
dayno = (unsigned long) time / SECS_DAY;
tmbuf->tm_sec = dayclock % 60;
tmbuf->tm_min = (dayclock % 3600) / 60;
tmbuf->tm_hour = dayclock / 3600;
tmbuf->tm_wday = (dayno + 4) % 7; // Day 0 was a thursday
while (dayno >= (unsigned long) YEARSIZE(year)) {
dayno -= YEARSIZE(year);
year++;
}
tmbuf->tm_year = year - YEAR0;
tmbuf->tm_yday = dayno;
tmbuf->tm_mon = 0;
while (dayno >= (unsigned long) _ytab[LEAPYEAR(year)][tmbuf->tm_mon]) {
dayno -= _ytab[LEAPYEAR(year)][tmbuf->tm_mon];
tmbuf->tm_mon++;
}
tmbuf->tm_mday = dayno + 1;
tmbuf->tm_isdst = 0;
return tmbuf;
}
//
// The functions findDOW & findNthDate are from:
//
// http://hackaday.com/2012/07/16/automatic-daylight-savings-time-compensation-for-your-clock-projects
//
/*--------------------------------------------------------------------------
FUNC: 6/11/11 - Returns day of week for any given date
PARAMS: year, month, date
RETURNS: day of week (0-7 is Sun-Sat)
NOTES: Sakamoto's Algorithm
http://en.wikipedia.org/wiki/Calculating_the_day_of_the_week#Sakamoto.27s_methods
Altered to use char when possible to save microcontroller ram
--------------------------------------------------------------------------*/
uint8_t TimeUtils::findDOW(uint16_t y, uint8_t m, uint8_t d)
{
static char t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
y -= m < 3;
return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
}
/*--------------------------------------------------------------------------
http://hackaday.com/2012/07/16/automatic-daylight-savings-time-compensation-for-your-clock-projects
FUNC: 6/11/11 - Returns the date for Nth day of month. For instance,
it will return the numeric date for the 2nd Sunday of April
PARAMS: year, month, day of week, Nth occurrence of that day in that month
RETURNS: date
NOTES: There is no error checking for invalid inputs.
--------------------------------------------------------------------------*/
uint8_t TimeUtils::findNthDate(uint16_t year, uint8_t month, uint8_t dow, uint8_t nthWeek)
{
dbprintf("findNthDate: year:%u month:%u, dow:%u nthWeek:%d\n", year, month, dow, nthWeek);
uint8_t targetDate = 1;
uint8_t firstDOW = findDOW(year,month,targetDate);
while (firstDOW != dow) {
firstDOW = (firstDOW+1)%7;
targetDate++;
}
//Adjust for weeks
targetDate += (nthWeek-1)*7;
return targetDate;
}
uint8_t TimeUtils::daysInMonth(uint16_t year, uint8_t month)
{
uint8_t days = 31;
switch(month)
{
case 2:
days = 28;
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
days = 29;
}
break;
case 4:
case 6:
case 9:
case 11:
days = 30;
break;
}
return days;
}
uint8_t TimeUtils::findDateForWeek(uint16_t year, uint8_t month, uint8_t dow, int8_t week)
{
dbprintf("findDateForWeek: year:%u month:%u, dow:%u week:%d\n", year, month, dow, week);
uint8_t weeks[5];
uint8_t max_day = daysInMonth(year, month);
int last = 0;
if (week >= 0)
{
return findNthDate(year, month, dow, week);
}
//
// find all times this weekday shows up in the month
// Note that 'last' will end up pointing 1 past the last
// valid occurrence. -1 will give the last one.
//
for(last = 0; last <= 5; ++last)
{
weeks[last] = findNthDate(year, month, dow, last+1);
dbprintf("findDateForWeek: last:%d date:%u\n", last, weeks[last]);
if (weeks[last] > max_day)
{
break;
}
}
return weeks[last+week];
}
int TimeUtils::computeUTCOffset(time_t now, int tz_offset, TimeChange* tc, int tc_count)
{
struct tm tm;
//
// get the current year
//
gmtime_r(&now, &tm);
int year = tm.tm_year;
//
// pre-set the offset to the last timechange of the year
//
int offset = tc[tc_count-1].tz_offset;
//
// loop thru each time change entry, converting it to the time in seconds for the
// current year. If now is greater/equal to the time change the use the new offset.
// We return the last offset that is greater/equal now.
//
for(int i = 0; i < tc_count; ++i)
{
dbprintf("TimeUtils::computeUTCOffset: index:%d offset:%d month:%u dow:%u occurrence:%d hour:%u day_offset:%d\n",
i,
tc[i].tz_offset,
tc[i].month,
tc[i].day_of_week,
tc[i].occurrence,
tc[i].hour,
tc[i].day_offset);
tm.tm_sec = 0;
tm.tm_min = 0;
tm.tm_hour = tc[i].hour;
tm.tm_mday = TimeUtils::findDateForWeek(year+1900, tc[i].month, tc[i].day_of_week, tc[i].occurrence);
tm.tm_mon = tc[i].month-1;
tm.tm_year = year;
dbprintf("computeUTCOffset: tm: %04d/%02d/%02d %02d:%02d:%02d + %d days\n", tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tc[i].day_offset);
// convert to seconds
time_t tc_time = mktime(&tm);
// convert to UTC
tc_time -= tz_offset;
dbprintf("computeUTCOffset: tc_time: %ld (UTC)\n", tc_time);
// add in days offset
tc_time += tc[i].day_offset*86400;
dbprintf("computeUTCOffset: now: %ld tc_time: %ld\n", now, tc_time);
if (now >= tc_time)
{
offset = tc[i].tz_offset;
dbprintf("computeUTCOffset: now > tc_time, offset: %d\n", offset);
}
}
return offset;
}