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

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