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. sec_usec_as_string
  22. crm_duration_as_string
  23. time_as_string_common
  24. crm_time_as_string
  25. crm_time_parse_sec
  26. crm_time_parse_offset
  27. crm_time_parse
  28. parse_date
  29. parse_int
  30. crm_time_parse_duration
  31. crm_time_parse_period
  32. crm_time_free_period
  33. crm_time_set
  34. ha_set_tm_time
  35. crm_time_set_timet
  36. pcmk__set_time_if_earlier
  37. pcmk_copy_time
  38. pcmk__copy_timet
  39. crm_time_add
  40. pcmk__time_component_attr
  41. component_fn
  42. pcmk__add_time_from_xml
  43. crm_time_calculate_duration
  44. crm_time_subtract
  45. crm_time_check
  46. crm_time_compare
  47. crm_time_add_seconds
  48. crm_time_add_days
  49. crm_time_add_months
  50. crm_time_add_minutes
  51. crm_time_add_hours
  52. crm_time_add_weeks
  53. crm_time_add_years
  54. ha_get_tm_time
  55. pcmk__time_hr_convert
  56. pcmk__time_set_hr_dt
  57. pcmk__time_hr_now
  58. pcmk__time_hr_new
  59. pcmk__time_hr_free
  60. pcmk__time_format_hr
  61. pcmk__epoch2str
  62. pcmk__timespec2str
  63. pcmk__readable_interval

   1 /*
   2  * Copyright 2005-2024 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 <inttypes.h>
  21 #include <limits.h>         // INT_MIN, INT_MAX
  22 #include <string.h>
  23 #include <stdbool.h>
  24 #include <crm/common/iso8601.h>
  25 #include <crm/common/iso8601_internal.h>
  26 #include "crmcommon_private.h"
  27 
  28 /*
  29  * Andrew's code was originally written for OSes whose "struct tm" contains:
  30  *      long tm_gmtoff;         :: Seconds east of UTC
  31  *      const char *tm_zone;    :: Timezone abbreviation
  32  * Some OSes lack these, instead having:
  33  *      time_t (or long) timezone;
  34                 :: "difference between UTC and local standard time"
  35  *      char *tzname[2] = { "...", "..." };
  36  * I (David Lee) confess to not understanding the details.  So my attempted
  37  * generalisations for where their use is necessary may be flawed.
  38  *
  39  * 1. Does "difference between ..." subtract the same or opposite way?
  40  * 2. Should it use "altzone" instead of "timezone"?
  41  * 3. Should it use tzname[0] or tzname[1]?  Interaction with timezone/altzone?
  42  */
  43 #if defined(HAVE_STRUCT_TM_TM_GMTOFF)
  44 #  define GMTOFF(tm) ((tm)->tm_gmtoff)
  45 #else
  46 /* Note: extern variable; macro argument not actually used.  */
  47 #  define GMTOFF(tm) (-timezone+daylight)
  48 #endif
  49 
  50 #define HOUR_SECONDS    (60 * 60)
  51 #define DAY_SECONDS     (HOUR_SECONDS * 24)
  52 
  53 /*!
  54  * \internal
  55  * \brief Validate a seconds/microseconds tuple
  56  *
  57  * The microseconds value must be in the correct range, and if both are nonzero
  58  * they must have the same sign.
  59  *
  60  * \param[in] sec   Seconds
  61  * \param[in] usec  Microseconds
  62  *
  63  * \return true if the seconds/microseconds tuple is valid, or false otherwise
  64  */
  65 #define valid_sec_usec(sec, usec)               \
  66         ((QB_ABS(usec) < QB_TIME_US_IN_SEC)     \
  67          && (((sec) == 0) || ((usec) == 0) || (((sec) < 0) == ((usec) < 0))))
  68 
  69 // A date/time or duration
  70 struct crm_time_s {
  71     int years;      // Calendar year (date/time) or number of years (duration)
  72     int months;     // Number of months (duration only)
  73     int days;       // Ordinal day of year (date/time) or number of days (duration)
  74     int seconds;    // Seconds of day (date/time) or number of seconds (duration)
  75     int offset;     // Seconds offset from UTC (date/time only)
  76     bool duration;  // True if duration
  77 };
  78 
  79 static crm_time_t *parse_date(const char *date_str);
  80 
  81 static crm_time_t *
  82 crm_get_utc_time(const crm_time_t *dt)
     /* [previous][next][first][last][top][bottom][index][help] */
  83 {
  84     crm_time_t *utc = NULL;
  85 
  86     if (dt == NULL) {
  87         errno = EINVAL;
  88         return NULL;
  89     }
  90 
  91     utc = crm_time_new_undefined();
  92     utc->years = dt->years;
  93     utc->days = dt->days;
  94     utc->seconds = dt->seconds;
  95     utc->offset = 0;
  96 
  97     if (dt->offset) {
  98         crm_time_add_seconds(utc, -dt->offset);
  99     } else {
 100         /* Durations (which are the only things that can include months, never have a timezone */
 101         utc->months = dt->months;
 102     }
 103 
 104     crm_time_log(LOG_TRACE, "utc-source", dt,
 105                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
 106     crm_time_log(LOG_TRACE, "utc-target", utc,
 107                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
 108     return utc;
 109 }
 110 
 111 crm_time_t *
 112 crm_time_new(const char *date_time)
     /* [previous][next][first][last][top][bottom][index][help] */
 113 {
 114     tzset();
 115     if (date_time == NULL) {
 116         return pcmk__copy_timet(time(NULL));
 117     }
 118     return parse_date(date_time);
 119 }
 120 
 121 /*!
 122  * \brief Allocate memory for an uninitialized time object
 123  *
 124  * \return Newly allocated time object
 125  * \note The caller is responsible for freeing the return value using
 126  *       crm_time_free().
 127  */
 128 crm_time_t *
 129 crm_time_new_undefined(void)
     /* [previous][next][first][last][top][bottom][index][help] */
 130 {
 131     return (crm_time_t *) pcmk__assert_alloc(1, sizeof(crm_time_t));
 132 }
 133 
 134 /*!
 135  * \brief Check whether a time object has been initialized yet
 136  *
 137  * \param[in] t  Time object to check
 138  *
 139  * \return TRUE if time object has been initialized, FALSE otherwise
 140  */
 141 bool
 142 crm_time_is_defined(const crm_time_t *t)
     /* [previous][next][first][last][top][bottom][index][help] */
 143 {
 144     // Any nonzero member indicates something has been done to t
 145     return (t != NULL) && (t->years || t->months || t->days || t->seconds
 146                            || t->offset || t->duration);
 147 }
 148 
 149 void
 150 crm_time_free(crm_time_t * dt)
     /* [previous][next][first][last][top][bottom][index][help] */
 151 {
 152     if (dt == NULL) {
 153         return;
 154     }
 155     free(dt);
 156 }
 157 
 158 static int
 159 year_days(int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 160 {
 161     int d = 365;
 162 
 163     if (crm_time_leapyear(year)) {
 164         d++;
 165     }
 166     return d;
 167 }
 168 
 169 /* From http://myweb.ecu.edu/mccartyr/ISOwdALG.txt :
 170  *
 171  * 5. Find the Jan1Weekday for Y (Monday=1, Sunday=7)
 172  *  YY = (Y-1) % 100
 173  *  C = (Y-1) - YY
 174  *  G = YY + YY/4
 175  *  Jan1Weekday = 1 + (((((C / 100) % 4) x 5) + G) % 7)
 176  */
 177 int
 178 crm_time_january1_weekday(int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 179 {
 180     int YY = (year - 1) % 100;
 181     int C = (year - 1) - YY;
 182     int G = YY + YY / 4;
 183     int jan1 = 1 + (((((C / 100) % 4) * 5) + G) % 7);
 184 
 185     crm_trace("YY=%d, C=%d, G=%d", YY, C, G);
 186     crm_trace("January 1 %.4d: %d", year, jan1);
 187     return jan1;
 188 }
 189 
 190 int
 191 crm_time_weeks_in_year(int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 192 {
 193     int weeks = 52;
 194     int jan1 = crm_time_january1_weekday(year);
 195 
 196     /* if jan1 == thursday */
 197     if (jan1 == 4) {
 198         weeks++;
 199     } else {
 200         jan1 = crm_time_january1_weekday(year + 1);
 201         /* if dec31 == thursday aka. jan1 of next year is a friday */
 202         if (jan1 == 5) {
 203             weeks++;
 204         }
 205 
 206     }
 207     return weeks;
 208 }
 209 
 210 // Jan-Dec plus Feb of leap years
 211 static int month_days[13] = {
 212     31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29
 213 };
 214 
 215 /*!
 216  * \brief Return number of days in given month of given year
 217  *
 218  * \param[in] month  Ordinal month (1-12)
 219  * \param[in] year   Gregorian year
 220  *
 221  * \return Number of days in given month (0 if given month or year is invalid)
 222  */
 223 int
 224 crm_time_days_in_month(int month, int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 225 {
 226     if ((month < 1) || (month > 12) || (year < 1)) {
 227         return 0;
 228     }
 229     if ((month == 2) && crm_time_leapyear(year)) {
 230         month = 13;
 231     }
 232     return month_days[month - 1];
 233 }
 234 
 235 bool
 236 crm_time_leapyear(int year)
     /* [previous][next][first][last][top][bottom][index][help] */
 237 {
 238     gboolean is_leap = FALSE;
 239 
 240     if (year % 4 == 0) {
 241         is_leap = TRUE;
 242     }
 243     if (year % 100 == 0 && year % 400 != 0) {
 244         is_leap = FALSE;
 245     }
 246     return is_leap;
 247 }
 248 
 249 /*!
 250  * \internal
 251  * \brief Get ordinal day number of year corresponding to given date
 252  *
 253  * \param[in] y   Year
 254  * \param[in] m   Month (1-12)
 255  * \param[in] d   Day of month (1-31)
 256  *
 257  * \return Day number of year \p y corresponding to month \p m and day \p d,
 258  *         or 0 for invalid arguments
 259  */
 260 static int
 261 get_ordinal_days(uint32_t y, uint32_t m, uint32_t d)
     /* [previous][next][first][last][top][bottom][index][help] */
 262 {
 263     int result = 0;
 264 
 265     CRM_CHECK((y > 0) && (y <= INT_MAX) && (m >= 1) && (m <= 12)
 266               && (d >= 1) && (d <= 31), return 0);
 267 
 268     result = d;
 269     for (int lpc = 1; lpc < m; lpc++) {
 270         result += crm_time_days_in_month(lpc, y);
 271     }
 272     return result;
 273 }
 274 
 275 void
 276 crm_time_log_alias(int log_level, const char *file, const char *function,
     /* [previous][next][first][last][top][bottom][index][help] */
 277                    int line, const char *prefix, const crm_time_t *date_time,
 278                    int flags)
 279 {
 280     char *date_s = crm_time_as_string(date_time, flags);
 281 
 282     if (log_level == LOG_STDOUT) {
 283         printf("%s%s%s\n",
 284                (prefix? prefix : ""), (prefix? ": " : ""), date_s);
 285     } else {
 286         do_crm_log_alias(log_level, file, function, line, "%s%s%s",
 287                          (prefix? prefix : ""), (prefix? ": " : ""), date_s);
 288     }
 289     free(date_s);
 290 }
 291 
 292 static void
 293 crm_time_get_sec(int sec, uint32_t *h, uint32_t *m, uint32_t *s)
     /* [previous][next][first][last][top][bottom][index][help] */
 294 {
 295     uint32_t hours, minutes, seconds;
 296 
 297     seconds = QB_ABS(sec);
 298 
 299     hours = seconds / HOUR_SECONDS;
 300     seconds -= HOUR_SECONDS * hours;
 301 
 302     minutes = seconds / 60;
 303     seconds -= 60 * minutes;
 304 
 305     crm_trace("%d == %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32,
 306               sec, hours, minutes, seconds);
 307 
 308     *h = hours;
 309     *m = minutes;
 310     *s = seconds;
 311 }
 312 
 313 int
 314 crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m,
     /* [previous][next][first][last][top][bottom][index][help] */
 315                        uint32_t *s)
 316 {
 317     crm_time_get_sec(dt->seconds, h, m, s);
 318     return TRUE;
 319 }
 320 
 321 int
 322 crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m)
     /* [previous][next][first][last][top][bottom][index][help] */
 323 {
 324     uint32_t s;
 325 
 326     crm_time_get_sec(dt->seconds, h, m, &s);
 327     return TRUE;
 328 }
 329 
 330 long long
 331 crm_time_get_seconds(const crm_time_t *dt)
     /* [previous][next][first][last][top][bottom][index][help] */
 332 {
 333     int lpc;
 334     crm_time_t *utc = NULL;
 335     long long in_seconds = 0;
 336 
 337     if (dt == NULL) {
 338         return 0;
 339     }
 340 
 341     utc = crm_get_utc_time(dt);
 342     if (utc == NULL) {
 343         return 0;
 344     }
 345 
 346     for (lpc = 1; lpc < utc->years; lpc++) {
 347         long long dmax = year_days(lpc);
 348 
 349         in_seconds += DAY_SECONDS * dmax;
 350     }
 351 
 352     /* utc->months is an offset that can only be set for a duration.
 353      * By definition, the value is variable depending on the date to
 354      * which it is applied.
 355      *
 356      * Force 30-day months so that something vaguely sane happens
 357      * for anyone that tries to use a month in this way.
 358      */
 359     if (utc->months > 0) {
 360         in_seconds += DAY_SECONDS * 30 * (long long) (utc->months);
 361     }
 362 
 363     if (utc->days > 0) {
 364         in_seconds += DAY_SECONDS * (long long) (utc->days - 1);
 365     }
 366     in_seconds += utc->seconds;
 367 
 368     crm_time_free(utc);
 369     return in_seconds;
 370 }
 371 
 372 #define EPOCH_SECONDS 62135596800ULL    /* Calculated using crm_time_get_seconds() */
 373 long long
 374 crm_time_get_seconds_since_epoch(const crm_time_t *dt)
     /* [previous][next][first][last][top][bottom][index][help] */
 375 {
 376     return (dt == NULL)? 0 : (crm_time_get_seconds(dt) - EPOCH_SECONDS);
 377 }
 378 
 379 int
 380 crm_time_get_gregorian(const crm_time_t *dt, uint32_t *y, uint32_t *m,
     /* [previous][next][first][last][top][bottom][index][help] */
 381                        uint32_t *d)
 382 {
 383     int months = 0;
 384     int days = dt->days;
 385 
 386     if(dt->years != 0) {
 387         for (months = 1; months <= 12 && days > 0; months++) {
 388             int mdays = crm_time_days_in_month(months, dt->years);
 389 
 390             if (mdays >= days) {
 391                 break;
 392             } else {
 393                 days -= mdays;
 394             }
 395         }
 396 
 397     } else if (dt->months) {
 398         /* This is a duration including months, don't convert the days field */
 399         months = dt->months;
 400 
 401     } else {
 402         /* This is a duration not including months, still don't convert the days field */
 403     }
 404 
 405     *y = dt->years;
 406     *m = months;
 407     *d = days;
 408     crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days);
 409     return TRUE;
 410 }
 411 
 412 int
 413 crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d)
     /* [previous][next][first][last][top][bottom][index][help] */
 414 {
 415     *y = dt->years;
 416     *d = dt->days;
 417     return TRUE;
 418 }
 419 
 420 int
 421 crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w,
     /* [previous][next][first][last][top][bottom][index][help] */
 422                      uint32_t *d)
 423 {
 424     /*
 425      * Monday 29 December 2008 is written "2009-W01-1"
 426      * Sunday 3 January 2010 is written "2009-W53-7"
 427      */
 428     int year_num = 0;
 429     int jan1 = crm_time_january1_weekday(dt->years);
 430     int h = -1;
 431 
 432     CRM_CHECK(dt->days > 0, return FALSE);
 433 
 434 /* 6. Find the Weekday for Y M D */
 435     h = dt->days + jan1 - 1;
 436     *d = 1 + ((h - 1) % 7);
 437 
 438 /* 7. Find if Y M D falls in YearNumber Y-1, WeekNumber 52 or 53 */
 439     if (dt->days <= (8 - jan1) && jan1 > 4) {
 440         crm_trace("year--, jan1=%d", jan1);
 441         year_num = dt->years - 1;
 442         *w = crm_time_weeks_in_year(year_num);
 443 
 444     } else {
 445         year_num = dt->years;
 446     }
 447 
 448 /* 8. Find if Y M D falls in YearNumber Y+1, WeekNumber 1 */
 449     if (year_num == dt->years) {
 450         int dmax = year_days(year_num);
 451         int correction = 4 - *d;
 452 
 453         if ((dmax - dt->days) < correction) {
 454             crm_trace("year++, jan1=%d, i=%d vs. %d", jan1, dmax - dt->days, correction);
 455             year_num = dt->years + 1;
 456             *w = 1;
 457         }
 458     }
 459 
 460 /* 9. Find if Y M D falls in YearNumber Y, WeekNumber 1 through 53 */
 461     if (year_num == dt->years) {
 462         int j = dt->days + (7 - *d) + (jan1 - 1);
 463 
 464         *w = j / 7;
 465         if (jan1 > 4) {
 466             *w -= 1;
 467         }
 468     }
 469 
 470     *y = year_num;
 471     crm_trace("Converted %.4d-%.3d to %.4" PRIu32 "-W%.2" PRIu32 "-%" PRIu32,
 472               dt->years, dt->days, *y, *w, *d);
 473     return TRUE;
 474 }
 475 
 476 #define DATE_MAX 128
 477 
 478 /*!
 479  * \internal
 480  * \brief Print "<seconds>.<microseconds>" to a buffer
 481  *
 482  * \param[in]     sec     Seconds
 483  * \param[in]     usec    Microseconds (must be of same sign as \p sec and of
 484  *                        absolute value less than \p QB_TIME_US_IN_SEC)
 485  * \param[in,out] buf     Result buffer
 486  * \param[in,out] offset  Current offset within \p buf
 487  */
 488 static inline void
 489 sec_usec_as_string(long long sec, int usec, char *buf, size_t *offset)
     /* [previous][next][first][last][top][bottom][index][help] */
 490 {
 491     *offset += snprintf(buf + *offset, DATE_MAX - *offset, "%s%lld.%06d",
 492                         ((sec == 0) && (usec < 0))? "-" : "",
 493                         sec, QB_ABS(usec));
 494 }
 495 
 496 /*!
 497  * \internal
 498  * \brief Get a string representation of a duration
 499  *
 500  * \param[in]  dt         Time object to interpret as a duration
 501  * \param[in]  usec       Microseconds to add to \p dt
 502  * \param[in]  show_usec  Whether to include microseconds in \p result
 503  * \param[out] result     Where to store the result string
 504  */
 505 static void
 506 crm_duration_as_string(const crm_time_t *dt, int usec, bool show_usec,
     /* [previous][next][first][last][top][bottom][index][help] */
 507                        char *result)
 508 {
 509     size_t offset = 0;
 510 
 511     pcmk__assert(valid_sec_usec(dt->seconds, usec));
 512 
 513     if (dt->years) {
 514         offset += snprintf(result + offset, DATE_MAX - offset, "%4d year%s ",
 515                            dt->years, pcmk__plural_s(dt->years));
 516     }
 517     if (dt->months) {
 518         offset += snprintf(result + offset, DATE_MAX - offset, "%2d month%s ",
 519                            dt->months, pcmk__plural_s(dt->months));
 520     }
 521     if (dt->days) {
 522         offset += snprintf(result + offset, DATE_MAX - offset, "%2d day%s ",
 523                            dt->days, pcmk__plural_s(dt->days));
 524     }
 525 
 526     // At least print seconds (and optionally usecs)
 527     if ((offset == 0) || (dt->seconds != 0) || (show_usec && (usec != 0))) {
 528         if (show_usec) {
 529             sec_usec_as_string(dt->seconds, usec, result, &offset);
 530         } else {
 531             offset += snprintf(result + offset, DATE_MAX - offset, "%d",
 532                                dt->seconds);
 533         }
 534         offset += snprintf(result + offset, DATE_MAX - offset, " second%s",
 535                            pcmk__plural_s(dt->seconds));
 536     }
 537 
 538     // More than one minute, so provide a more readable breakdown into units
 539     if (QB_ABS(dt->seconds) >= 60) {
 540         uint32_t h = 0;
 541         uint32_t m = 0;
 542         uint32_t s = 0;
 543         uint32_t u = QB_ABS(usec);
 544         bool print_sec_component = false;
 545 
 546         crm_time_get_sec(dt->seconds, &h, &m, &s);
 547         print_sec_component = ((s != 0) || (show_usec && (u != 0)));
 548 
 549         offset += snprintf(result + offset, DATE_MAX - offset, " (");
 550 
 551         if (h) {
 552             offset += snprintf(result + offset, DATE_MAX - offset,
 553                                "%" PRIu32 " hour%s%s", h, pcmk__plural_s(h),
 554                                ((m != 0) || print_sec_component)? " " : "");
 555         }
 556 
 557         if (m) {
 558             offset += snprintf(result + offset, DATE_MAX - offset,
 559                                "%" PRIu32 " minute%s%s", m, pcmk__plural_s(m),
 560                                print_sec_component? " " : "");
 561         }
 562 
 563         if (print_sec_component) {
 564             if (show_usec) {
 565                 sec_usec_as_string(s, u, result, &offset);
 566             } else {
 567                 offset += snprintf(result + offset, DATE_MAX - offset,
 568                                    "%" PRIu32, s);
 569             }
 570             offset += snprintf(result + offset, DATE_MAX - offset, " second%s",
 571                                pcmk__plural_s(dt->seconds));
 572         }
 573 
 574         offset += snprintf(result + offset, DATE_MAX - offset, ")");
 575     }
 576 }
 577 
 578 /*!
 579  * \internal
 580  * \brief Get a string representation of a time object
 581  *
 582  * \param[in]  dt      Time to convert to string
 583  * \param[in]  usec    Microseconds to add to \p dt
 584  * \param[in]  flags   Group of \p crm_time_* string format options
 585  * \param[out] result  Where to store the result string
 586  *
 587  * \note \p result must be of size \p DATE_MAX or larger.
 588  */
 589 static void
 590 time_as_string_common(const crm_time_t *dt, int usec, uint32_t flags,
     /* [previous][next][first][last][top][bottom][index][help] */
 591                       char *result)
 592 {
 593     crm_time_t *utc = NULL;
 594     size_t offset = 0;
 595 
 596     if (!crm_time_is_defined(dt)) {
 597         strcpy(result, "<undefined time>");
 598         return;
 599     }
 600 
 601     pcmk__assert(valid_sec_usec(dt->seconds, usec));
 602 
 603     /* Simple cases: as duration, seconds, or seconds since epoch.
 604      * These never depend on time zone.
 605      */
 606 
 607     if (pcmk_is_set(flags, crm_time_log_duration)) {
 608         crm_duration_as_string(dt, usec, pcmk_is_set(flags, crm_time_usecs),
 609                                result);
 610         return;
 611     }
 612 
 613     if (pcmk_any_flags_set(flags, crm_time_seconds|crm_time_epoch)) {
 614         long long seconds = 0;
 615 
 616         if (pcmk_is_set(flags, crm_time_seconds)) {
 617             seconds = crm_time_get_seconds(dt);
 618         } else {
 619             seconds = crm_time_get_seconds_since_epoch(dt);
 620         }
 621 
 622         if (pcmk_is_set(flags, crm_time_usecs)) {
 623             sec_usec_as_string(seconds, usec, result, &offset);
 624         } else {
 625             snprintf(result, DATE_MAX, "%lld", seconds);
 626         }
 627         return;
 628     }
 629 
 630     // Convert to UTC if local timezone was not requested
 631     if ((dt->offset != 0) && !pcmk_is_set(flags, crm_time_log_with_timezone)) {
 632         crm_trace("UTC conversion");
 633         utc = crm_get_utc_time(dt);
 634         dt = utc;
 635     }
 636 
 637     // As readable string
 638 
 639     if (pcmk_is_set(flags, crm_time_log_date)) {
 640         if (pcmk_is_set(flags, crm_time_weeks)) { // YYYY-WW-D
 641             uint32_t y = 0;
 642             uint32_t w = 0;
 643             uint32_t d = 0;
 644 
 645             if (crm_time_get_isoweek(dt, &y, &w, &d)) {
 646                 offset += snprintf(result + offset, DATE_MAX - offset,
 647                                    "%" PRIu32 "-W%.2" PRIu32 "-%" PRIu32,
 648                                    y, w, d);
 649             }
 650 
 651         } else if (pcmk_is_set(flags, crm_time_ordinal)) { // YYYY-DDD
 652             uint32_t y = 0;
 653             uint32_t d = 0;
 654 
 655             if (crm_time_get_ordinal(dt, &y, &d)) {
 656                 offset += snprintf(result + offset, DATE_MAX - offset,
 657                                    "%" PRIu32 "-%.3" PRIu32, y, d);
 658             }
 659 
 660         } else { // YYYY-MM-DD
 661             uint32_t y = 0;
 662             uint32_t m = 0;
 663             uint32_t d = 0;
 664 
 665             if (crm_time_get_gregorian(dt, &y, &m, &d)) {
 666                 offset += snprintf(result + offset, DATE_MAX - offset,
 667                                    "%.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32,
 668                                    y, m, d);
 669             }
 670         }
 671     }
 672 
 673     if (pcmk_is_set(flags, crm_time_log_timeofday)) {
 674         uint32_t h = 0, m = 0, s = 0;
 675 
 676         if (offset > 0) {
 677             offset += snprintf(result + offset, DATE_MAX - offset, " ");
 678         }
 679 
 680         if (crm_time_get_timeofday(dt, &h, &m, &s)) {
 681             offset += snprintf(result + offset, DATE_MAX - offset,
 682                                "%.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32,
 683                                h, m, s);
 684 
 685             if (pcmk_is_set(flags, crm_time_usecs)) {
 686                 offset += snprintf(result + offset, DATE_MAX - offset,
 687                                    ".%06" PRIu32, QB_ABS(usec));
 688             }
 689         }
 690 
 691         if (pcmk_is_set(flags, crm_time_log_with_timezone)
 692             && (dt->offset != 0)) {
 693             crm_time_get_sec(dt->offset, &h, &m, &s);
 694             offset += snprintf(result + offset, DATE_MAX - offset,
 695                                " %c%.2" PRIu32 ":%.2" PRIu32,
 696                                ((dt->offset < 0)? '-' : '+'), h, m);
 697         } else {
 698             offset += snprintf(result + offset, DATE_MAX - offset, "Z");
 699         }
 700     }
 701 
 702     crm_time_free(utc);
 703 }
 704 
 705 /*!
 706  * \brief Get a string representation of a \p crm_time_t object
 707  *
 708  * \param[in]  dt      Time to convert to string
 709  * \param[in]  flags   Group of \p crm_time_* string format options
 710  *
 711  * \note The caller is responsible for freeing the return value using \p free().
 712  */
 713 char *
 714 crm_time_as_string(const crm_time_t *dt, int flags)
     /* [previous][next][first][last][top][bottom][index][help] */
 715 {
 716     char result[DATE_MAX] = { '\0', };
 717 
 718     time_as_string_common(dt, 0, flags, result);
 719     return pcmk__str_copy(result);
 720 }
 721 
 722 /*!
 723  * \internal
 724  * \brief Determine number of seconds from an hour:minute:second string
 725  *
 726  * \param[in]  time_str  Time specification string
 727  * \param[out] result    Number of seconds equivalent to time_str
 728  *
 729  * \return TRUE if specification was valid, FALSE (and set errno) otherwise
 730  * \note This may return the number of seconds in a day (which is out of bounds
 731  *       for a time object) if given 24:00:00.
 732  */
 733 static bool
 734 crm_time_parse_sec(const char *time_str, int *result)
     /* [previous][next][first][last][top][bottom][index][help] */
 735 {
 736     int rc;
 737     uint32_t hour = 0;
 738     uint32_t minute = 0;
 739     uint32_t second = 0;
 740 
 741     *result = 0;
 742 
 743     // Must have at least hour, but minutes and seconds are optional
 744     rc = sscanf(time_str, "%" SCNu32 ":%" SCNu32 ":%" SCNu32,
 745                 &hour, &minute, &second);
 746     if (rc == 1) {
 747         rc = sscanf(time_str, "%2" SCNu32 "%2" SCNu32 "%2" SCNu32,
 748                     &hour, &minute, &second);
 749     }
 750     if (rc == 0) {
 751         crm_err("%s is not a valid ISO 8601 time specification", time_str);
 752         errno = EINVAL;
 753         return FALSE;
 754     }
 755 
 756     crm_trace("Got valid time: %.2" PRIu32 ":%.2" PRIu32 ":%.2" PRIu32,
 757               hour, minute, second);
 758 
 759     if ((hour == 24) && (minute == 0) && (second == 0)) {
 760         // Equivalent to 00:00:00 of next day, return number of seconds in day
 761     } else if (hour >= 24) {
 762         crm_err("%s is not a valid ISO 8601 time specification "
 763                 "because %" PRIu32 " is not a valid hour", time_str, hour);
 764         errno = EINVAL;
 765         return FALSE;
 766     }
 767     if (minute >= 60) {
 768         crm_err("%s is not a valid ISO 8601 time specification "
 769                 "because %" PRIu32 " is not a valid minute", time_str, minute);
 770         errno = EINVAL;
 771         return FALSE;
 772     }
 773     if (second >= 60) {
 774         crm_err("%s is not a valid ISO 8601 time specification "
 775                 "because %" PRIu32 " is not a valid second", time_str, second);
 776         errno = EINVAL;
 777         return FALSE;
 778     }
 779 
 780     *result = (hour * HOUR_SECONDS) + (minute * 60) + second;
 781     return TRUE;
 782 }
 783 
 784 static bool
 785 crm_time_parse_offset(const char *offset_str, int *offset)
     /* [previous][next][first][last][top][bottom][index][help] */
 786 {
 787     tzset();
 788 
 789     if (offset_str == NULL) {
 790         // Use local offset
 791 #if defined(HAVE_STRUCT_TM_TM_GMTOFF)
 792         time_t now = time(NULL);
 793         struct tm *now_tm = localtime(&now);
 794 #endif
 795         int h_offset = GMTOFF(now_tm) / HOUR_SECONDS;
 796         int m_offset = (GMTOFF(now_tm) - (HOUR_SECONDS * h_offset)) / 60;
 797 
 798         if (h_offset < 0 && m_offset < 0) {
 799             m_offset = 0 - m_offset;
 800         }
 801         *offset = (HOUR_SECONDS * h_offset) + (60 * m_offset);
 802         return TRUE;
 803     }
 804 
 805     if (offset_str[0] == 'Z') { // @TODO invalid if anything after?
 806         *offset = 0;
 807         return TRUE;
 808     }
 809 
 810     *offset = 0;
 811     if ((offset_str[0] == '+') || (offset_str[0] == '-')
 812         || isdigit((int)offset_str[0])) {
 813 
 814         gboolean negate = FALSE;
 815 
 816         if (offset_str[0] == '+') {
 817             offset_str++;
 818         } else if (offset_str[0] == '-') {
 819             negate = TRUE;
 820             offset_str++;
 821         }
 822         if (crm_time_parse_sec(offset_str, offset) == FALSE) {
 823             return FALSE;
 824         }
 825         if (negate) {
 826             *offset = 0 - *offset;
 827         }
 828     } // @TODO else invalid?
 829     return TRUE;
 830 }
 831 
 832 /*!
 833  * \internal
 834  * \brief Parse the time portion of an ISO 8601 date/time string
 835  *
 836  * \param[in]     time_str  Time portion of specification (after any 'T')
 837  * \param[in,out] a_time    Time object to parse into
 838  *
 839  * \return TRUE if valid time was parsed, FALSE (and set errno) otherwise
 840  * \note This may add a day to a_time (if the time is 24:00:00).
 841  */
 842 static bool
 843 crm_time_parse(const char *time_str, crm_time_t *a_time)
     /* [previous][next][first][last][top][bottom][index][help] */
 844 {
 845     uint32_t h, m, s;
 846     char *offset_s = NULL;
 847 
 848     tzset();
 849 
 850     if (time_str) {
 851         if (crm_time_parse_sec(time_str, &(a_time->seconds)) == FALSE) {
 852             return FALSE;
 853         }
 854         offset_s = strstr(time_str, "Z");
 855         if (offset_s == NULL) {
 856             offset_s = strstr(time_str, " ");
 857             if (offset_s) {
 858                 while (isspace(offset_s[0])) {
 859                     offset_s++;
 860                 }
 861             }
 862         }
 863     }
 864 
 865     if (crm_time_parse_offset(offset_s, &(a_time->offset)) == FALSE) {
 866         return FALSE;
 867     }
 868     crm_time_get_sec(a_time->offset, &h, &m, &s);
 869     crm_trace("Got tz: %c%2." PRIu32 ":%.2" PRIu32,
 870               (a_time->offset < 0)? '-' : '+', h, m);
 871 
 872     if (a_time->seconds == DAY_SECONDS) {
 873         // 24:00:00 == 00:00:00 of next day
 874         a_time->seconds = 0;
 875         crm_time_add_days(a_time, 1);
 876     }
 877     return TRUE;
 878 }
 879 
 880 /*
 881  * \internal
 882  * \brief Parse a time object from an ISO 8601 date/time specification
 883  *
 884  * \param[in] date_str  ISO 8601 date/time specification (or
 885  *                      \c PCMK__VALUE_EPOCH)
 886  *
 887  * \return New time object on success, NULL (and set errno) otherwise
 888  */
 889 static crm_time_t *
 890 parse_date(const char *date_str)
     /* [previous][next][first][last][top][bottom][index][help] */
 891 {
 892     const char *time_s = NULL;
 893     crm_time_t *dt = NULL;
 894 
 895     uint32_t year = 0U;
 896     uint32_t month = 0U;
 897     uint32_t day = 0U;
 898     uint32_t week = 0U;
 899 
 900     int rc = 0;
 901 
 902     if (pcmk__str_empty(date_str)) {
 903         crm_err("No ISO 8601 date/time specification given");
 904         goto invalid;
 905     }
 906 
 907     if ((date_str[0] == 'T')
 908         || ((strlen(date_str) > 2) && (date_str[2] == ':'))) {
 909         /* Just a time supplied - Infer current date */
 910         dt = crm_time_new(NULL);
 911         if (date_str[0] == 'T') {
 912             time_s = date_str + 1;
 913         } else {
 914             time_s = date_str;
 915         }
 916         goto parse_time;
 917     }
 918 
 919     dt = crm_time_new_undefined();
 920 
 921     if ((strncasecmp(PCMK__VALUE_EPOCH, date_str, 5) == 0)
 922         && ((date_str[5] == '\0')
 923             || (date_str[5] == '/')
 924             || isspace(date_str[5]))) {
 925         dt->days = 1;
 926         dt->years = 1970;
 927         crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
 928         return dt;
 929     }
 930 
 931     /* YYYY-MM-DD */
 932     rc = sscanf(date_str, "%" SCNu32 "-%" SCNu32 "-%" SCNu32 "",
 933                 &year, &month, &day);
 934     if (rc == 1) {
 935         /* YYYYMMDD */
 936         rc = sscanf(date_str, "%4" SCNu32 "%2" SCNu32 "%2" SCNu32 "",
 937                     &year, &month, &day);
 938     }
 939     if (rc == 3) {
 940         if ((month < 1U) || (month > 12U)) {
 941             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 942                     "because '%" PRIu32 "' is not a valid month",
 943                     date_str, month);
 944             goto invalid;
 945         } else if ((year < 1U) || (year > INT_MAX)) {
 946             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 947                     "because '%" PRIu32 "' is not a valid year",
 948                     date_str, year);
 949             goto invalid;
 950         } else if ((day < 1) || (day > INT_MAX)
 951                    || (day > crm_time_days_in_month(month, year))) {
 952             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 953                     "because '%" PRIu32 "' is not a valid day of the month",
 954                     date_str, day);
 955             goto invalid;
 956         } else {
 957             dt->years = year;
 958             dt->days = get_ordinal_days(year, month, day);
 959             crm_trace("Parsed Gregorian date '%.4" PRIu32 "-%.3d' "
 960                       "from date string '%s'", year, dt->days, date_str);
 961         }
 962         goto parse_time;
 963     }
 964 
 965     /* YYYY-DDD */
 966     rc = sscanf(date_str, "%" SCNu32 "-%" SCNu32, &year, &day);
 967     if (rc == 2) {
 968         if ((year < 1U) || (year > INT_MAX)) {
 969             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 970                     "because '%" PRIu32 "' is not a valid year",
 971                     date_str, year);
 972             goto invalid;
 973         } else if ((day < 1U) || (day > INT_MAX) || (day > year_days(year))) {
 974             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 975                     "because '%" PRIu32 "' is not a valid day of year %"
 976                     PRIu32 " (1-%d)",
 977                     date_str, day, year, year_days(year));
 978             goto invalid;
 979         }
 980         crm_trace("Parsed ordinal year %d and days %d from date string '%s'",
 981                   year, day, date_str);
 982         dt->days = day;
 983         dt->years = year;
 984         goto parse_time;
 985     }
 986 
 987     /* YYYY-Www-D */
 988     rc = sscanf(date_str, "%" SCNu32 "-W%" SCNu32 "-%" SCNu32,
 989                 &year, &week, &day);
 990     if (rc == 3) {
 991         if ((week < 1U) || (week > crm_time_weeks_in_year(year))) {
 992             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 993                     "because '%" PRIu32 "' is not a valid week of year %"
 994                     PRIu32 " (1-%d)",
 995                     date_str, week, year, crm_time_weeks_in_year(year));
 996             goto invalid;
 997         } else if ((day < 1U) || (day > 7U)) {
 998             crm_err("'%s' is not a valid ISO 8601 date/time specification "
 999                     "because '%" PRIu32 "' is not a valid day of the week",
1000                     date_str, day);
1001             goto invalid;
1002         } else {
1003             /*
1004              * See https://en.wikipedia.org/wiki/ISO_week_date
1005              *
1006              * Monday 29 December 2008 is written "2009-W01-1"
1007              * Sunday 3 January 2010 is written "2009-W53-7"
1008              * Saturday 27 September 2008 is written "2008-W37-6"
1009              *
1010              * If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it
1011              * is in week 1. If 1 January is on a Friday, Saturday or Sunday,
1012              * it is in week 52 or 53 of the previous year.
1013              */
1014             int jan1 = crm_time_january1_weekday(year);
1015 
1016             crm_trace("Parsed year %" PRIu32 " (Jan 1 = %d), week %" PRIu32
1017                       ", and day %" PRIu32 " from date string '%s'",
1018                       year, jan1, week, day, date_str);
1019 
1020             dt->years = year;
1021             crm_time_add_days(dt, (week - 1) * 7);
1022 
1023             if (jan1 <= 4) {
1024                 crm_time_add_days(dt, 1 - jan1);
1025             } else {
1026                 crm_time_add_days(dt, 8 - jan1);
1027             }
1028 
1029             crm_time_add_days(dt, day);
1030         }
1031         goto parse_time;
1032     }
1033 
1034     crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str);
1035     goto invalid;
1036 
1037   parse_time:
1038 
1039     if (time_s == NULL) {
1040         time_s = date_str + strspn(date_str, "0123456789-W");
1041         if ((time_s[0] == ' ') || (time_s[0] == 'T')) {
1042             ++time_s;
1043         } else {
1044             time_s = NULL;
1045         }
1046     }
1047     if ((time_s != NULL) && (crm_time_parse(time_s, dt) == FALSE)) {
1048         goto invalid;
1049     }
1050 
1051     crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
1052     if (crm_time_check(dt) == FALSE) {
1053         crm_err("'%s' is not a valid ISO 8601 date/time specification",
1054                 date_str);
1055         goto invalid;
1056     }
1057     return dt;
1058 
1059 invalid:
1060     crm_time_free(dt);
1061     errno = EINVAL;
1062     return NULL;
1063 }
1064 
1065 // Parse an ISO 8601 numeric value and return number of characters consumed
1066 static int
1067 parse_int(const char *str, int *result)
     /* [previous][next][first][last][top][bottom][index][help] */
1068 {
1069     unsigned int lpc;
1070     int offset = (str[0] == 'T')? 1 : 0;
1071     bool negate = false;
1072 
1073     *result = 0;
1074 
1075     // @TODO This cannot handle combinations of these characters
1076     switch (str[offset]) {
1077         case '.':
1078         case ',':
1079             return 0; // Fractions are not supported
1080 
1081         case '-':
1082             negate = true;
1083             offset++;
1084             break;
1085 
1086         case '+':
1087         case ':':
1088             offset++;
1089             break;
1090 
1091         default:
1092             break;
1093     }
1094 
1095     for (lpc = 0; (lpc < 10) && isdigit(str[offset]); lpc++) {
1096         const int digit = str[offset++] - '0';
1097 
1098         if ((*result * 10LL + digit) > INT_MAX) {
1099             return 0; // Overflow
1100         }
1101         *result = *result * 10 + digit;
1102     }
1103     if (negate) {
1104         *result = 0 - *result;
1105     }
1106     return (lpc > 0)? offset : 0;
1107 }
1108 
1109 /*!
1110  * \brief Parse a time duration from an ISO 8601 duration specification
1111  *
1112  * \param[in] period_s  ISO 8601 duration specification (optionally followed by
1113  *                      whitespace, after which the rest of the string will be
1114  *                      ignored)
1115  *
1116  * \return New time object on success, NULL (and set errno) otherwise
1117  * \note It is the caller's responsibility to return the result using
1118  *       crm_time_free().
1119  */
1120 crm_time_t *
1121 crm_time_parse_duration(const char *period_s)
     /* [previous][next][first][last][top][bottom][index][help] */
1122 {
1123     gboolean is_time = FALSE;
1124     crm_time_t *diff = NULL;
1125 
1126     if (pcmk__str_empty(period_s)) {
1127         crm_err("No ISO 8601 time duration given");
1128         goto invalid;
1129     }
1130     if (period_s[0] != 'P') {
1131         crm_err("'%s' is not a valid ISO 8601 time duration "
1132                 "because it does not start with a 'P'", period_s);
1133         goto invalid;
1134     }
1135     if ((period_s[1] == '\0') || isspace(period_s[1])) {
1136         crm_err("'%s' is not a valid ISO 8601 time duration "
1137                 "because nothing follows 'P'", period_s);
1138         goto invalid;
1139     }
1140 
1141     diff = crm_time_new_undefined();
1142 
1143     for (const char *current = period_s + 1;
1144          current[0] && (current[0] != '/') && !isspace(current[0]);
1145          ++current) {
1146 
1147         int an_int = 0, rc;
1148         long long result = 0LL;
1149 
1150         if (current[0] == 'T') {
1151             /* A 'T' separates year/month/day from hour/minute/seconds. We don't
1152              * require it strictly, but just use it to differentiate month from
1153              * minutes.
1154              */
1155             is_time = TRUE;
1156             continue;
1157         }
1158 
1159         // An integer must be next
1160         rc = parse_int(current, &an_int);
1161         if (rc == 0) {
1162             crm_err("'%s' is not a valid ISO 8601 time duration "
1163                     "because no valid integer at '%s'", period_s, current);
1164             goto invalid;
1165         }
1166         current += rc;
1167 
1168         // A time unit must be next (we're not strict about the order)
1169         switch (current[0]) {
1170             case 'Y':
1171                 diff->years = an_int;
1172                 break;
1173 
1174             case 'M':
1175                 if (!is_time) { // Months
1176                     diff->months = an_int;
1177                 } else { // Minutes
1178                     result = diff->seconds + an_int * 60LL;
1179                     if ((result < INT_MIN) || (result > INT_MAX)) {
1180                         crm_err("'%s' is not a valid ISO 8601 time duration "
1181                                 "because integer at '%s' is too %s",
1182                                 period_s, current - rc,
1183                                 ((result > 0)? "large" : "small"));
1184                         goto invalid;
1185                     } else {
1186                         diff->seconds = (int) result;
1187                     }
1188                 }
1189 
1190                 break;
1191 
1192             case 'W':
1193                 result = diff->days + an_int * 7LL;
1194                 if ((result < INT_MIN) || (result > INT_MAX)) {
1195                     crm_err("'%s' is not a valid ISO 8601 time duration "
1196                             "because integer at '%s' is too %s",
1197                             period_s, current - rc,
1198                             ((result > 0)? "large" : "small"));
1199                     goto invalid;
1200                 } else {
1201                     diff->days = (int) result;
1202                 }
1203                 break;
1204 
1205             case 'D':
1206                 result = diff->days + (long long) an_int;
1207                 if ((result < INT_MIN) || (result > INT_MAX)) {
1208                     crm_err("'%s' is not a valid ISO 8601 time duration "
1209                             "because integer at '%s' is too %s",
1210                             period_s, current - rc,
1211                             ((result > 0)? "large" : "small"));
1212                     goto invalid;
1213                 } else {
1214                     diff->days = (int) result;
1215                 }
1216                 break;
1217 
1218             case 'H':
1219                 result = diff->seconds + (long long) an_int * HOUR_SECONDS;
1220                 if ((result < INT_MIN) || (result > INT_MAX)) {
1221                     crm_err("'%s' is not a valid ISO 8601 time duration "
1222                             "because integer at '%s' is too %s",
1223                             period_s, current - rc,
1224                             ((result > 0)? "large" : "small"));
1225                     goto invalid;
1226                 } else {
1227                     diff->seconds = (int) result;
1228                 }
1229                 break;
1230 
1231             case 'S':
1232                 result = diff->seconds + (long long) an_int;
1233                 if ((result < INT_MIN) || (result > INT_MAX)) {
1234                     crm_err("'%s' is not a valid ISO 8601 time duration "
1235                             "because integer at '%s' is too %s",
1236                             period_s, current - rc,
1237                             ((result > 0)? "large" : "small"));
1238                     goto invalid;
1239                 } else {
1240                     diff->seconds = (int) result;
1241                 }
1242                 break;
1243 
1244             case '\0':
1245                 crm_err("'%s' is not a valid ISO 8601 time duration "
1246                         "because no units after %d", period_s, an_int);
1247                 goto invalid;
1248 
1249             default:
1250                 crm_err("'%s' is not a valid ISO 8601 time duration "
1251                         "because '%c' is not a valid time unit",
1252                         period_s, current[0]);
1253                 goto invalid;
1254         }
1255     }
1256 
1257     if (!crm_time_is_defined(diff)) {
1258         crm_err("'%s' is not a valid ISO 8601 time duration "
1259                 "because no amounts and units given", period_s);
1260         goto invalid;
1261     }
1262 
1263     diff->duration = TRUE;
1264     return diff;
1265 
1266 invalid:
1267     crm_time_free(diff);
1268     errno = EINVAL;
1269     return NULL;
1270 }
1271 
1272 /*!
1273  * \brief Parse a time period from an ISO 8601 interval specification
1274  *
1275  * \param[in] period_str  ISO 8601 interval specification (start/end,
1276  *                        start/duration, or duration/end)
1277  *
1278  * \return New time period object on success, NULL (and set errno) otherwise
1279  * \note The caller is responsible for freeing the result using
1280  *       crm_time_free_period().
1281  */
1282 crm_time_period_t *
1283 crm_time_parse_period(const char *period_str)
     /* [previous][next][first][last][top][bottom][index][help] */
1284 {
1285     const char *original = period_str;
1286     crm_time_period_t *period = NULL;
1287 
1288     if (pcmk__str_empty(period_str)) {
1289         crm_err("No ISO 8601 time period given");
1290         goto invalid;
1291     }
1292 
1293     tzset();
1294     period = pcmk__assert_alloc(1, sizeof(crm_time_period_t));
1295 
1296     if (period_str[0] == 'P') {
1297         period->diff = crm_time_parse_duration(period_str);
1298         if (period->diff == NULL) {
1299             goto error;
1300         }
1301     } else {
1302         period->start = parse_date(period_str);
1303         if (period->start == NULL) {
1304             goto error;
1305         }
1306     }
1307 
1308     period_str = strstr(original, "/");
1309     if (period_str) {
1310         ++period_str;
1311         if (period_str[0] == 'P') {
1312             if (period->diff != NULL) {
1313                 crm_err("'%s' is not a valid ISO 8601 time period "
1314                         "because it has two durations",
1315                         original);
1316                 goto invalid;
1317             }
1318             period->diff = crm_time_parse_duration(period_str);
1319             if (period->diff == NULL) {
1320                 goto error;
1321             }
1322         } else {
1323             period->end = parse_date(period_str);
1324             if (period->end == NULL) {
1325                 goto error;
1326             }
1327         }
1328 
1329     } else if (period->diff != NULL) {
1330         // Only duration given, assume start is now
1331         period->start = crm_time_new(NULL);
1332 
1333     } else {
1334         // Only start given
1335         crm_err("'%s' is not a valid ISO 8601 time period "
1336                 "because it has no duration or ending time",
1337                 original);
1338         goto invalid;
1339     }
1340 
1341     if (period->start == NULL) {
1342         period->start = crm_time_subtract(period->end, period->diff);
1343 
1344     } else if (period->end == NULL) {
1345         period->end = crm_time_add(period->start, period->diff);
1346     }
1347 
1348     if (crm_time_check(period->start) == FALSE) {
1349         crm_err("'%s' is not a valid ISO 8601 time period "
1350                 "because the start is invalid", period_str);
1351         goto invalid;
1352     }
1353     if (crm_time_check(period->end) == FALSE) {
1354         crm_err("'%s' is not a valid ISO 8601 time period "
1355                 "because the end is invalid", period_str);
1356         goto invalid;
1357     }
1358     return period;
1359 
1360 invalid:
1361     errno = EINVAL;
1362 error:
1363     crm_time_free_period(period);
1364     return NULL;
1365 }
1366 
1367 /*!
1368  * \brief Free a dynamically allocated time period object
1369  *
1370  * \param[in,out] period  Time period to free
1371  */
1372 void
1373 crm_time_free_period(crm_time_period_t *period)
     /* [previous][next][first][last][top][bottom][index][help] */
1374 {
1375     if (period) {
1376         crm_time_free(period->start);
1377         crm_time_free(period->end);
1378         crm_time_free(period->diff);
1379         free(period);
1380     }
1381 }
1382 
1383 void
1384 crm_time_set(crm_time_t *target, const crm_time_t *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1385 {
1386     crm_trace("target=%p, source=%p", target, source);
1387 
1388     CRM_CHECK(target != NULL && source != NULL, return);
1389 
1390     target->years = source->years;
1391     target->days = source->days;
1392     target->months = source->months;    /* Only for durations */
1393     target->seconds = source->seconds;
1394     target->offset = source->offset;
1395 
1396     crm_time_log(LOG_TRACE, "source", source,
1397                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
1398     crm_time_log(LOG_TRACE, "target", target,
1399                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
1400 }
1401 
1402 static void
1403 ha_set_tm_time(crm_time_t *target, const struct tm *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1404 {
1405     int h_offset = 0;
1406     int m_offset = 0;
1407 
1408     /* Ensure target is fully initialized */
1409     target->years = 0;
1410     target->months = 0;
1411     target->days = 0;
1412     target->seconds = 0;
1413     target->offset = 0;
1414     target->duration = FALSE;
1415 
1416     if (source->tm_year > 0) {
1417         /* years since 1900 */
1418         target->years = 1900;
1419         crm_time_add_years(target, source->tm_year);
1420     }
1421 
1422     if (source->tm_yday >= 0) {
1423         /* days since January 1 [0-365] */
1424         target->days = 1 + source->tm_yday;
1425     }
1426 
1427     if (source->tm_hour >= 0) {
1428         target->seconds += HOUR_SECONDS * source->tm_hour;
1429     }
1430     if (source->tm_min >= 0) {
1431         target->seconds += 60 * source->tm_min;
1432     }
1433     if (source->tm_sec >= 0) {
1434         target->seconds += source->tm_sec;
1435     }
1436 
1437     /* tm_gmtoff == offset from UTC in seconds */
1438     h_offset = GMTOFF(source) / HOUR_SECONDS;
1439     m_offset = (GMTOFF(source) - (HOUR_SECONDS * h_offset)) / 60;
1440     crm_trace("Time offset is %lds (%.2d:%.2d)",
1441               GMTOFF(source), h_offset, m_offset);
1442 
1443     target->offset += HOUR_SECONDS * h_offset;
1444     target->offset += 60 * m_offset;
1445 }
1446 
1447 void
1448 crm_time_set_timet(crm_time_t *target, const time_t *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1449 {
1450     ha_set_tm_time(target, localtime(source));
1451 }
1452 
1453 /*!
1454  * \internal
1455  * \brief Set one time object to another if the other is earlier
1456  *
1457  * \param[in,out] target  Time object to set
1458  * \param[in]     source  Time object to use if earlier
1459  */
1460 void
1461 pcmk__set_time_if_earlier(crm_time_t *target, const crm_time_t *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1462 {
1463     if ((target != NULL) && (source != NULL)
1464         && (!crm_time_is_defined(target)
1465             || (crm_time_compare(source, target) < 0))) {
1466         crm_time_set(target, source);
1467     }
1468 }
1469 
1470 crm_time_t *
1471 pcmk_copy_time(const crm_time_t *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1472 {
1473     crm_time_t *target = crm_time_new_undefined();
1474 
1475     crm_time_set(target, source);
1476     return target;
1477 }
1478 
1479 /*!
1480  * \internal
1481  * \brief Convert a \p time_t time to a \p crm_time_t time
1482  *
1483  * \param[in] source  Time to convert
1484  *
1485  * \return A \p crm_time_t object representing \p source
1486  */
1487 crm_time_t *
1488 pcmk__copy_timet(time_t source)
     /* [previous][next][first][last][top][bottom][index][help] */
1489 {
1490     crm_time_t *target = crm_time_new_undefined();
1491 
1492     crm_time_set_timet(target, &source);
1493     return target;
1494 }
1495 
1496 crm_time_t *
1497 crm_time_add(const crm_time_t *dt, const crm_time_t *value)
     /* [previous][next][first][last][top][bottom][index][help] */
1498 {
1499     crm_time_t *utc = NULL;
1500     crm_time_t *answer = NULL;
1501 
1502     if ((dt == NULL) || (value == NULL)) {
1503         errno = EINVAL;
1504         return NULL;
1505     }
1506 
1507     answer = pcmk_copy_time(dt);
1508 
1509     utc = crm_get_utc_time(value);
1510     if (utc == NULL) {
1511         crm_time_free(answer);
1512         return NULL;
1513     }
1514 
1515     crm_time_add_years(answer, utc->years);
1516     crm_time_add_months(answer, utc->months);
1517     crm_time_add_days(answer, utc->days);
1518     crm_time_add_seconds(answer, utc->seconds);
1519 
1520     crm_time_free(utc);
1521     return answer;
1522 }
1523 
1524 /*!
1525  * \internal
1526  * \brief Return the XML attribute name corresponding to a time component
1527  *
1528  * \param[in] component  Component to check
1529  *
1530  * \return XML attribute name corresponding to \p component, or NULL if
1531  *         \p component is invalid
1532  */
1533 const char *
1534 pcmk__time_component_attr(enum pcmk__time_component component)
     /* [previous][next][first][last][top][bottom][index][help] */
1535 {
1536     switch (component) {
1537         case pcmk__time_years:
1538             return PCMK_XA_YEARS;
1539 
1540         case pcmk__time_months:
1541             return PCMK_XA_MONTHS;
1542 
1543         case pcmk__time_weeks:
1544             return PCMK_XA_WEEKS;
1545 
1546         case pcmk__time_days:
1547             return PCMK_XA_DAYS;
1548 
1549         case pcmk__time_hours:
1550             return PCMK_XA_HOURS;
1551 
1552         case pcmk__time_minutes:
1553             return PCMK_XA_MINUTES;
1554 
1555         case pcmk__time_seconds:
1556             return PCMK_XA_SECONDS;
1557 
1558         default:
1559             return NULL;
1560     }
1561 }
1562 
1563 typedef void (*component_fn_t)(crm_time_t *, int);
1564 
1565 /*!
1566  * \internal
1567  * \brief Get the addition function corresponding to a time component
1568  * \param[in] component  Component to check
1569  *
1570  * \return Addition function corresponding to \p component, or NULL if
1571  *         \p component is invalid
1572  */
1573 static component_fn_t
1574 component_fn(enum pcmk__time_component component)
     /* [previous][next][first][last][top][bottom][index][help] */
1575 {
1576     switch (component) {
1577         case pcmk__time_years:
1578             return crm_time_add_years;
1579 
1580         case pcmk__time_months:
1581             return crm_time_add_months;
1582 
1583         case pcmk__time_weeks:
1584             return crm_time_add_weeks;
1585 
1586         case pcmk__time_days:
1587             return crm_time_add_days;
1588 
1589         case pcmk__time_hours:
1590             return crm_time_add_hours;
1591 
1592         case pcmk__time_minutes:
1593             return crm_time_add_minutes;
1594 
1595         case pcmk__time_seconds:
1596             return crm_time_add_seconds;
1597 
1598         default:
1599             return NULL;
1600     }
1601 
1602 }
1603 
1604 /*!
1605  * \internal
1606  * \brief Add the value of an XML attribute to a time object
1607  *
1608  * \param[in,out] t          Time object to add to
1609  * \param[in]     component  Component of \p t to add to
1610  * \param[in]     xml        XML with value to add
1611  *
1612  * \return Standard Pacemaker return code
1613  */
1614 int
1615 pcmk__add_time_from_xml(crm_time_t *t, enum pcmk__time_component component,
     /* [previous][next][first][last][top][bottom][index][help] */
1616                         const xmlNode *xml)
1617 {
1618     long long value;
1619     const char *attr = pcmk__time_component_attr(component);
1620     component_fn_t add = component_fn(component);
1621 
1622     if ((t == NULL) || (attr == NULL) || (add == NULL)) {
1623         return EINVAL;
1624     }
1625 
1626     if (xml == NULL) {
1627         return pcmk_rc_ok;
1628     }
1629 
1630     if (pcmk__scan_ll(crm_element_value(xml, attr), &value,
1631                       0LL) != pcmk_rc_ok) {
1632         return pcmk_rc_unpack_error;
1633     }
1634 
1635     if ((value < INT_MIN) || (value > INT_MAX)) {
1636         return ERANGE;
1637     }
1638 
1639     if (value != 0LL) {
1640         add(t, (int) value);
1641     }
1642     return pcmk_rc_ok;
1643 }
1644 
1645 crm_time_t *
1646 crm_time_calculate_duration(const crm_time_t *dt, const crm_time_t *value)
     /* [previous][next][first][last][top][bottom][index][help] */
1647 {
1648     crm_time_t *utc = NULL;
1649     crm_time_t *answer = NULL;
1650 
1651     if ((dt == NULL) || (value == NULL)) {
1652         errno = EINVAL;
1653         return NULL;
1654     }
1655 
1656     utc = crm_get_utc_time(value);
1657     if (utc == NULL) {
1658         return NULL;
1659     }
1660 
1661     answer = crm_get_utc_time(dt);
1662     if (answer == NULL) {
1663         crm_time_free(utc);
1664         return NULL;
1665     }
1666     answer->duration = TRUE;
1667 
1668     crm_time_add_years(answer, -utc->years);
1669     if(utc->months != 0) {
1670         crm_time_add_months(answer, -utc->months);
1671     }
1672     crm_time_add_days(answer, -utc->days);
1673     crm_time_add_seconds(answer, -utc->seconds);
1674 
1675     crm_time_free(utc);
1676     return answer;
1677 }
1678 
1679 crm_time_t *
1680 crm_time_subtract(const crm_time_t *dt, const crm_time_t *value)
     /* [previous][next][first][last][top][bottom][index][help] */
1681 {
1682     crm_time_t *utc = NULL;
1683     crm_time_t *answer = NULL;
1684 
1685     if ((dt == NULL) || (value == NULL)) {
1686         errno = EINVAL;
1687         return NULL;
1688     }
1689 
1690     utc = crm_get_utc_time(value);
1691     if (utc == NULL) {
1692         return NULL;
1693     }
1694 
1695     answer = pcmk_copy_time(dt);
1696     crm_time_add_years(answer, -utc->years);
1697     if(utc->months != 0) {
1698         crm_time_add_months(answer, -utc->months);
1699     }
1700     crm_time_add_days(answer, -utc->days);
1701     crm_time_add_seconds(answer, -utc->seconds);
1702     crm_time_free(utc);
1703 
1704     return answer;
1705 }
1706 
1707 /*!
1708  * \brief Check whether a time object represents a sensible date/time
1709  *
1710  * \param[in] dt  Date/time object to check
1711  *
1712  * \return \c true if years, days, and seconds are sensible, \c false otherwise
1713  */
1714 bool
1715 crm_time_check(const crm_time_t *dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1716 {
1717     return (dt != NULL)
1718            && (dt->days > 0) && (dt->days <= year_days(dt->years))
1719            && (dt->seconds >= 0) && (dt->seconds < DAY_SECONDS);
1720 }
1721 
1722 #define do_cmp_field(l, r, field)                                       \
1723     if(rc == 0) {                                                       \
1724                 if(l->field > r->field) {                               \
1725                         crm_trace("%s: %d > %d",                        \
1726                                     #field, l->field, r->field);        \
1727                         rc = 1;                                         \
1728                 } else if(l->field < r->field) {                        \
1729                         crm_trace("%s: %d < %d",                        \
1730                                     #field, l->field, r->field);        \
1731                         rc = -1;                                        \
1732                 }                                                       \
1733     }
1734 
1735 int
1736 crm_time_compare(const crm_time_t *a, const crm_time_t *b)
     /* [previous][next][first][last][top][bottom][index][help] */
1737 {
1738     int rc = 0;
1739     crm_time_t *t1 = crm_get_utc_time(a);
1740     crm_time_t *t2 = crm_get_utc_time(b);
1741 
1742     if ((t1 == NULL) && (t2 == NULL)) {
1743         rc = 0;
1744     } else if (t1 == NULL) {
1745         rc = -1;
1746     } else if (t2 == NULL) {
1747         rc = 1;
1748     } else {
1749         do_cmp_field(t1, t2, years);
1750         do_cmp_field(t1, t2, days);
1751         do_cmp_field(t1, t2, seconds);
1752     }
1753 
1754     crm_time_free(t1);
1755     crm_time_free(t2);
1756     return rc;
1757 }
1758 
1759 /*!
1760  * \brief Add a given number of seconds to a date/time or duration
1761  *
1762  * \param[in,out] a_time  Date/time or duration to add seconds to
1763  * \param[in]     extra   Number of seconds to add
1764  */
1765 void
1766 crm_time_add_seconds(crm_time_t *a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1767 {
1768     int days = extra / DAY_SECONDS;
1769 
1770     pcmk__assert(a_time != NULL);
1771 
1772     crm_trace("Adding %d seconds (including %d whole day%s) to %d",
1773               extra, days, pcmk__plural_s(days), a_time->seconds);
1774 
1775     a_time->seconds += extra % DAY_SECONDS;
1776 
1777     // Check whether the addition crossed a day boundary
1778     if (a_time->seconds > DAY_SECONDS) {
1779         ++days;
1780         a_time->seconds -= DAY_SECONDS;
1781 
1782     } else if (a_time->seconds < 0) {
1783         --days;
1784         a_time->seconds += DAY_SECONDS;
1785     }
1786 
1787     crm_time_add_days(a_time, days);
1788 }
1789 
1790 #define ydays(t) (crm_time_leapyear((t)->years)? 366 : 365)
1791 
1792 /*!
1793  * \brief Add days to a date/time
1794  *
1795  * \param[in,out] a_time  Time to modify
1796  * \param[in]     extra   Number of days to add (may be negative to subtract)
1797  */
1798 void
1799 crm_time_add_days(crm_time_t *a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1800 {
1801     pcmk__assert(a_time != NULL);
1802 
1803     crm_trace("Adding %d days to %.4d-%.3d", extra, a_time->years, a_time->days);
1804 
1805     if (extra > 0) {
1806         while ((a_time->days + (long long) extra) > ydays(a_time)) {
1807             if ((a_time->years + 1LL) > INT_MAX) {
1808                 a_time->days = ydays(a_time); // Clip to latest we can handle
1809                 return;
1810             }
1811             extra -= ydays(a_time);
1812             a_time->years++;
1813         }
1814     } else if (extra < 0) {
1815         const int min_days = a_time->duration? 0 : 1;
1816 
1817         while ((a_time->days + (long long) extra) < min_days) {
1818             if ((a_time->years - 1) < 1) {
1819                 a_time->days = 1; // Clip to earliest we can handle (no BCE)
1820                 return;
1821             }
1822             a_time->years--;
1823             extra += ydays(a_time);
1824         }
1825     }
1826     a_time->days += extra;
1827 }
1828 
1829 void
1830 crm_time_add_months(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1831 {
1832     int lpc;
1833     uint32_t y, m, d, dmax;
1834 
1835     crm_time_get_gregorian(a_time, &y, &m, &d);
1836     crm_trace("Adding %d months to %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32,
1837               extra, y, m, d);
1838 
1839     if (extra > 0) {
1840         for (lpc = extra; lpc > 0; lpc--) {
1841             m++;
1842             if (m == 13) {
1843                 m = 1;
1844                 y++;
1845             }
1846         }
1847     } else {
1848         for (lpc = -extra; lpc > 0; lpc--) {
1849             m--;
1850             if (m == 0) {
1851                 m = 12;
1852                 y--;
1853             }
1854         }
1855     }
1856 
1857     dmax = crm_time_days_in_month(m, y);
1858     if (dmax < d) {
1859         /* Preserve day-of-month unless the month doesn't have enough days */
1860         d = dmax;
1861     }
1862 
1863     crm_trace("Calculated %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d);
1864 
1865     a_time->years = y;
1866     a_time->days = get_ordinal_days(y, m, d);
1867 
1868     crm_time_get_gregorian(a_time, &y, &m, &d);
1869     crm_trace("Got %.4" PRIu32 "-%.2" PRIu32 "-%.2" PRIu32, y, m, d);
1870 }
1871 
1872 void
1873 crm_time_add_minutes(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1874 {
1875     crm_time_add_seconds(a_time, extra * 60);
1876 }
1877 
1878 void
1879 crm_time_add_hours(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1880 {
1881     crm_time_add_seconds(a_time, extra * HOUR_SECONDS);
1882 }
1883 
1884 void
1885 crm_time_add_weeks(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1886 {
1887     crm_time_add_days(a_time, extra * 7);
1888 }
1889 
1890 void
1891 crm_time_add_years(crm_time_t * a_time, int extra)
     /* [previous][next][first][last][top][bottom][index][help] */
1892 {
1893     pcmk__assert(a_time != NULL);
1894 
1895     if ((extra > 0) && ((a_time->years + (long long) extra) > INT_MAX)) {
1896         a_time->years = INT_MAX;
1897     } else if ((extra < 0) && ((a_time->years + (long long) extra) < 1)) {
1898         a_time->years = 1; // Clip to earliest we can handle (no BCE)
1899     } else {
1900         a_time->years += extra;
1901     }
1902 }
1903 
1904 static void
1905 ha_get_tm_time(struct tm *target, const crm_time_t *source)
     /* [previous][next][first][last][top][bottom][index][help] */
1906 {
1907     *target = (struct tm) {
1908         .tm_year = source->years - 1900,
1909         .tm_mday = source->days,
1910         .tm_sec = source->seconds % 60,
1911         .tm_min = ( source->seconds / 60 ) % 60,
1912         .tm_hour = source->seconds / HOUR_SECONDS,
1913         .tm_isdst = -1, /* don't adjust */
1914 
1915 #if defined(HAVE_STRUCT_TM_TM_GMTOFF)
1916         .tm_gmtoff = source->offset
1917 #endif
1918     };
1919     mktime(target);
1920 }
1921 
1922 /* The high-resolution variant of time object was added to meet an immediate
1923  * need, and is kept internal API.
1924  *
1925  * @TODO The long-term goal is to come up with a clean, unified design for a
1926  *       time type (or types) that meets all the various needs, to replace
1927  *       crm_time_t, pcmk__time_hr_t, and struct timespec (in lrmd_cmd_t).
1928  */
1929 
1930 pcmk__time_hr_t *
1931 pcmk__time_hr_convert(pcmk__time_hr_t *target, const crm_time_t *dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1932 {
1933     pcmk__time_hr_t *hr_dt = NULL;
1934 
1935     if (dt) {
1936         hr_dt = target;
1937         if (hr_dt == NULL) {
1938             hr_dt = pcmk__assert_alloc(1, sizeof(pcmk__time_hr_t));
1939         }
1940 
1941         *hr_dt = (pcmk__time_hr_t) {
1942             .years = dt->years,
1943             .months = dt->months,
1944             .days = dt->days,
1945             .seconds = dt->seconds,
1946             .offset = dt->offset,
1947             .duration = dt->duration
1948         };
1949     }
1950 
1951     return hr_dt;
1952 }
1953 
1954 void
1955 pcmk__time_set_hr_dt(crm_time_t *target, const pcmk__time_hr_t *hr_dt)
     /* [previous][next][first][last][top][bottom][index][help] */
1956 {
1957     pcmk__assert((target != NULL) && (hr_dt != NULL));
1958     *target = (crm_time_t) {
1959         .years = hr_dt->years,
1960         .months = hr_dt->months,
1961         .days = hr_dt->days,
1962         .seconds = hr_dt->seconds,
1963         .offset = hr_dt->offset,
1964         .duration = hr_dt->duration
1965     };
1966 }
1967 
1968 /*!
1969  * \internal
1970  * \brief Return the current time as a high-resolution time
1971  *
1972  * \param[out] epoch  If not NULL, this will be set to seconds since epoch
1973  *
1974  * \return Newly allocated high-resolution time set to the current time
1975  */
1976 pcmk__time_hr_t *
1977 pcmk__time_hr_now(time_t *epoch)
     /* [previous][next][first][last][top][bottom][index][help] */
1978 {
1979     struct timespec tv;
1980     crm_time_t dt;
1981     pcmk__time_hr_t *hr;
1982 
1983     qb_util_timespec_from_epoch_get(&tv);
1984     if (epoch != NULL) {
1985         *epoch = tv.tv_sec;
1986     }
1987     crm_time_set_timet(&dt, &(tv.tv_sec));
1988     hr = pcmk__time_hr_convert(NULL, &dt);
1989     if (hr != NULL) {
1990         hr->useconds = tv.tv_nsec / QB_TIME_NS_IN_USEC;
1991     }
1992     return hr;
1993 }
1994 
1995 pcmk__time_hr_t *
1996 pcmk__time_hr_new(const char *date_time)
     /* [previous][next][first][last][top][bottom][index][help] */
1997 {
1998     pcmk__time_hr_t *hr_dt = NULL;
1999 
2000     if (date_time == NULL) {
2001         hr_dt = pcmk__time_hr_now(NULL);
2002     } else {
2003         crm_time_t *dt;
2004 
2005         dt = parse_date(date_time);
2006         hr_dt = pcmk__time_hr_convert(NULL, dt);
2007         crm_time_free(dt);
2008     }
2009     return hr_dt;
2010 }
2011 
2012 void
2013 pcmk__time_hr_free(pcmk__time_hr_t * hr_dt)
     /* [previous][next][first][last][top][bottom][index][help] */
2014 {
2015     free(hr_dt);
2016 }
2017 
2018 /*!
2019  * \internal
2020  * \brief Expand a date/time format string, including %N for nanoseconds
2021  *
2022  * \param[in] format  Date/time format string as per strftime(3) with the
2023  *                    addition of %N for nanoseconds
2024  * \param[in] hr_dt   Time value to format
2025  *
2026  * \return Newly allocated string with formatted string
2027  */
2028 char *
2029 pcmk__time_format_hr(const char *format, const pcmk__time_hr_t *hr_dt)
     /* [previous][next][first][last][top][bottom][index][help] */
2030 {
2031     int scanned_pos = 0; // How many characters of format have been parsed
2032     int printed_pos = 0; // How many characters of format have been processed
2033     size_t date_len = 0;
2034 
2035     char nano_s[10] = { '\0', };
2036     char date_s[128] = { '\0', };
2037 
2038     struct tm tm = { 0, };
2039     crm_time_t dt = { 0, };
2040 
2041     if (format == NULL) {
2042         return NULL;
2043     }
2044     pcmk__time_set_hr_dt(&dt, hr_dt);
2045     ha_get_tm_time(&tm, &dt);
2046     sprintf(nano_s, "%06d000", hr_dt->useconds);
2047 
2048     while (format[scanned_pos] != '\0') {
2049         int fmt_pos;            // Index after last character to pass as-is
2050         int nano_digits = 0;    // Length of %N field width (if any)
2051         char *tmp_fmt_s = NULL;
2052         size_t nbytes = 0;
2053 
2054         // Look for next format specifier
2055         const char *mark_s = strchr(&format[scanned_pos], '%');
2056 
2057         if (mark_s == NULL) {
2058             // No more specifiers, so pass remaining string to strftime() as-is
2059             scanned_pos = strlen(format);
2060             fmt_pos = scanned_pos;
2061 
2062         } else {
2063             fmt_pos = mark_s - format; // Index of %
2064 
2065             // Skip % and any field width
2066             scanned_pos = fmt_pos + 1;
2067             while (isdigit(format[scanned_pos])) {
2068                 scanned_pos++;
2069             }
2070 
2071             switch (format[scanned_pos]) {
2072                 case '\0': // Literal % and possibly digits at end of string
2073                     fmt_pos = scanned_pos; // Pass remaining string as-is
2074                     break;
2075 
2076                 case 'N': // %[width]N
2077                     scanned_pos++;
2078 
2079                     // Parse field width
2080                     nano_digits = atoi(&format[fmt_pos + 1]);
2081                     nano_digits = QB_MAX(nano_digits, 0);
2082                     nano_digits = QB_MIN(nano_digits, 6);
2083                     break;
2084 
2085                 default: // Some other specifier
2086                     if (format[++scanned_pos] != '\0') { // More to parse
2087                         continue;
2088                     }
2089                     fmt_pos = scanned_pos; // Pass remaining string as-is
2090                     break;
2091             }
2092         }
2093 
2094         if (date_len >= sizeof(date_s)) {
2095             return NULL; // No room for remaining string
2096         }
2097 
2098         tmp_fmt_s = strndup(&format[printed_pos], fmt_pos - printed_pos);
2099 #ifdef HAVE_FORMAT_NONLITERAL
2100 #pragma GCC diagnostic push
2101 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
2102 #endif
2103         nbytes = strftime(&date_s[date_len], sizeof(date_s) - date_len,
2104                           tmp_fmt_s, &tm);
2105 #ifdef HAVE_FORMAT_NONLITERAL
2106 #pragma GCC diagnostic pop
2107 #endif
2108         free(tmp_fmt_s);
2109         if (nbytes == 0) { // Would overflow buffer
2110             return NULL;
2111         }
2112         date_len += nbytes;
2113         printed_pos = scanned_pos;
2114         if (nano_digits != 0) {
2115             int nc = 0;
2116 
2117             if (date_len >= sizeof(date_s)) {
2118                 return NULL; // No room to add nanoseconds
2119             }
2120             nc = snprintf(&date_s[date_len], sizeof(date_s) - date_len,
2121                           "%.*s", nano_digits, nano_s);
2122 
2123             if ((nc < 0) || (nc == (sizeof(date_s) - date_len))) {
2124                 return NULL; // Error or would overflow buffer
2125             }
2126             date_len += nc;
2127         }
2128     }
2129 
2130     return (date_len == 0)? NULL : pcmk__str_copy(date_s);
2131 }
2132 
2133 /*!
2134  * \internal
2135  * \brief Return a human-friendly string corresponding to an epoch time value
2136  *
2137  * \param[in]  source  Pointer to epoch time value (or \p NULL for current time)
2138  * \param[in]  flags   Group of \p crm_time_* flags controlling display format
2139  *                     (0 to use \p ctime() with newline removed)
2140  *
2141  * \return String representation of \p source on success (may be empty depending
2142  *         on \p flags; guaranteed not to be \p NULL)
2143  *
2144  * \note The caller is responsible for freeing the return value using \p free().
2145  */
2146 char *
2147 pcmk__epoch2str(const time_t *source, uint32_t flags)
     /* [previous][next][first][last][top][bottom][index][help] */
2148 {
2149     time_t epoch_time = (source == NULL)? time(NULL) : *source;
2150 
2151     if (flags == 0) {
2152         return pcmk__str_copy(pcmk__trim(ctime(&epoch_time)));
2153     } else {
2154         crm_time_t dt;
2155 
2156         crm_time_set_timet(&dt, &epoch_time);
2157         return crm_time_as_string(&dt, flags);
2158     }
2159 }
2160 
2161 /*!
2162  * \internal
2163  * \brief Return a human-friendly string corresponding to seconds-and-
2164  *        nanoseconds value
2165  *
2166  * Time is shown with microsecond resolution if \p crm_time_usecs is in \p
2167  * flags.
2168  *
2169  * \param[in]  ts     Time in seconds and nanoseconds (or \p NULL for current
2170  *                    time)
2171  * \param[in]  flags  Group of \p crm_time_* flags controlling display format
2172  *
2173  * \return String representation of \p ts on success (may be empty depending on
2174  *         \p flags; guaranteed not to be \p NULL)
2175  *
2176  * \note The caller is responsible for freeing the return value using \p free().
2177  */
2178 char *
2179 pcmk__timespec2str(const struct timespec *ts, uint32_t flags)
     /* [previous][next][first][last][top][bottom][index][help] */
2180 {
2181     struct timespec tmp_ts;
2182     crm_time_t dt;
2183     char result[DATE_MAX] = { 0 };
2184 
2185     if (ts == NULL) {
2186         qb_util_timespec_from_epoch_get(&tmp_ts);
2187         ts = &tmp_ts;
2188     }
2189     crm_time_set_timet(&dt, &ts->tv_sec);
2190     time_as_string_common(&dt, ts->tv_nsec / QB_TIME_NS_IN_USEC, flags, result);
2191     return pcmk__str_copy(result);
2192 }
2193 
2194 /*!
2195  * \internal
2196  * \brief Given a millisecond interval, return a log-friendly string
2197  *
2198  * \param[in] interval_ms  Interval in milliseconds
2199  *
2200  * \return Readable version of \p interval_ms
2201  *
2202  * \note The return value is a pointer to static memory that will be
2203  *       overwritten by later calls to this function.
2204  */
2205 const char *
2206 pcmk__readable_interval(guint interval_ms)
     /* [previous][next][first][last][top][bottom][index][help] */
2207 {
2208 #define MS_IN_S (1000)
2209 #define MS_IN_M (MS_IN_S * 60)
2210 #define MS_IN_H (MS_IN_M * 60)
2211 #define MS_IN_D (MS_IN_H * 24)
2212 #define MAXSTR sizeof("..d..h..m..s...ms")
2213     static char str[MAXSTR];
2214     int offset = 0;
2215 
2216     str[0] = '\0';
2217     if (interval_ms >= MS_IN_D) {
2218         offset += snprintf(str + offset, MAXSTR - offset, "%ud",
2219                            interval_ms / MS_IN_D);
2220         interval_ms -= (interval_ms / MS_IN_D) * MS_IN_D;
2221     }
2222     if (interval_ms >= MS_IN_H) {
2223         offset += snprintf(str + offset, MAXSTR - offset, "%uh",
2224                            interval_ms / MS_IN_H);
2225         interval_ms -= (interval_ms / MS_IN_H) * MS_IN_H;
2226     }
2227     if (interval_ms >= MS_IN_M) {
2228         offset += snprintf(str + offset, MAXSTR - offset, "%um",
2229                            interval_ms / MS_IN_M);
2230         interval_ms -= (interval_ms / MS_IN_M) * MS_IN_M;
2231     }
2232 
2233     // Ns, N.NNNs, or NNNms
2234     if (interval_ms >= MS_IN_S) {
2235         offset += snprintf(str + offset, MAXSTR - offset, "%u",
2236                            interval_ms / MS_IN_S);
2237         interval_ms -= (interval_ms / MS_IN_S) * MS_IN_S;
2238         if (interval_ms > 0) {
2239             offset += snprintf(str + offset, MAXSTR - offset, ".%03u",
2240                                interval_ms);
2241         }
2242         (void) snprintf(str + offset, MAXSTR - offset, "s");
2243 
2244     } else if (interval_ms > 0) {
2245         (void) snprintf(str + offset, MAXSTR - offset, "%ums", interval_ms);
2246 
2247     } else if (str[0] == '\0') {
2248         strcpy(str, "0s");
2249     }
2250     return str;
2251 }

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