root/lib/common/iso8601.c

/* [previous][next][first][last][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. crm_get_utc_time
  2. crm_time_new
  3. crm_time_new_undefined
  4. crm_time_is_defined
  5. crm_time_free
  6. year_days
  7. crm_time_january1_weekday
  8. crm_time_weeks_in_year
  9. crm_time_days_in_month
  10. crm_time_leapyear
  11. get_ordinal_days
  12. crm_time_log_alias
  13. crm_time_get_sec
  14. crm_time_get_timeofday
  15. crm_time_get_timezone
  16. crm_time_get_seconds
  17. crm_time_get_seconds_since_epoch
  18. crm_time_get_gregorian
  19. crm_time_get_ordinal
  20. crm_time_get_isoweek
  21. crm_duration_as_string
  22. crm_time_as_string
  23. crm_time_parse_sec
  24. crm_time_parse_offset
  25. crm_time_parse
  26. parse_date
  27. parse_int
  28. crm_time_parse_duration
  29. crm_time_parse_period
  30. crm_time_free_period
  31. crm_time_set
  32. ha_set_tm_time
  33. crm_time_set_timet
  34. pcmk_copy_time
  35. crm_time_add
  36. crm_time_calculate_duration
  37. crm_time_subtract
  38. crm_time_check
  39. crm_time_compare
  40. crm_time_add_seconds
  41. crm_time_add_days
  42. crm_time_add_months
  43. crm_time_add_minutes
  44. crm_time_add_hours
  45. crm_time_add_weeks
  46. crm_time_add_years
  47. ha_get_tm_time
  48. pcmk__time_hr_convert
  49. pcmk__time_set_hr_dt
  50. pcmk__time_timeval_hr_convert
  51. pcmk__time_hr_new
  52. pcmk__time_hr_free
  53. pcmk__time_format_hr
  54. pcmk__epoch2str
  55. pcmk__readable_interval

   1 /*
   2  * Copyright 2005-2022 the Pacemaker project contributors
   3  *
   4  * The version control history for this file may have further details.
   5  *
   6  * This source code is licensed under the GNU Lesser General Public License
   7  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
   8  */
   9 
  10 /*
  11  * References:
  12  *      https://en.wikipedia.org/wiki/ISO_8601
  13  *      http://www.staff.science.uu.nl/~gent0113/calendar/isocalendar.htm
  14  */
  15 
  16 #include <crm_internal.h>
  17 #include <crm/crm.h>
  18 #include <time.h>
  19 #include <ctype.h>
  20 #include <string.h>
  21 #include <stdbool.h>
  22 #include <crm/common/iso8601.h>
  23 
  24 /*
  25  * Andrew's code was originally written for OSes whose "struct tm" contains:
  26  *      long tm_gmtoff;         :: Seconds east of UTC
  27  *      const char *tm_zone;    :: Timezone abbreviation
  28  * Some OSes lack these, instead having:
  29  *      time_t (or long) timezone;
  30                 :: "difference between UTC and local standard time"
  31  *      char *tzname[2] = { "...", "..." };
  32  * I (David Lee) confess to not understanding the details.  So my attempted
  33  * generalisations for where their use is necessary may be flawed.
  34  *
  35  * 1. Does "difference between ..." subtract the same or opposite way?
  36  * 2. Should it use "altzone" instead of "timezone"?
  37  * 3. Should it use tzname[0] or tzname[1]?  Interaction with timezone/altzone?
  38  */
  39 #if defined(HAVE_STRUCT_TM_TM_GMTOFF)
  40 #  define GMTOFF(tm) ((tm)->tm_gmtoff)
  41 #else
  42 /* Note: extern variable; macro argument not actually used.  */
  43 #  define GMTOFF(tm) (-timezone+daylight)
  44 #endif
  45 
  46 #define HOUR_SECONDS    (60 * 60)
  47 #define DAY_SECONDS     (HOUR_SECONDS * 24)
  48 
  49 // A date/time or duration
  50 struct crm_time_s {
  51     int years;      // Calendar year (date/time) or number of years (duration)
  52     int months;     // Number of months (duration only)
  53     int days;       // Ordinal day of year (date/time) or number of days (duration)
  54     int seconds;    // Seconds of day (date/time) or number of seconds (duration)
  55     int offset;     // Seconds offset from UTC (date/time only)
  56     bool duration;  // True if duration
  57 };
  58 
  59 static crm_time_t *parse_date(const char *date_str);
  60 
  61 static crm_time_t *
  62 crm_get_utc_time(crm_time_t *dt)
     /* [previous][next][first][last][top][bottom][index][help] */
  63 {
  64     crm_time_t *utc = NULL;
  65 
  66     if (dt == NULL) {
  67         errno = EINVAL;
  68         return NULL;
  69     }
  70 
  71     utc = crm_time_new_undefined();
  72     utc->years = dt->years;
  73     utc->days = dt->days;
  74     utc->seconds = dt->seconds;
  75     utc->offset = 0;
  76 
  77     if (dt->offset) {
  78         crm_time_add_seconds(utc, -dt->offset);
  79     } else {
  80         /* Durations (which are the only things that can include months, never have a timezone */
  81         utc->months = dt->months;
  82     }
  83 
  84     crm_time_log(LOG_TRACE, "utc-source", dt,
  85                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
  86     crm_time_log(LOG_TRACE, "utc-target", utc,
  87                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
  88     return utc;
  89 }
  90 
  91 crm_time_t *
  92 crm_time_new(const char *date_time)
     /* [previous][next][first][last][top][bottom][index][help] */
  93 {
  94     time_t tm_now;
  95     crm_time_t *dt = NULL;
  96 
  97     tzset();
  98     if (date_time == NULL) {
  99         tm_now = time(NULL);
 100         dt = crm_time_new_undefined();
 101         crm_time_set_timet(dt, &tm_now);
 102     } else {
 103         dt = parse_date(date_time);
 104     }
 105     return dt;
 106 }
 107 
 108 /*!
 109  * \brief Allocate memory for an uninitialized time object
 110  *
 111  * \return Newly allocated time object
 112  * \note The caller is responsible for freeing the return value using
 113  *       crm_time_free().
 114  */
 115 crm_time_t *
 116 crm_time_new_undefined()
     /* [previous][next][first][last][top][bottom][index][help] */
 117 {
 118     crm_time_t *result = calloc(1, sizeof(crm_time_t));
 119 
 120     CRM_ASSERT(result != NULL);
 121     return result;
 122 }
 123 
 124 /*!
 125  * \brief Check whether a time object has been initialized yet
 126  *
 127  * \param[in] t  Time object to check
 128  *
 129  * \return TRUE if time object has been initialized, FALSE otherwise
 130  */
 131 bool
 132 crm_time_is_defined(const crm_time_t *t)
     /* [previous][next][first][last][top][bottom][index][help] */
 133 {
 134     // Any nonzero member indicates something has been done to t
 135     return (t != NULL) && (t->years || t->months || t->days || t->seconds
 136                            || t->offset || t->duration);
 137 }
 138 
 139 void
 140 crm_time_free(crm_time_t * dt)
     /* [previous][next][first][last][top][bottom][index][help] */
 141 {
 142     if (dt == NULL) {
 143         return;
 144     }
 145     free(dt);
 146 }
 147 
 148 static int
 149 year_days(int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 150 {
 151     int d = 365;
 152 
 153     if (crm_time_leapyear(year)) {
 154         d++;
 155     }
 156     return d;
 157 }
 158 
 159 /* From http://myweb.ecu.edu/mccartyr/ISOwdALG.txt :
 160  *
 161  * 5. Find the Jan1Weekday for Y (Monday=1, Sunday=7)
 162  *  YY = (Y-1) % 100
 163  *  C = (Y-1) - YY
 164  *  G = YY + YY/4
 165  *  Jan1Weekday = 1 + (((((C / 100) % 4) x 5) + G) % 7)
 166  */
 167 int
 168 crm_time_january1_weekday(int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 169 {
 170     int YY = (year - 1) % 100;
 171     int C = (year - 1) - YY;
 172     int G = YY + YY / 4;
 173     int jan1 = 1 + (((((C / 100) % 4) * 5) + G) % 7);
 174 
 175     crm_trace("YY=%d, C=%d, G=%d", YY, C, G);
 176     crm_trace("January 1 %.4d: %d", year, jan1);
 177     return jan1;
 178 }
 179 
 180 int
 181 crm_time_weeks_in_year(int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 182 {
 183     int weeks = 52;
 184     int jan1 = crm_time_january1_weekday(year);
 185 
 186     /* if jan1 == thursday */
 187     if (jan1 == 4) {
 188         weeks++;
 189     } else {
 190         jan1 = crm_time_january1_weekday(year + 1);
 191         /* if dec31 == thursday aka. jan1 of next year is a friday */
 192         if (jan1 == 5) {
 193             weeks++;
 194         }
 195 
 196     }
 197     return weeks;
 198 }
 199 
 200 // Jan-Dec plus Feb of leap years
 201 static int month_days[13] = {
 202     31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29
 203 };
 204 
 205 /*!
 206  * \brief Return number of days in given month of given year
 207  *
 208  * \param[in]  Ordinal month (1-12)
 209  * \param[in]  Gregorian year
 210  *
 211  * \return Number of days in given month (0 if given month is invalid)
 212  */
 213 int
 214 crm_time_days_in_month(int month, int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 215 {
 216     if ((month < 1) || (month > 12)) {
 217         return 0;
 218     }
 219     if ((month == 2) && crm_time_leapyear(year)) {
 220         month = 13;
 221     }
 222     return month_days[month - 1];
 223 }
 224 
 225 bool
 226 crm_time_leapyear(int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 227 {
 228     gboolean is_leap = FALSE;
 229 
 230     if (year % 4 == 0) {
 231         is_leap = TRUE;
 232     }
 233     if (year % 100 == 0 && year % 400 != 0) {
 234         is_leap = FALSE;
 235     }
 236     return is_leap;
 237 }
 238 
 239 static uint32_t
 240 get_ordinal_days(uint32_t y, uint32_t m, uint32_t d)
     /* [previous][next][first][last][top][bottom][index][help] */
 241 {
 242     int lpc;
 243 
 244     for (lpc = 1; lpc < m; lpc++) {
 245         d += crm_time_days_in_month(lpc, y);
 246     }
 247     return d;
 248 }
 249 
 250 void
 251 crm_time_log_alias(int log_level, const char *file, const char *function, int line,
     /* [previous][next][first][last][top][bottom][index][help] */
 252                    const char *prefix, crm_time_t * date_time, int flags)
 253 {
 254     char *date_s = crm_time_as_string(date_time, flags);
 255 
 256     if (log_level == LOG_STDOUT) {
 257         printf("%s%s%s\n",
 258                (prefix? prefix : ""), (prefix? ": " : ""), date_s);
 259     } else {
 260         do_crm_log_alias(log_level, file, function, line, "%s%s%s",
 261                          (prefix? prefix : ""), (prefix? ": " : ""), date_s);
 262     }
 263     free(date_s);
 264 }
 265 
 266 static void
 267 crm_time_get_sec(int sec, uint * h, uint * m, uint * s)
     /* [previous][next][first][last][top][bottom][index][help] */
 268 {
 269     uint hours, minutes, seconds;
 270 
 271     if (sec < 0) {
 272         seconds = 0 - sec;
 273     } else {
 274         seconds = sec;
 275     }
 276 
 277     hours = seconds / HOUR_SECONDS;
 278     seconds -= HOUR_SECONDS * hours;
 279 
 280     minutes = seconds / 60;
 281     seconds -= 60 * minutes;
 282 
 283     crm_trace("%d == %.2d:%.2d:%.2d", sec, hours, minutes, seconds);
 284 
 285     *h = hours;
 286     *m = minutes;
 287     *s = seconds;
 288 }
 289 
 290 int
 291 crm_time_get_timeofday(crm_time_t * dt, uint * h, uint * m, uint * s)
     /* [previous][next][first][last][top][bottom][index][help] */
 292 {
 293     crm_time_get_sec(dt->seconds, h, m, s);
 294     return TRUE;
 295 }
 296 
 297 int
 298 crm_time_get_timezone(crm_time_t * dt, uint * h, uint * m)
     /* [previous][next][first][last][top][bottom][index][help] */
 299 {
 300     uint s;
 301 
 302     crm_time_get_sec(dt->seconds, h, m, &s);
 303     return TRUE;
 304 }
 305 
 306 long long
 307 crm_time_get_seconds(crm_time_t * dt)
     /* [previous][next][first][last][top][bottom][index][help] */
 308 {
 309     int lpc;
 310     crm_time_t *utc = NULL;
 311     long long in_seconds = 0;
 312 
 313     if (dt == NULL) {
 314         return 0;
 315     }
 316 
 317     utc = crm_get_utc_time(dt);
 318     if (utc == NULL) {
 319         return 0;
 320     }
 321 
 322     for (lpc = 1; lpc < utc->years; lpc++) {
 323         long long dmax = year_days(lpc);
 324 
 325         in_seconds += DAY_SECONDS * dmax;
 326     }
 327 
 328     /* utc->months is an offset that can only be set for a duration.
 329      * By definition, the value is variable depending on the date to
 330      * which it is applied.
 331      *
 332      * Force 30-day months so that something vaguely sane happens
 333      * for anyone that tries to use a month in this way.
 334      */
 335     if (utc->months > 0) {
 336         in_seconds += DAY_SECONDS * 30 * (long long) (utc->months);
 337     }
 338 
 339     if (utc->days > 0) {
 340         in_seconds += DAY_SECONDS * (long long) (utc->days - 1);
 341     }
 342     in_seconds += utc->seconds;
 343 
 344     crm_time_free(utc);
 345     return in_seconds;
 346 }
 347 
 348 #define EPOCH_SECONDS 62135596800ULL    /* Calculated using crm_time_get_seconds() */
 349 long long
 350 crm_time_get_seconds_since_epoch(crm_time_t * dt)
     /* [previous][next][first][last][top][bottom][index][help] */
 351 {
 352     return (dt == NULL)? 0 : (crm_time_get_seconds(dt) - EPOCH_SECONDS);
 353 }
 354 
 355 int
 356 crm_time_get_gregorian(crm_time_t * dt, uint * y, uint * m, uint * d)
     /* [previous][next][first][last][top][bottom][index][help] */
 357 {
 358     int months = 0;
 359     int days = dt->days;
 360 
 361     if(dt->years != 0) {
 362         for (months = 1; months <= 12 && days > 0; months++) {
 363             int mdays = crm_time_days_in_month(months, dt->years);
 364 
 365             if (mdays >= days) {
 366                 break;
 367             } else {
 368                 days -= mdays;
 369             }
 370         }
 371 
 372     } else if (dt->months) {
 373         /* This is a duration including months, don't convert the days field */
 374         months = dt->months;
 375 
 376     } else {
 377         /* This is a duration not including months, still don't convert the days field */
 378     }
 379 
 380     *y = dt->years;
 381     *m = months;
 382     *d = days;
 383     crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days);
 384     return TRUE;
 385 }
 386 
 387 int
 388 crm_time_get_ordinal(crm_time_t * dt, uint * y, uint * d)
     /* [previous][next][first][last][top][bottom][index][help] */
 389 {
 390     *y = dt->years;
 391     *d = dt->days;
 392     return TRUE;
 393 }
 394 
 395 int
 396 crm_time_get_isoweek(crm_time_t * dt, uint * y, uint * w, uint * d)
     /* [previous][next][first][last][top][bottom][index][help] */
 397 {
 398     /*
 399      * Monday 29 December 2008 is written "2009-W01-1"
 400      * Sunday 3 January 2010 is written "2009-W53-7"
 401      */
 402     int year_num = 0;
 403     int jan1 = crm_time_january1_weekday(dt->years);
 404     int h = -1;
 405 
 406     CRM_CHECK(dt->days > 0, return FALSE);
 407 
 408 /* 6. Find the Weekday for Y M D */
 409     h = dt->days + jan1 - 1;
 410     *d = 1 + ((h - 1) % 7);
 411 
 412 /* 7. Find if Y M D falls in YearNumber Y-1, WeekNumber 52 or 53 */
 413     if (dt->days <= (8 - jan1) && jan1 > 4) {
 414         crm_trace("year--, jan1=%d", jan1);
 415         year_num = dt->years - 1;
 416         *w = crm_time_weeks_in_year(year_num);
 417 
 418     } else {
 419         year_num = dt->years;
 420     }
 421 
 422 /* 8. Find if Y M D falls in YearNumber Y+1, WeekNumber 1 */
 423     if (year_num == dt->years) {
 424         int dmax = year_days(year_num);
 425         int correction = 4 - *d;
 426 
 427         if ((dmax - dt->days) < correction) {
 428             crm_trace("year++, jan1=%d, i=%d vs. %d", jan1, dmax - dt->days, correction);
 429             year_num = dt->years + 1;
 430             *w = 1;
 431         }
 432     }
 433 
 434 /* 9. Find if Y M D falls in YearNumber Y, WeekNumber 1 through 53 */
 435     if (year_num == dt->years) {
 436         int j = dt->days + (7 - *d) + (jan1 - 1);
 437 
 438         *w = j / 7;
 439         if (jan1 > 4) {
 440             *w -= 1;
 441         }
 442     }
 443 
 444     *y = year_num;
 445     crm_trace("Converted %.4d-%.3d to %.4d-W%.2d-%d", dt->years, dt->days, *y, *w, *d);
 446     return TRUE;
 447 }
 448 
 449 #define DATE_MAX 128
 450 
 451 static void
 452 crm_duration_as_string(crm_time_t *dt, char *result)
     /* [previous][next][first][last][top][bottom][index][help] */
 453 {
 454     size_t offset = 0;
 455 
 456     if (dt->years) {
 457         offset += snprintf(result + offset, DATE_MAX - offset, "%4d year%s ",
 458                            dt->years, pcmk__plural_s(dt->years));
 459     }
 460     if (dt->months) {
 461         offset += snprintf(result + offset, DATE_MAX - offset, "%2d month%s ",
 462                            dt->months, pcmk__plural_s(dt->months));
 463     }
 464     if (dt->days) {
 465         offset += snprintf(result + offset, DATE_MAX - offset, "%2d day%s ",
 466                            dt->days, pcmk__plural_s(dt->days));
 467     }
 468 
 469     if (((offset == 0) || (dt->seconds != 0))
 470         && (dt->seconds > -60) && (dt->seconds < 60)) {
 471         offset += snprintf(result + offset, DATE_MAX - offset, "%d second%s",
 472                            dt->seconds, pcmk__plural_s(dt->seconds));
 473     } else if (dt->seconds) {
 474         uint h = 0, m = 0, s = 0;
 475 
 476         offset += snprintf(result + offset, DATE_MAX - offset, "%d seconds (",
 477                            dt->seconds);
 478         crm_time_get_sec(dt->seconds, &h, &m, &s);
 479         if (h) {
 480             offset += snprintf(result + offset, DATE_MAX - offset, "%u hour%s%s",
 481                                h, pcmk__plural_s(h), ((m || s)? " " : ""));
 482         }
 483         if (m) {
 484             offset += snprintf(result + offset, DATE_MAX - offset, "%u minute%s%s",
 485                                m, pcmk__plural_s(m), (s? " " : ""));
 486         }
 487         if (s) {
 488             offset += snprintf(result + offset, DATE_MAX - offset, "%u second%s",
 489                                s, pcmk__plural_s(s));
 490         }
 491         offset += snprintf(result + offset, DATE_MAX - offset, ")");
 492     }
 493 }
 494 
 495 char *
 496 crm_time_as_string(crm_time_t * date_time, int flags)
     /* [previous][next][first][last][top][bottom][index][help] */
 497 {
 498     crm_time_t *dt = NULL;
 499     crm_time_t *utc = NULL;
 500     char result[DATE_MAX] = { '\0', };
 501     char *result_copy = NULL;
 502     size_t offset = 0;
 503 
 504     // Convert to UTC if local timezone was not requested
 505     if (date_time && date_time->offset
 506         && !pcmk_is_set(flags, crm_time_log_with_timezone)) {
 507         crm_trace("UTC conversion");
 508         utc = crm_get_utc_time(date_time);
 509         dt = utc;
 510     } else {
 511         dt = date_time;
 512     }
 513 
 514     if (!crm_time_is_defined(dt)) {
 515         strcpy(result, "<undefined time>");
 516         goto done;
 517     }
 518 
 519     // Simple cases: as duration, seconds, or seconds since epoch
 520 
 521     if (flags & crm_time_log_duration) {
 522         crm_duration_as_string(date_time, result);
 523         goto done;
 524     }
 525 
 526     if (flags & crm_time_seconds) {
 527         snprintf(result, DATE_MAX, "%lld", crm_time_get_seconds(date_time));
 528         goto done;
 529     }
 530 
 531     if (flags & crm_time_epoch) {
 532         snprintf(result, DATE_MAX, "%lld",
 533                  crm_time_get_seconds_since_epoch(date_time));
 534         goto done;
 535     }
 536 
 537     // As readable string
 538 
 539     if (flags & crm_time_log_date) {
 540         if (flags & crm_time_weeks) { // YYYY-WW-D
 541             uint y, w, d;
 542 
 543             if (crm_time_get_isoweek(dt, &y, &w, &d)) {
 544                 offset += snprintf(result + offset, DATE_MAX - offset,
 545                                    "%u-W%.2u-%u", y, w, d);
 546             }
 547 
 548         } else if (flags & crm_time_ordinal) { // YYYY-DDD
 549             uint y, d;
 550 
 551             if (crm_time_get_ordinal(dt, &y, &d)) {
 552                 offset += snprintf(result + offset, DATE_MAX - offset,
 553                                    "%u-%.3u", y, d);
 554             }
 555 
 556         } else { // YYYY-MM-DD
 557             uint y, m, d;
 558 
 559             if (crm_time_get_gregorian(dt, &y, &m, &d)) {
 560                 offset += snprintf(result + offset, DATE_MAX - offset,
 561                                    "%.4u-%.2u-%.2u", y, m, d);
 562             }
 563         }
 564     }
 565 
 566     if (flags & crm_time_log_timeofday) {
 567         uint h = 0, m = 0, s = 0;
 568 
 569         if (offset > 0) {
 570             offset += snprintf(result + offset, DATE_MAX - offset, " ");
 571         }
 572 
 573         if (crm_time_get_timeofday(dt, &h, &m, &s)) {
 574             offset += snprintf(result + offset, DATE_MAX - offset,
 575                                "%.2u:%.2u:%.2u", h, m, s);
 576         }
 577 
 578         if ((flags & crm_time_log_with_timezone) && (dt->offset != 0)) {
 579             crm_time_get_sec(dt->offset, &h, &m, &s);
 580             offset += snprintf(result + offset, DATE_MAX - offset,
 581                                " %c%.2u:%.2u",
 582                                ((dt->offset < 0)? '-' : '+'), h, m);
 583         } else {
 584             offset += snprintf(result + offset, DATE_MAX - offset, "Z");
 585         }
 586     }
 587 
 588   done:
 589     crm_time_free(utc);
 590 
 591     result_copy = strdup(result);
 592     CRM_ASSERT(result_copy != NULL);
 593     return result_copy;
 594 }
 595 
 596 /*!
 597  * \internal
 598  * \brief Determine number of seconds from an hour:minute:second string
 599  *
 600  * \param[in]  time_str  Time specification string
 601  * \param[out] result    Number of seconds equivalent to time_str
 602  *
 603  * \return TRUE if specification was valid, FALSE (and set errno) otherwise
 604  * \note This may return the number of seconds in a day (which is out of bounds
 605  *       for a time object) if given 24:00:00.
 606  */
 607 static bool
 608 crm_time_parse_sec(const char *time_str, int *result)
     /* [previous][next][first][last][top][bottom][index][help] */
 609 {
 610     int rc;
 611     uint hour = 0;
 612     uint minute = 0;
 613     uint second = 0;
 614 
 615     *result = 0;
 616 
 617     // Must have at least hour, but minutes and seconds are optional
 618     rc = sscanf(time_str, "%d:%d:%d", &hour, &minute, &second);
 619     if (rc == 1) {
 620         rc = sscanf(time_str, "%2d%2d%2d", &hour, &minute, &second);
 621     }
 622     if (rc == 0) {
 623         crm_err("%s is not a valid ISO 8601 time specification", time_str);
 624         errno = EINVAL;
 625         return FALSE;
 626     }
 627 
 628     crm_trace("Got valid time: %.2d:%.2d:%.2d", hour, minute, second);
 629 
 630     if ((hour == 24) && (minute == 0) && (second == 0)) {
 631         // Equivalent to 00:00:00 of next day, return number of seconds in day
 632     } else if (hour >= 24) {
 633         crm_err("%s is not a valid ISO 8601 time specification "
 634                 "because %d is not a valid hour", time_str, hour);
 635         errno = EINVAL;
 636         return FALSE;
 637     }
 638     if (minute >= 60) {
 639         crm_err("%s is not a valid ISO 8601 time specification "
 640                 "because %d is not a valid minute", time_str, minute);
 641         errno = EINVAL;
 642         return FALSE;
 643     }
 644     if (second >= 60) {
 645         crm_err("%s is not a valid ISO 8601 time specification "
 646                 "because %d is not a valid second", time_str, second);
 647         errno = EINVAL;
 648         return FALSE;
 649     }
 650 
 651     *result = (hour * HOUR_SECONDS) + (minute * 60) + second;
 652     return TRUE;
 653 }
 654 
 655 static bool
 656 crm_time_parse_offset(const char *offset_str, int *offset)
     /* [previous][next][first][last][top][bottom][index][help] */
 657 {
 658     tzset();
 659 
 660     if (offset_str == NULL) {
 661         // Use local offset
 662 #if defined(HAVE_STRUCT_TM_TM_GMTOFF)
 663         time_t now = time(NULL);
 664         struct tm *now_tm = localtime(&now);
 665 #endif
 666         int h_offset = GMTOFF(now_tm) / HOUR_SECONDS;
 667         int m_offset = (GMTOFF(now_tm) - (HOUR_SECONDS * h_offset)) / 60;
 668 
 669         if (h_offset < 0 && m_offset < 0) {
 670             m_offset = 0 - m_offset;
 671         }
 672         *offset = (HOUR_SECONDS * h_offset) + (60 * m_offset);
 673         return TRUE;
 674     }
 675 
 676     if (offset_str[0] == 'Z') { // @TODO invalid if anything after?
 677         *offset = 0;
 678         return TRUE;
 679     }
 680 
 681     *offset = 0;
 682     if ((offset_str[0] == '+') || (offset_str[0] == '-')
 683         || isdigit((int)offset_str[0])) {
 684 
 685         gboolean negate = FALSE;
 686 
 687         if (offset_str[0] == '+') {
 688             offset_str++;
 689         } else if (offset_str[0] == '-') {
 690             negate = TRUE;
 691             offset_str++;
 692         }
 693         if (crm_time_parse_sec(offset_str, offset) == FALSE) {
 694             return FALSE;
 695         }
 696         if (negate) {
 697             *offset = 0 - *offset;
 698         }
 699     } // @TODO else invalid?
 700     return TRUE;
 701 }
 702 
 703 /*!
 704  * \internal
 705  * \brief Parse the time portion of an ISO 8601 date/time string
 706  *
 707  * \param[in]     time_str  Time portion of specification (after any 'T')
 708  * \param[in,out] a_time    Time object to parse into
 709  *
 710  * \return TRUE if valid time was parsed, FALSE (and set errno) otherwise
 711  * \note This may add a day to a_time (if the time is 24:00:00).
 712  */
 713 static bool
 714 crm_time_parse(const char *time_str, crm_time_t *a_time)
     /* [previous][next][first][last][top][bottom][index][help] */
 715 {
 716     uint h, m, s;
 717     char *offset_s = NULL;
 718 
 719     tzset();
 720 
 721     if (time_str) {
 722         if (crm_time_parse_sec(time_str, &(a_time->seconds)) == FALSE) {
 723             return FALSE;
 724         }
 725         offset_s = strstr(time_str, "Z");
 726         if (offset_s == NULL) {
 727             offset_s = strstr(time_str, " ");
 728             if (offset_s) {
 729                 while (isspace(offset_s[0])) {
 730                     offset_s++;
 731                 }
 732             }
 733         }
 734     }
 735 
 736     if (crm_time_parse_offset(offset_s, &(a_time->offset)) == FALSE) {
 737         return FALSE;
 738     }
 739     crm_time_get_sec(a_time->offset, &h, &m, &s);
 740     crm_trace("Got tz: %c%2.d:%.2d", ((a_time->offset < 0)? '-' : '+'), h, m);
 741 
 742     if (a_time->seconds == DAY_SECONDS) {
 743         // 24:00:00 == 00:00:00 of next day
 744         a_time->seconds = 0;
 745         crm_time_add_days(a_time, 1);
 746     }
 747     return TRUE;
 748 }
 749 
 750 /*
 751  * \internal
 752  * \brief Parse a time object from an ISO 8601 date/time specification
 753  *
 754  * \param[in] date_str  ISO 8601 date/time specification (or "epoch")
 755  *
 756  * \return New time object on success, NULL (and set errno) otherwise
 757  */
 758 static crm_time_t *
 759 parse_date(const char *date_str)
     /* [previous][next][first][last][top][bottom][index][help] */
 760 {
 761     const char *time_s = NULL;
 762     crm_time_t *dt = NULL;
 763 
 764     int year = 0;
 765     int month = 0;
 766     int week = 0;
 767     int day = 0;
 768     int rc = 0;
 769 
 770     if (pcmk__str_empty(date_str)) {
 771         crm_err("No ISO 8601 date/time specification given");
 772         goto invalid;
 773     }
 774 
 775     if ((date_str[0] == 'T') || (date_str[2] == ':')) {
 776         /* Just a time supplied - Infer current date */
 777         dt = crm_time_new(NULL);
 778         if (date_str[0] == 'T') {
 779             time_s = date_str + 1;
 780         } else {
 781             time_s = date_str;
 782         }
 783         goto parse_time;
 784     }
 785 
 786     dt = crm_time_new_undefined();
 787 
 788     if (!strncasecmp("epoch", date_str, 5)
 789         && ((date_str[5] == '\0') || (date_str[5] == '/') || isspace(date_str[5]))) {
 790         dt->days = 1;
 791         dt->years = 1970;
 792         crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
 793         return dt;
 794     }
 795 
 796     /* YYYY-MM-DD */
 797     rc = sscanf(date_str, "%d-%d-%d", &year, &month, &day);
 798     if (rc == 1) {
 799         /* YYYYMMDD */
 800         rc = sscanf(date_str, "%4d%2d%2d", &year, &month, &day);
 801     }
 802     if (rc == 3) {
 803         if (month > 12) {
 804             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 805                     "because '%d' is not a valid month", date_str, month);
 806             goto invalid;
 807         } else if (day > crm_time_days_in_month(month, year)) {
 808             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 809                     "because '%d' is not a valid day of the month",
 810                     date_str, day);
 811             goto invalid;
 812         } else {
 813             dt->years = year;
 814             dt->days = get_ordinal_days(year, month, day);
 815             crm_trace("Parsed Gregorian date '%.4d-%.3d' from date string '%s'",
 816                       year, dt->days, date_str);
 817         }
 818         goto parse_time;
 819     }
 820 
 821     /* YYYY-DDD */
 822     rc = sscanf(date_str, "%d-%d", &year, &day);
 823     if (rc == 2) {
 824         if (day > year_days(year)) {
 825             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 826                     "because '%d' is not a valid day of the year (max %d)",
 827                     date_str, day, year_days(year));
 828             goto invalid;
 829         }
 830         crm_trace("Parsed ordinal year %d and days %d from date string '%s'",
 831                   year, day, date_str);
 832         dt->days = day;
 833         dt->years = year;
 834         goto parse_time;
 835     }
 836 
 837     /* YYYY-Www-D */
 838     rc = sscanf(date_str, "%d-W%d-%d", &year, &week, &day);
 839     if (rc == 3) {
 840         if (week > crm_time_weeks_in_year(year)) {
 841             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 842                     "because '%d' is not a valid week of the year (max %d)",
 843                     date_str, week, crm_time_weeks_in_year(year));
 844             goto invalid;
 845         } else if (day < 1 || day > 7) {
 846             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 847                     "because '%d' is not a valid day of the week",
 848                     date_str, day);
 849             goto invalid;
 850         } else {
 851             /*
 852              * See https://en.wikipedia.org/wiki/ISO_week_date
 853              *
 854              * Monday 29 December 2008 is written "2009-W01-1"
 855              * Sunday 3 January 2010 is written "2009-W53-7"
 856              * Saturday 27 September 2008 is written "2008-W37-6"
 857              *
 858              * If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it is in week 01.
 859              * If 1 January is on a Friday, Saturday or Sunday, it is in week 52 or 53 of the previous year.
 860              */
 861             int jan1 = crm_time_january1_weekday(year);
 862 
 863             crm_trace("Got year %d (Jan 1 = %d), week %d, and day %d from date string '%s'",
 864                       year, jan1, week, day, date_str);
 865 
 866             dt->years = year;
 867             crm_time_add_days(dt, (week - 1) * 7);
 868 
 869             if (jan1 <= 4) {
 870                 crm_time_add_days(dt, 1 - jan1);
 871             } else {
 872                 crm_time_add_days(dt, 8 - jan1);
 873             }
 874 
 875             crm_time_add_days(dt, day);
 876         }
 877         goto parse_time;
 878     }
 879 
 880     crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str);
 881     goto invalid;
 882 
 883   parse_time:
 884 
 885     if (time_s == NULL) {
 886         time_s = date_str + strspn(date_str, "0123456789-W");
 887         if ((time_s[0] == ' ') || (time_s[0] == 'T')) {
 888             ++time_s;
 889         } else {
 890             time_s = NULL;
 891         }
 892     }
 893     if ((time_s != NULL) && (crm_time_parse(time_s, dt) == FALSE)) {
 894         goto invalid;
 895     }
 896 
 897     crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
 898     if (crm_time_check(dt) == FALSE) {
 899         crm_err("'%s' is not a valid ISO 8601 date/time specification",
 900                 date_str);
 901         goto invalid;
 902     }
 903     return dt;
 904 
 905 invalid:
 906     crm_time_free(dt);
 907     errno = EINVAL;
 908     return NULL;
 909 }
 910 
 911 // Parse an ISO 8601 numeric value and return number of characters consumed
 912 // @TODO This cannot handle >INT_MAX int values
 913 // @TODO Fractions appear to be not working
 914 // @TODO Error out on invalid specifications
 915 static int
 916 parse_int(const char *str, int field_width, int upper_bound, int *result)
     /* [previous][next][first][last][top][bottom][index][help] */
 917 {
 918     int lpc = 0;
 919     int offset = 0;
 920     int intermediate = 0;
 921     gboolean fraction = FALSE;
 922     gboolean negate = FALSE;
 923 
 924     *result = 0;
 925     if (*str == '\0') {
 926         return 0;
 927     }
 928 
 929     if (str[offset] == 'T') {
 930         offset++;
 931     }
 932 
 933     if (str[offset] == '.' || str[offset] == ',') {
 934         fraction = TRUE;
 935         field_width = -1;
 936         offset++;
 937     } else if (str[offset] == '-') {
 938         negate = TRUE;
 939         offset++;
 940     } else if (str[offset] == '+' || str[offset] == ':') {
 941         offset++;
 942     }
 943 
 944     for (; (fraction || lpc < field_width) && isdigit((int)str[offset]); lpc++) {
 945         if (fraction) {
 946             intermediate = (str[offset] - '0') / (10 ^ lpc);
 947         } else {
 948             *result *= 10;
 949             intermediate = str[offset] - '0';
 950         }
 951         *result += intermediate;
 952         offset++;
 953     }
 954     if (fraction) {
 955         *result = (int)(*result * upper_bound);
 956 
 957     } else if (upper_bound > 0 && *result > upper_bound) {
 958         *result = upper_bound;
 959     }
 960     if (negate) {
 961         *result = 0 - *result;
 962     }
 963     if (lpc > 0) {
 964         crm_trace("Found int: %d.  Stopped at str[%d]='%c'", *result, lpc, str[lpc]);
 965         return offset;
 966     }
 967     return 0;
 968 }
 969 
 970 /*!
 971  * \brief Parse a time duration from an ISO 8601 duration specification
 972  *
 973  * \param[in] period_s  ISO 8601 duration specification (optionally followed by
 974  *                      whitespace, after which the rest of the string will be
 975  *                      ignored)
 976  *
 977  * \return New time object on success, NULL (and set errno) otherwise
 978  * \note It is the caller's responsibility to return the result using
 979  *       crm_time_free().
 980  */
 981 crm_time_t *
 982 crm_time_parse_duration(const char *period_s)
     /* [previous][next][first][last][top][bottom][index][help] */
 983 {
 984     gboolean is_time = FALSE;
 985     crm_time_t *diff = NULL;
 986 
 987     if (pcmk__str_empty(period_s)) {
 988         crm_err("No ISO 8601 time duration given");
 989         goto invalid;
 990     }
 991     if (period_s[0] != 'P') {
 992         crm_err("'%s' is not a valid ISO 8601 time duration "
 993                 "because it does not start with a 'P'", period_s);
 994         goto invalid;
 995     }
 996     if ((period_s[1] == '\0') || isspace(period_s[1])) {
 997         crm_err("'%s' is not a valid ISO 8601 time duration "
 998                 "because nothing follows 'P'", period_s);
 999         goto invalid;
1000     }
1001 
1002     diff = crm_time_new_undefined();
1003     diff->duration = TRUE;
1004 
1005     for (const char *current = period_s + 1;
1006          current[0] && (current[0] != '/') && !isspace(current[0]);
1007          ++current) {
1008 
1009         int an_int = 0, rc;
1010 
1011         if (current[0] == 'T') {
1012             /* A 'T' separates year/month/day from hour/minute/seconds. We don't
1013              * require it strictly, but just use it to differentiate month from
1014              * minutes.
1015              */
1016             is_time = TRUE;
1017             continue;
1018         }
1019 
1020         // An integer must be next
1021         rc = parse_int(current, 10, 0, &an_int);
1022         if (rc == 0) {
1023             crm_err("'%s' is not a valid ISO 8601 time duration "
1024                     "because no integer at '%s'", period_s, current);
1025             goto invalid;
1026         }
1027         current += rc;
1028 
1029         // A time unit must be next (we're not strict about the order)
1030         switch (current[0]) {
1031             case 'Y':
1032                 diff->years = an_int;
1033                 break;
1034             case 'M':
1035                 if (is_time) {
1036                     /* Minutes */
1037                     diff->seconds += an_int * 60;
1038                 } else {
1039                     diff->months = an_int;
1040                 }
1041                 break;
1042             case 'W':
1043                 diff->days += an_int * 7;
1044                 break;
1045             case 'D':
1046                 diff->days += an_int;
1047                 break;
1048             case 'H':
1049                 diff->seconds += an_int * HOUR_SECONDS;
1050                 break;
1051             case 'S':
1052                 diff->seconds += an_int;
1053                 break;
1054             case '\0':
1055                 crm_err("'%s' is not a valid ISO 8601 time duration "
1056                         "because no units after %d", period_s, an_int);
1057                 goto invalid;
1058             default:
1059                 crm_err("'%s' is not a valid ISO 8601 time duration "
1060                         "because '%c' is not a valid time unit",
1061                         period_s, current[0]);
1062                 goto invalid;
1063         }
1064     }
1065 
1066     if (!crm_time_is_defined(diff)) {
1067         crm_err("'%s' is not a valid ISO 8601 time duration "
1068                 "because no amounts and units given", period_s);
1069         goto invalid;
1070     }
1071     return diff;
1072 
1073 invalid:
1074     crm_time_free(diff);
1075     errno = EINVAL;
1076     return NULL;
1077 }
1078 
1079 /*!
1080  * \brief Parse a time period from an ISO 8601 interval specification
1081  *
1082  * \param[in] period_str  ISO 8601 interval specification (start/end,
1083  *                        start/duration, or duration/end)
1084  *
1085  * \return New time period object on success, NULL (and set errno) otherwise
1086  * \note The caller is responsible for freeing the result using
1087  *       crm_time_free_period().
1088  */
1089 crm_time_period_t *
1090 crm_time_parse_period(const char *period_str)
     /* [previous][next][first][last][top][bottom][index][help] */
1091 {
1092     const char *original = period_str;
1093     crm_time_period_t *period = NULL;
1094 
1095     if (pcmk__str_empty(period_str)) {
1096         crm_err("No ISO 8601 time period given");
1097         goto invalid;
1098     }
1099 
1100     tzset();
1101     period = calloc(1, sizeof(crm_time_period_t));
1102     CRM_ASSERT(period != NULL);
1103 
1104     if (period_str[0] == 'P') {
1105         period->diff = crm_time_parse_duration(period_str);
1106         if (period->diff == NULL) {
1107             goto error;
1108         }
1109     } else {
1110         period->start = parse_date(period_str);
1111         if (period->start == NULL) {
1112             goto error;
1113         }
1114     }
1115 
1116     period_str = strstr(original, "/");
1117     if (period_str) {
1118         ++period_str;
1119         if (period_str[0] == 'P') {
1120             if (period->diff != NULL) {
1121                 crm_err("'%s' is not a valid ISO 8601 time period "
1122                         "because it has two durations",
1123                         original);
1124                 goto invalid;
1125             }
1126             period->diff = crm_time_parse_duration(period_str);
1127             if (period->diff == NULL) {
1128                 goto error;
1129             }
1130         } else {
1131             period->end = parse_date(period_str);
1132             if (period->end == NULL) {
1133                 goto error;
1134             }
1135         }
1136 
1137     } else if (period->diff != NULL) {
1138         // Only duration given, assume start is now
1139         period->start = crm_time_new(NULL);
1140 
1141     } else {
1142         // Only start given
1143         crm_err("'%s' is not a valid ISO 8601 time period "
1144                 "because it has no duration or ending time",
1145                 original);
1146         goto invalid;
1147     }
1148 
1149     if (period->start == NULL) {
1150         period->start = crm_time_subtract(period->end, period->diff);
1151 
1152     } else if (period->end == NULL) {
1153         period->end = crm_time_add(period->start, period->diff);
1154     }
1155 
1156     if (crm_time_check(period->start) == FALSE) {
1157         crm_err("'%s' is not a valid ISO 8601 time period "
1158                 "because the start is invalid", period_str);
1159         goto invalid;
1160     }
1161     if (crm_time_check(period->end) == FALSE) {
1162         crm_err("'%s' is not a valid ISO 8601 time period "
1163                 "because the end is invalid", period_str);
1164         goto invalid;
1165     }
1166     return period;
1167 
1168 invalid:
1169     errno = EINVAL;
1170 error:
1171     crm_time_free_period(period);
1172     return NULL;
1173 }
1174 
1175 /*!
1176  * \brief Free a dynamically allocated time period object
1177  *
1178  * \param[in] period  Time period to free
1179  */
1180 void
1181 crm_time_free_period(crm_time_period_t *period)
     /* [previous][next][first][last][top][bottom][index][help] */
1182 {
1183     if (period) {
1184         crm_time_free(period->start);
1185         crm_time_free(period->end);
1186         crm_time_free(period->diff);
1187         free(period);
1188     }
1189 }
1190 
1191 void
1192 crm_time_set(crm_time_t * target, crm_time_t * source)
     /* [previous][next][first][last][top][bottom][index][help] */
1193 {
1194     crm_trace("target=%p, source=%p", target, source);
1195 
1196     CRM_CHECK(target != NULL && source != NULL, return);
1197 
1198     target->years = source->years;
1199     target->days = source->days;
1200     target->months = source->months;    /* Only for durations */
1201     target->seconds = source->seconds;
1202     target->offset = source->offset;
1203 
1204     crm_time_log(LOG_TRACE, "source", source,
1205                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
1206     crm_time_log(LOG_TRACE, "target", target,
1207                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
1208 }
1209 
1210 static void
1211 ha_set_tm_time(crm_time_t * target, struct tm *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1212 {
1213     int h_offset = 0;
1214     int m_offset = 0;
1215 
1216     /* Ensure target is fully initialized */
1217     target->years = 0;
1218     target->months = 0;
1219     target->days = 0;
1220     target->seconds = 0;
1221     target->offset = 0;
1222     target->duration = FALSE;
1223 
1224     if (source->tm_year > 0) {
1225         /* years since 1900 */
1226         target->years = 1900 + source->tm_year;
1227     }
1228 
1229     if (source->tm_yday >= 0) {
1230         /* days since January 1 [0-365] */
1231         target->days = 1 + source->tm_yday;
1232     }
1233 
1234     if (source->tm_hour >= 0) {
1235         target->seconds += HOUR_SECONDS * source->tm_hour;
1236     }
1237     if (source->tm_min >= 0) {
1238         target->seconds += 60 * source->tm_min;
1239     }
1240     if (source->tm_sec >= 0) {
1241         target->seconds += source->tm_sec;
1242     }
1243 
1244     /* tm_gmtoff == offset from UTC in seconds */
1245     h_offset = GMTOFF(source) / HOUR_SECONDS;
1246     m_offset = (GMTOFF(source) - (HOUR_SECONDS * h_offset)) / 60;
1247     crm_trace("Time offset is %lds (%.2d:%.2d)",
1248               GMTOFF(source), h_offset, m_offset);
1249 
1250     target->offset += HOUR_SECONDS * h_offset;
1251     target->offset += 60 * m_offset;
1252 }
1253 
1254 void
1255 crm_time_set_timet(crm_time_t * target, time_t * source)
     /* [previous][next][first][last][top][bottom][index][help] */
1256 {
1257     ha_set_tm_time(target, localtime(source));
1258 }
1259 
1260 crm_time_t *
1261 pcmk_copy_time(crm_time_t *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1262 {
1263     crm_time_t *target = crm_time_new_undefined();
1264 
1265     crm_time_set(target, source);
1266     return target;
1267 }
1268 
1269 crm_time_t *
1270 crm_time_add(crm_time_t * dt, crm_time_t * value)
     /* [previous][next][first][last][top][bottom][index][help] */
1271 {
1272     crm_time_t *utc = NULL;
1273     crm_time_t *answer = NULL;
1274 
1275     if ((dt == NULL) || (value == NULL)) {
1276         errno = EINVAL;
1277         return NULL;
1278     }
1279 
1280     answer = pcmk_copy_time(dt);
1281 
1282     utc = crm_get_utc_time(value);
1283     if (utc == NULL) {
1284         crm_time_free(answer);
1285         return NULL;
1286     }
1287 
1288     answer->years += utc->years;
1289     crm_time_add_months(answer, utc->months);
1290     crm_time_add_days(answer, utc->days);
1291     crm_time_add_seconds(answer, utc->seconds);
1292 
1293     crm_time_free(utc);
1294     return answer;
1295 }
1296 
1297 crm_time_t *
1298 crm_time_calculate_duration(crm_time_t * dt, crm_time_t * value)
     /* [previous][next][first][last][top][bottom][index][help] */
1299 {
1300     crm_time_t *utc = NULL;
1301     crm_time_t *answer = NULL;
1302 
1303     if ((dt == NULL) || (value == NULL)) {
1304         errno = EINVAL;
1305         return NULL;
1306     }
1307 
1308     utc = crm_get_utc_time(value);
1309     if (utc == NULL) {
1310         return NULL;
1311     }
1312 
1313     answer = crm_get_utc_time(dt);
1314     if (answer == NULL) {
1315         crm_time_free(utc);
1316         return NULL;
1317     }
1318     answer->duration = TRUE;
1319 
1320     answer->years -= utc->years;
1321     if(utc->months != 0) {
1322         crm_time_add_months(answer, -utc->months);
1323     }
1324     crm_time_add_days(answer, -utc->days);
1325     crm_time_add_seconds(answer, -utc->seconds);
1326 
1327     crm_time_free(utc);
1328     return answer;
1329 }
1330 
1331 crm_time_t *
1332 crm_time_subtract(crm_time_t * dt, crm_time_t * value)
     /* [previous][next][first][last][top][bottom][index][help] */
1333 {
1334     crm_time_t *utc = NULL;
1335     crm_time_t *answer = NULL;
1336 
1337     if ((dt == NULL) || (value == NULL)) {
1338         errno = EINVAL;
1339         return NULL;
1340     }
1341 
1342     utc = crm_get_utc_time(value);
1343     if (utc == NULL) {
1344         return NULL;
1345     }
1346 
1347     answer = pcmk_copy_time(dt);
1348     answer->years -= utc->years;
1349     if(utc->months != 0) {
1350         crm_time_add_months(answer, -utc->months);
1351     }
1352     crm_time_add_days(answer, -utc->days);
1353     crm_time_add_seconds(answer, -utc->seconds);
1354 
1355     return answer;
1356 }
1357 
1358 /*!
1359  * \brief Check whether a time object represents a sensible date/time
1360  *
1361  * \param[in] dt  Date/time object to check
1362  *
1363  * \return TRUE if years, days, and seconds are sensible, FALSE otherwise
1364  */
1365 bool
1366 crm_time_check(crm_time_t * dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1367 {
1368     return (dt != NULL)
1369            && (dt->days > 0) && (dt->days <= year_days(dt->years))
1370            && (dt->seconds >= 0) && (dt->seconds < DAY_SECONDS);
1371 }
1372 
1373 #define do_cmp_field(l, r, field)                                       \
1374     if(rc == 0) {                                                       \
1375                 if(l->field > r->field) {                               \
1376                         crm_trace("%s: %d > %d",                        \
1377                                     #field, l->field, r->field);        \
1378                         rc = 1;                                         \
1379                 } else if(l->field < r->field) {                        \
1380                         crm_trace("%s: %d < %d",                        \
1381                                     #field, l->field, r->field);        \
1382                         rc = -1;                                        \
1383                 }                                                       \
1384     }
1385 
1386 int
1387 crm_time_compare(crm_time_t *a, crm_time_t *b)
     /* [previous][next][first][last][top][bottom][index][help] */
1388 {
1389     int rc = 0;
1390     crm_time_t *t1 = crm_get_utc_time(a);
1391     crm_time_t *t2 = crm_get_utc_time(b);
1392 
1393     if ((t1 == NULL) && (t2 == NULL)) {
1394         rc = 0;
1395     } else if (t1 == NULL) {
1396         rc = -1;
1397     } else if (t2 == NULL) {
1398         rc = 1;
1399     } else {
1400         do_cmp_field(t1, t2, years);
1401         do_cmp_field(t1, t2, days);
1402         do_cmp_field(t1, t2, seconds);
1403     }
1404 
1405     crm_time_free(t1);
1406     crm_time_free(t2);
1407     return rc;
1408 }
1409 
1410 /*!
1411  * \brief Add a given number of seconds to a date/time or duration
1412  *
1413  * \param[in] a_time  Date/time or duration to add seconds to
1414  * \param[in] extra   Number of seconds to add
1415  */
1416 void
1417 crm_time_add_seconds(crm_time_t *a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1418 {
1419     int days = 0;
1420 
1421     crm_trace("Adding %d seconds to %d (max=%d)",
1422               extra, a_time->seconds, DAY_SECONDS);
1423     a_time->seconds += extra;
1424     days = a_time->seconds / DAY_SECONDS;
1425     a_time->seconds %= DAY_SECONDS;
1426 
1427     // Don't have negative seconds
1428     if (a_time->seconds < 0) {
1429         a_time->seconds += DAY_SECONDS;
1430         --days;
1431     }
1432 
1433     crm_time_add_days(a_time, days);
1434 }
1435 
1436 void
1437 crm_time_add_days(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1438 {
1439     int lower_bound = 1;
1440     int ydays = crm_time_leapyear(a_time->years) ? 366 : 365;
1441 
1442     crm_trace("Adding %d days to %.4d-%.3d", extra, a_time->years, a_time->days);
1443 
1444     a_time->days += extra;
1445     while (a_time->days > ydays) {
1446         a_time->years++;
1447         a_time->days -= ydays;
1448         ydays = crm_time_leapyear(a_time->years) ? 366 : 365;
1449     }
1450 
1451     if(a_time->duration) {
1452         lower_bound = 0;
1453     }
1454 
1455     while (a_time->days < lower_bound) {
1456         a_time->years--;
1457         a_time->days += crm_time_leapyear(a_time->years) ? 366 : 365;
1458     }
1459 }
1460 
1461 void
1462 crm_time_add_months(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1463 {
1464     int lpc;
1465     uint32_t y, m, d, dmax;
1466 
1467     crm_time_get_gregorian(a_time, &y, &m, &d);
1468     crm_trace("Adding %d months to %.4d-%.2d-%.2d", extra, y, m, d);
1469 
1470     if (extra > 0) {
1471         for (lpc = extra; lpc > 0; lpc--) {
1472             m++;
1473             if (m == 13) {
1474                 m = 1;
1475                 y++;
1476             }
1477         }
1478     } else {
1479         for (lpc = -extra; lpc > 0; lpc--) {
1480             m--;
1481             if (m == 0) {
1482                 m = 12;
1483                 y--;
1484             }
1485         }
1486     }
1487 
1488     dmax = crm_time_days_in_month(m, y);
1489     if (dmax < d) {
1490         /* Preserve day-of-month unless the month doesn't have enough days */
1491         d = dmax;
1492     }
1493 
1494     crm_trace("Calculated %.4d-%.2d-%.2d", y, m, d);
1495 
1496     a_time->years = y;
1497     a_time->days = get_ordinal_days(y, m, d);
1498 
1499     crm_time_get_gregorian(a_time, &y, &m, &d);
1500     crm_trace("Got %.4d-%.2d-%.2d", y, m, d);
1501 }
1502 
1503 void
1504 crm_time_add_minutes(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1505 {
1506     crm_time_add_seconds(a_time, extra * 60);
1507 }
1508 
1509 void
1510 crm_time_add_hours(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1511 {
1512     crm_time_add_seconds(a_time, extra * HOUR_SECONDS);
1513 }
1514 
1515 void
1516 crm_time_add_weeks(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1517 {
1518     crm_time_add_days(a_time, extra * 7);
1519 }
1520 
1521 void
1522 crm_time_add_years(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1523 {
1524     a_time->years += extra;
1525 }
1526 
1527 static void
1528 ha_get_tm_time( struct tm *target, crm_time_t *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1529 {
1530     *target = (struct tm) {
1531         .tm_year = source->years - 1900,
1532         .tm_mday = source->days,
1533         .tm_sec = source->seconds % 60,
1534         .tm_min = ( source->seconds / 60 ) % 60,
1535         .tm_hour = source->seconds / HOUR_SECONDS,
1536         .tm_isdst = -1, /* don't adjust */
1537 
1538 #if defined(HAVE_STRUCT_TM_TM_GMTOFF)
1539         .tm_gmtoff = source->offset
1540 #endif
1541     };
1542     mktime(target);
1543 }
1544 
1545 /* The high-resolution variant of time object was added to meet an immediate
1546  * need, and is kept internal API.
1547  *
1548  * @TODO The long-term goal is to come up with a clean, unified design for a
1549  *       time type (or types) that meets all the various needs, to replace
1550  *       crm_time_t, pcmk__time_hr_t, and struct timespec (in lrmd_cmd_t).
1551  *       Using glib's GDateTime is a possibility (if we are willing to require
1552  *       glib >= 2.26).
1553  */
1554 
1555 pcmk__time_hr_t *
1556 pcmk__time_hr_convert(pcmk__time_hr_t *target, crm_time_t *dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1557 {
1558     pcmk__time_hr_t *hr_dt = NULL;
1559 
1560     if (dt) {
1561         hr_dt = target?target:calloc(1, sizeof(pcmk__time_hr_t));
1562         CRM_ASSERT(hr_dt != NULL);
1563         *hr_dt = (pcmk__time_hr_t) {
1564             .years = dt->years,
1565             .months = dt->months,
1566             .days = dt->days,
1567             .seconds = dt->seconds,
1568             .offset = dt->offset,
1569             .duration = dt->duration
1570         };
1571     }
1572 
1573     return hr_dt;
1574 }
1575 
1576 void
1577 pcmk__time_set_hr_dt(crm_time_t *target, pcmk__time_hr_t *hr_dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1578 {
1579     CRM_ASSERT((hr_dt) && (target));
1580     *target = (crm_time_t) {
1581         .years = hr_dt->years,
1582         .months = hr_dt->months,
1583         .days = hr_dt->days,
1584         .seconds = hr_dt->seconds,
1585         .offset = hr_dt->offset,
1586         .duration = hr_dt->duration
1587     };
1588 }
1589 
1590 pcmk__time_hr_t *
1591 pcmk__time_timeval_hr_convert(pcmk__time_hr_t *target, struct timeval *tv)
     /* [previous][next][first][last][top][bottom][index][help] */
1592 {
1593     crm_time_t dt;
1594     pcmk__time_hr_t *ret;
1595 
1596     crm_time_set_timet(&dt, &tv->tv_sec);
1597     ret = pcmk__time_hr_convert(target, &dt);
1598     if (ret) {
1599         ret->useconds = tv->tv_usec;
1600     }
1601     return ret;
1602 }
1603 
1604 pcmk__time_hr_t *
1605 pcmk__time_hr_new(const char *date_time)
     /* [previous][next][first][last][top][bottom][index][help] */
1606 {
1607     pcmk__time_hr_t *hr_dt = NULL;
1608     struct timeval tv_now;
1609 
1610     if (!date_time) {
1611         if (gettimeofday(&tv_now, NULL) == 0) {
1612             hr_dt = pcmk__time_timeval_hr_convert(NULL, &tv_now);
1613         }
1614     } else {
1615         crm_time_t *dt;
1616 
1617         dt = parse_date(date_time);
1618         hr_dt = pcmk__time_hr_convert(NULL, dt);
1619         crm_time_free(dt);
1620     }
1621     return hr_dt;
1622 }
1623 
1624 void
1625 pcmk__time_hr_free(pcmk__time_hr_t * hr_dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1626 {
1627     free(hr_dt);
1628 }
1629 
1630 char *
1631 pcmk__time_format_hr(const char *format, pcmk__time_hr_t * hr_dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1632 {
1633     const char *mark_s;
1634     int max = 128, scanned_pos = 0, printed_pos = 0, fmt_pos = 0,
1635         date_len = 0, nano_digits = 0;
1636     char nano_s[10], date_s[max+1], nanofmt_s[5] = "%", *tmp_fmt_s;
1637     struct tm tm;
1638     crm_time_t dt;
1639 
1640     if (!format) {
1641         return NULL;
1642     }
1643     pcmk__time_set_hr_dt(&dt, hr_dt);
1644     ha_get_tm_time(&tm, &dt);
1645     sprintf(nano_s, "%06d000", hr_dt->useconds);
1646 
1647     while ((format[scanned_pos]) != '\0') {
1648         mark_s = strchr(&format[scanned_pos], '%');
1649         if (mark_s) {
1650             int fmt_len = 1;
1651 
1652             fmt_pos = mark_s - format;
1653             while ((format[fmt_pos+fmt_len] != '\0') &&
1654                 (format[fmt_pos+fmt_len] >= '0') &&
1655                 (format[fmt_pos+fmt_len] <= '9')) {
1656                 fmt_len++;
1657             }
1658             scanned_pos = fmt_pos + fmt_len + 1;
1659             if (format[fmt_pos+fmt_len] == 'N') {
1660                 nano_digits = atoi(&format[fmt_pos+1]);
1661                 nano_digits = (nano_digits > 6)?6:nano_digits;
1662                 nano_digits = (nano_digits < 0)?0:nano_digits;
1663                 sprintf(&nanofmt_s[1], ".%ds", nano_digits);
1664             } else {
1665                 if (format[scanned_pos] != '\0') {
1666                     continue;
1667                 }
1668                 fmt_pos = scanned_pos; /* print till end */
1669             }
1670         } else {
1671             scanned_pos = strlen(format);
1672             fmt_pos = scanned_pos; /* print till end */
1673         }
1674         tmp_fmt_s = strndup(&format[printed_pos], fmt_pos - printed_pos);
1675 #ifdef GCC_FORMAT_NONLITERAL_CHECKING_ENABLED
1676 #pragma GCC diagnostic push
1677 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
1678 #endif
1679         date_len += strftime(&date_s[date_len], max-date_len, tmp_fmt_s, &tm);
1680 #ifdef GCC_FORMAT_NONLITERAL_CHECKING_ENABLED
1681 #pragma GCC diagnostic pop
1682 #endif
1683         printed_pos = scanned_pos;
1684         free(tmp_fmt_s);
1685         if (nano_digits) {
1686 #ifdef GCC_FORMAT_NONLITERAL_CHECKING_ENABLED
1687 #pragma GCC diagnostic push
1688 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
1689 #endif
1690             date_len += snprintf(&date_s[date_len], max-date_len,
1691                                  nanofmt_s, nano_s);
1692 #ifdef GCC_FORMAT_NONLITERAL_CHECKING_ENABLED
1693 #pragma GCC diagnostic pop
1694 #endif
1695             nano_digits = 0;
1696         }
1697     }
1698 
1699     return (date_len == 0)?NULL:strdup(date_s);
1700 }
1701 
1702 /*!
1703  * \internal
1704  * \brief Return human-friendly string corresponding to a time
1705  *
1706  * \param[in] when   Pointer to epoch time value (or NULL for current time)
1707  *
1708  * \return Current time as string (as by ctime() but without newline) on
1709  *         success, NULL otherwise
1710  * \note The return value points to a statically allocated string which might be
1711  *       overwritten by subsequent calls to any of the C library date and time
1712  *       functions.
1713  */
1714 const char *
1715 pcmk__epoch2str(time_t *when)
     /* [previous][next][first][last][top][bottom][index][help] */
1716 {
1717     char *since_epoch = NULL;
1718 
1719     if (when == NULL) {
1720         time_t a_time = time(NULL);
1721 
1722         if (a_time == (time_t) -1) {
1723             return NULL;
1724         } else {
1725             since_epoch = ctime(&a_time);
1726         }
1727     } else {
1728         since_epoch = ctime(when);
1729     }
1730 
1731     if (since_epoch == NULL) {
1732         return NULL;
1733     } else {
1734         return pcmk__trim(since_epoch);
1735     }
1736 }
1737 
1738 /*!
1739  * \internal
1740  * \brief Given a millisecond interval, return a log-friendly string
1741  *
1742  * \param[in] interval_ms  Interval in milliseconds
1743  *
1744  * \return Readable version of \p interval_ms
1745  *
1746  * \note The return value is a pointer to static memory that will be
1747  *       overwritten by later calls to this function.
1748  */
1749 const char *
1750 pcmk__readable_interval(guint interval_ms)
     /* [previous][next][first][last][top][bottom][index][help] */
1751 {
1752 #define MS_IN_S (1000)
1753 #define MS_IN_M (MS_IN_S * 60)
1754 #define MS_IN_H (MS_IN_M * 60)
1755 #define MS_IN_D (MS_IN_H * 24)
1756 #define MAXSTR sizeof("..d..h..m..s...ms")
1757     static char str[MAXSTR] = { '\0', };
1758     int offset = 0;
1759 
1760     if (interval_ms > MS_IN_D) {
1761         offset += snprintf(str + offset, MAXSTR - offset, "%ud",
1762                            interval_ms / MS_IN_D);
1763         interval_ms -= (interval_ms / MS_IN_D) * MS_IN_D;
1764     }
1765     if (interval_ms > MS_IN_H) {
1766         offset += snprintf(str + offset, MAXSTR - offset, "%uh",
1767                            interval_ms / MS_IN_H);
1768         interval_ms -= (interval_ms / MS_IN_H) * MS_IN_H;
1769     }
1770     if (interval_ms > MS_IN_M) {
1771         offset += snprintf(str + offset, MAXSTR - offset, "%um",
1772                            interval_ms / MS_IN_M);
1773         interval_ms -= (interval_ms / MS_IN_M) * MS_IN_M;
1774     }
1775 
1776     // Ns, N.NNNs, or NNNms
1777     if (interval_ms > MS_IN_S) {
1778         offset += snprintf(str + offset, MAXSTR - offset, "%u",
1779                            interval_ms / MS_IN_S);
1780         interval_ms -= (interval_ms / MS_IN_S) * MS_IN_S;
1781         if (interval_ms > 0) {
1782             offset += snprintf(str + offset, MAXSTR - offset, ".%03u",
1783                                interval_ms);
1784         }
1785         (void) snprintf(str + offset, MAXSTR - offset, "s");
1786 
1787     } else if (interval_ms > 0) {
1788         (void) snprintf(str + offset, MAXSTR - offset, "%ums", interval_ms);
1789 
1790     } else if (str[0] == '\0') {
1791         strcpy(str, "0s");
1792     }
1793     return str;
1794 }

/* [previous][next][first][last][top][bottom][index][help] */