diff --git a/include/crm/common/iso8601.h b/include/crm/common/iso8601.h
index ec03913541..06fb5dacec 100644
--- a/include/crm/common/iso8601.h
+++ b/include/crm/common/iso8601.h
@@ -1,128 +1,129 @@
 /*
  * Copyright 2005-2020 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PCMK__CRM_COMMON_ISO8601__H
 #  define PCMK__CRM_COMMON_ISO8601__H
 
 #  include <time.h>
 #  include <ctype.h>
 #  include <stdint.h>   // uint32_t
 #  include <stdbool.h>  // bool
 
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 /**
  * \file
  * \brief ISO_8601 Date handling
  * \ingroup date
  */
 
 /*
  * See https://en.wikipedia.org/wiki/ISO_8601
  */
 
 typedef struct crm_time_s crm_time_t;
 
 typedef struct crm_time_period_s {
     crm_time_t *start;
     crm_time_t *end;
     crm_time_t *diff;
 } crm_time_period_t;
 
 /* Creates a new date/time object conforming to ISO 8601, for example:
  *   Ordinal:   2010-01 12:00:00 +10:00
  *   Gregorian: 2010-01-01 12:00:00 +10:00
  *   ISO Week:  2010-W53-6 12:00:00 +10:00
  *
  * Notes:
  *   Only one of date, time is required
  *   If date or timezone is unspecified, they default to the current one
  *   Supplying NULL results in the current date/time
  *   Dashes may be omitted from dates
  *   Colons may be omitted from times and timezones
  *   A timezone of 'Z' denotes UTC time
  */
 crm_time_t *crm_time_new(const char *string);
 crm_time_t *crm_time_new_undefined(void);
 void crm_time_free(crm_time_t * dt);
 
 bool crm_time_is_defined(const crm_time_t *t);
 char *crm_time_as_string(const crm_time_t *dt, int flags);
 
 #define crm_time_log(level, prefix, dt, flags)  \
     crm_time_log_alias(level, __FILE__, __func__, __LINE__, prefix, dt, flags)
 
 void crm_time_log_alias(int log_level, const char *file, const char *function,
                         int line, const char *prefix,
                         const crm_time_t *date_time, int flags);
 
 #  define crm_time_log_date          0x001
 #  define crm_time_log_timeofday     0x002
 #  define crm_time_log_with_timezone 0x004
 #  define crm_time_log_duration      0x008
 
 #  define crm_time_ordinal           0x010
 #  define crm_time_weeks             0x020
 #  define crm_time_seconds           0x100
 #  define crm_time_epoch             0x200
 
 crm_time_t *crm_time_parse_duration(const char *duration_str);
-crm_time_t *crm_time_calculate_duration(crm_time_t * dt, crm_time_t * value);
+crm_time_t *crm_time_calculate_duration(const crm_time_t *dt,
+                                        const crm_time_t *value);
 crm_time_period_t *crm_time_parse_period(const char *period_str);
 void crm_time_free_period(crm_time_period_t *period);
 
 int crm_time_compare(crm_time_t * dt, crm_time_t * rhs);
 
 int crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m,
                            uint32_t *s);
 int crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m);
 int crm_time_get_gregorian(const crm_time_t *dt, uint32_t *y, uint32_t *m,
                            uint32_t *d);
 int crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d);
 int crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w,
                          uint32_t * d);
 
 /* Time in seconds since 0000-01-01 00:00:00Z */
 long long crm_time_get_seconds(const crm_time_t *dt);
 
 /* Time in seconds since 1970-01-01 00:00:00Z */
 long long crm_time_get_seconds_since_epoch(const crm_time_t *dt);
 
 void crm_time_set(crm_time_t *target, const crm_time_t *source);
 void crm_time_set_timet(crm_time_t *target, const time_t *source);
 
 /* Returns a new time object */
 crm_time_t *pcmk_copy_time(const crm_time_t *source);
 crm_time_t *crm_time_add(const crm_time_t *dt, const crm_time_t *value);
 crm_time_t *crm_time_subtract(crm_time_t * dt, crm_time_t * value);
 
 /* All crm_time_add_... functions support negative values */
 void crm_time_add_seconds(crm_time_t * dt, int value);
 void crm_time_add_minutes(crm_time_t * dt, int value);
 void crm_time_add_hours(crm_time_t * dt, int value);
 void crm_time_add_days(crm_time_t * dt, int value);
 void crm_time_add_weeks(crm_time_t * dt, int value);
 void crm_time_add_months(crm_time_t * dt, int value);
 void crm_time_add_years(crm_time_t * dt, int value);
 
 /* Useful helper functions */
 int crm_time_january1_weekday(int year);
 int crm_time_weeks_in_year(int year);
 int crm_time_days_in_month(int month, int year);
 
 bool crm_time_leapyear(int year);
 bool crm_time_check(crm_time_t * dt);
 
 #ifdef __cplusplus
 }
 #endif
 
 #endif
diff --git a/lib/common/iso8601.c b/lib/common/iso8601.c
index 0babe76ca0..daa0ab60a4 100644
--- a/lib/common/iso8601.c
+++ b/lib/common/iso8601.c
@@ -1,1808 +1,1808 @@
 /*
  * Copyright 2005-2022 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU Lesser General Public License
  * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY.
  */
 
 /*
  * References:
  *	https://en.wikipedia.org/wiki/ISO_8601
  *	http://www.staff.science.uu.nl/~gent0113/calendar/isocalendar.htm
  */
 
 #include <crm_internal.h>
 #include <crm/crm.h>
 #include <time.h>
 #include <ctype.h>
 #include <string.h>
 #include <stdbool.h>
 #include <crm/common/iso8601.h>
 
 /*
  * Andrew's code was originally written for OSes whose "struct tm" contains:
  *	long tm_gmtoff;		:: Seconds east of UTC
  *	const char *tm_zone;	:: Timezone abbreviation
  * Some OSes lack these, instead having:
  *	time_t (or long) timezone;
 		:: "difference between UTC and local standard time"
  *	char *tzname[2] = { "...", "..." };
  * I (David Lee) confess to not understanding the details.  So my attempted
  * generalisations for where their use is necessary may be flawed.
  *
  * 1. Does "difference between ..." subtract the same or opposite way?
  * 2. Should it use "altzone" instead of "timezone"?
  * 3. Should it use tzname[0] or tzname[1]?  Interaction with timezone/altzone?
  */
 #if defined(HAVE_STRUCT_TM_TM_GMTOFF)
 #  define GMTOFF(tm) ((tm)->tm_gmtoff)
 #else
 /* Note: extern variable; macro argument not actually used.  */
 #  define GMTOFF(tm) (-timezone+daylight)
 #endif
 
 #define HOUR_SECONDS    (60 * 60)
 #define DAY_SECONDS     (HOUR_SECONDS * 24)
 
 // A date/time or duration
 struct crm_time_s {
     int years;      // Calendar year (date/time) or number of years (duration)
     int months;     // Number of months (duration only)
     int days;       // Ordinal day of year (date/time) or number of days (duration)
     int seconds;    // Seconds of day (date/time) or number of seconds (duration)
     int offset;     // Seconds offset from UTC (date/time only)
     bool duration;  // True if duration
 };
 
 static crm_time_t *parse_date(const char *date_str);
 
 static crm_time_t *
 crm_get_utc_time(const crm_time_t *dt)
 {
     crm_time_t *utc = NULL;
 
     if (dt == NULL) {
         errno = EINVAL;
         return NULL;
     }
 
     utc = crm_time_new_undefined();
     utc->years = dt->years;
     utc->days = dt->days;
     utc->seconds = dt->seconds;
     utc->offset = 0;
 
     if (dt->offset) {
         crm_time_add_seconds(utc, -dt->offset);
     } else {
         /* Durations (which are the only things that can include months, never have a timezone */
         utc->months = dt->months;
     }
 
     crm_time_log(LOG_TRACE, "utc-source", dt,
                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
     crm_time_log(LOG_TRACE, "utc-target", utc,
                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
     return utc;
 }
 
 crm_time_t *
 crm_time_new(const char *date_time)
 {
     time_t tm_now;
     crm_time_t *dt = NULL;
 
     tzset();
     if (date_time == NULL) {
         tm_now = time(NULL);
         dt = crm_time_new_undefined();
         crm_time_set_timet(dt, &tm_now);
     } else {
         dt = parse_date(date_time);
     }
     return dt;
 }
 
 /*!
  * \brief Allocate memory for an uninitialized time object
  *
  * \return Newly allocated time object
  * \note The caller is responsible for freeing the return value using
  *       crm_time_free().
  */
 crm_time_t *
 crm_time_new_undefined(void)
 {
     crm_time_t *result = calloc(1, sizeof(crm_time_t));
 
     CRM_ASSERT(result != NULL);
     return result;
 }
 
 /*!
  * \brief Check whether a time object has been initialized yet
  *
  * \param[in] t  Time object to check
  *
  * \return TRUE if time object has been initialized, FALSE otherwise
  */
 bool
 crm_time_is_defined(const crm_time_t *t)
 {
     // Any nonzero member indicates something has been done to t
     return (t != NULL) && (t->years || t->months || t->days || t->seconds
                            || t->offset || t->duration);
 }
 
 void
 crm_time_free(crm_time_t * dt)
 {
     if (dt == NULL) {
         return;
     }
     free(dt);
 }
 
 static int
 year_days(int year)
 {
     int d = 365;
 
     if (crm_time_leapyear(year)) {
         d++;
     }
     return d;
 }
 
 /* From http://myweb.ecu.edu/mccartyr/ISOwdALG.txt :
  *
  * 5. Find the Jan1Weekday for Y (Monday=1, Sunday=7)
  *  YY = (Y-1) % 100
  *  C = (Y-1) - YY
  *  G = YY + YY/4
  *  Jan1Weekday = 1 + (((((C / 100) % 4) x 5) + G) % 7)
  */
 int
 crm_time_january1_weekday(int year)
 {
     int YY = (year - 1) % 100;
     int C = (year - 1) - YY;
     int G = YY + YY / 4;
     int jan1 = 1 + (((((C / 100) % 4) * 5) + G) % 7);
 
     crm_trace("YY=%d, C=%d, G=%d", YY, C, G);
     crm_trace("January 1 %.4d: %d", year, jan1);
     return jan1;
 }
 
 int
 crm_time_weeks_in_year(int year)
 {
     int weeks = 52;
     int jan1 = crm_time_january1_weekday(year);
 
     /* if jan1 == thursday */
     if (jan1 == 4) {
         weeks++;
     } else {
         jan1 = crm_time_january1_weekday(year + 1);
         /* if dec31 == thursday aka. jan1 of next year is a friday */
         if (jan1 == 5) {
             weeks++;
         }
 
     }
     return weeks;
 }
 
 // Jan-Dec plus Feb of leap years
 static int month_days[13] = {
     31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 29
 };
 
 /*!
  * \brief Return number of days in given month of given year
  *
  * \param[in]  Ordinal month (1-12)
  * \param[in]  Gregorian year
  *
  * \return Number of days in given month (0 if given month is invalid)
  */
 int
 crm_time_days_in_month(int month, int year)
 {
     if ((month < 1) || (month > 12)) {
         return 0;
     }
     if ((month == 2) && crm_time_leapyear(year)) {
         month = 13;
     }
     return month_days[month - 1];
 }
 
 bool
 crm_time_leapyear(int year)
 {
     gboolean is_leap = FALSE;
 
     if (year % 4 == 0) {
         is_leap = TRUE;
     }
     if (year % 100 == 0 && year % 400 != 0) {
         is_leap = FALSE;
     }
     return is_leap;
 }
 
 static uint32_t
 get_ordinal_days(uint32_t y, uint32_t m, uint32_t d)
 {
     int lpc;
 
     for (lpc = 1; lpc < m; lpc++) {
         d += crm_time_days_in_month(lpc, y);
     }
     return d;
 }
 
 void
 crm_time_log_alias(int log_level, const char *file, const char *function,
                    int line, const char *prefix, const crm_time_t *date_time,
                    int flags)
 {
     char *date_s = crm_time_as_string(date_time, flags);
 
     if (log_level == LOG_STDOUT) {
         printf("%s%s%s\n",
                (prefix? prefix : ""), (prefix? ": " : ""), date_s);
     } else {
         do_crm_log_alias(log_level, file, function, line, "%s%s%s",
                          (prefix? prefix : ""), (prefix? ": " : ""), date_s);
     }
     free(date_s);
 }
 
 static void
 crm_time_get_sec(int sec, uint32_t *h, uint32_t *m, uint32_t *s)
 {
     uint32_t hours, minutes, seconds;
 
     if (sec < 0) {
         seconds = 0 - sec;
     } else {
         seconds = sec;
     }
 
     hours = seconds / HOUR_SECONDS;
     seconds -= HOUR_SECONDS * hours;
 
     minutes = seconds / 60;
     seconds -= 60 * minutes;
 
     crm_trace("%d == %.2d:%.2d:%.2d", sec, hours, minutes, seconds);
 
     *h = hours;
     *m = minutes;
     *s = seconds;
 }
 
 int
 crm_time_get_timeofday(const crm_time_t *dt, uint32_t *h, uint32_t *m,
                        uint32_t *s)
 {
     crm_time_get_sec(dt->seconds, h, m, s);
     return TRUE;
 }
 
 int
 crm_time_get_timezone(const crm_time_t *dt, uint32_t *h, uint32_t *m)
 {
     uint32_t s;
 
     crm_time_get_sec(dt->seconds, h, m, &s);
     return TRUE;
 }
 
 long long
 crm_time_get_seconds(const crm_time_t *dt)
 {
     int lpc;
     crm_time_t *utc = NULL;
     long long in_seconds = 0;
 
     if (dt == NULL) {
         return 0;
     }
 
     utc = crm_get_utc_time(dt);
     if (utc == NULL) {
         return 0;
     }
 
     for (lpc = 1; lpc < utc->years; lpc++) {
         long long dmax = year_days(lpc);
 
         in_seconds += DAY_SECONDS * dmax;
     }
 
     /* utc->months is an offset that can only be set for a duration.
      * By definition, the value is variable depending on the date to
      * which it is applied.
      *
      * Force 30-day months so that something vaguely sane happens
      * for anyone that tries to use a month in this way.
      */
     if (utc->months > 0) {
         in_seconds += DAY_SECONDS * 30 * (long long) (utc->months);
     }
 
     if (utc->days > 0) {
         in_seconds += DAY_SECONDS * (long long) (utc->days - 1);
     }
     in_seconds += utc->seconds;
 
     crm_time_free(utc);
     return in_seconds;
 }
 
 #define EPOCH_SECONDS 62135596800ULL    /* Calculated using crm_time_get_seconds() */
 long long
 crm_time_get_seconds_since_epoch(const crm_time_t *dt)
 {
     return (dt == NULL)? 0 : (crm_time_get_seconds(dt) - EPOCH_SECONDS);
 }
 
 int
 crm_time_get_gregorian(const crm_time_t *dt, uint32_t *y, uint32_t *m,
                        uint32_t *d)
 {
     int months = 0;
     int days = dt->days;
 
     if(dt->years != 0) {
         for (months = 1; months <= 12 && days > 0; months++) {
             int mdays = crm_time_days_in_month(months, dt->years);
 
             if (mdays >= days) {
                 break;
             } else {
                 days -= mdays;
             }
         }
 
     } else if (dt->months) {
         /* This is a duration including months, don't convert the days field */
         months = dt->months;
 
     } else {
         /* This is a duration not including months, still don't convert the days field */
     }
 
     *y = dt->years;
     *m = months;
     *d = days;
     crm_trace("%.4d-%.3d -> %.4d-%.2d-%.2d", dt->years, dt->days, dt->years, months, days);
     return TRUE;
 }
 
 int
 crm_time_get_ordinal(const crm_time_t *dt, uint32_t *y, uint32_t *d)
 {
     *y = dt->years;
     *d = dt->days;
     return TRUE;
 }
 
 int
 crm_time_get_isoweek(const crm_time_t *dt, uint32_t *y, uint32_t *w,
                      uint32_t *d)
 {
     /*
      * Monday 29 December 2008 is written "2009-W01-1"
      * Sunday 3 January 2010 is written "2009-W53-7"
      */
     int year_num = 0;
     int jan1 = crm_time_january1_weekday(dt->years);
     int h = -1;
 
     CRM_CHECK(dt->days > 0, return FALSE);
 
 /* 6. Find the Weekday for Y M D */
     h = dt->days + jan1 - 1;
     *d = 1 + ((h - 1) % 7);
 
 /* 7. Find if Y M D falls in YearNumber Y-1, WeekNumber 52 or 53 */
     if (dt->days <= (8 - jan1) && jan1 > 4) {
         crm_trace("year--, jan1=%d", jan1);
         year_num = dt->years - 1;
         *w = crm_time_weeks_in_year(year_num);
 
     } else {
         year_num = dt->years;
     }
 
 /* 8. Find if Y M D falls in YearNumber Y+1, WeekNumber 1 */
     if (year_num == dt->years) {
         int dmax = year_days(year_num);
         int correction = 4 - *d;
 
         if ((dmax - dt->days) < correction) {
             crm_trace("year++, jan1=%d, i=%d vs. %d", jan1, dmax - dt->days, correction);
             year_num = dt->years + 1;
             *w = 1;
         }
     }
 
 /* 9. Find if Y M D falls in YearNumber Y, WeekNumber 1 through 53 */
     if (year_num == dt->years) {
         int j = dt->days + (7 - *d) + (jan1 - 1);
 
         *w = j / 7;
         if (jan1 > 4) {
             *w -= 1;
         }
     }
 
     *y = year_num;
     crm_trace("Converted %.4d-%.3d to %.4d-W%.2d-%d", dt->years, dt->days, *y, *w, *d);
     return TRUE;
 }
 
 #define DATE_MAX 128
 
 static void
 crm_duration_as_string(const crm_time_t *dt, char *result)
 {
     size_t offset = 0;
 
     if (dt->years) {
         offset += snprintf(result + offset, DATE_MAX - offset, "%4d year%s ",
                            dt->years, pcmk__plural_s(dt->years));
     }
     if (dt->months) {
         offset += snprintf(result + offset, DATE_MAX - offset, "%2d month%s ",
                            dt->months, pcmk__plural_s(dt->months));
     }
     if (dt->days) {
         offset += snprintf(result + offset, DATE_MAX - offset, "%2d day%s ",
                            dt->days, pcmk__plural_s(dt->days));
     }
 
     if (((offset == 0) || (dt->seconds != 0))
         && (dt->seconds > -60) && (dt->seconds < 60)) {
         offset += snprintf(result + offset, DATE_MAX - offset, "%d second%s",
                            dt->seconds, pcmk__plural_s(dt->seconds));
     } else if (dt->seconds) {
         uint32_t h = 0, m = 0, s = 0;
 
         offset += snprintf(result + offset, DATE_MAX - offset, "%d seconds (",
                            dt->seconds);
         crm_time_get_sec(dt->seconds, &h, &m, &s);
         if (h) {
             offset += snprintf(result + offset, DATE_MAX - offset, "%u hour%s%s",
                                h, pcmk__plural_s(h), ((m || s)? " " : ""));
         }
         if (m) {
             offset += snprintf(result + offset, DATE_MAX - offset, "%u minute%s%s",
                                m, pcmk__plural_s(m), (s? " " : ""));
         }
         if (s) {
             offset += snprintf(result + offset, DATE_MAX - offset, "%u second%s",
                                s, pcmk__plural_s(s));
         }
         offset += snprintf(result + offset, DATE_MAX - offset, ")");
     }
 }
 
 char *
 crm_time_as_string(const crm_time_t *date_time, int flags)
 {
     const crm_time_t *dt = NULL;
     crm_time_t *utc = NULL;
     char result[DATE_MAX] = { '\0', };
     char *result_copy = NULL;
     size_t offset = 0;
 
     // Convert to UTC if local timezone was not requested
     if (date_time && date_time->offset
         && !pcmk_is_set(flags, crm_time_log_with_timezone)) {
         crm_trace("UTC conversion");
         utc = crm_get_utc_time(date_time);
         dt = utc;
     } else {
         dt = date_time;
     }
 
     if (!crm_time_is_defined(dt)) {
         strcpy(result, "<undefined time>");
         goto done;
     }
 
     // Simple cases: as duration, seconds, or seconds since epoch
 
     if (flags & crm_time_log_duration) {
         crm_duration_as_string(date_time, result);
         goto done;
     }
 
     if (flags & crm_time_seconds) {
         snprintf(result, DATE_MAX, "%lld", crm_time_get_seconds(date_time));
         goto done;
     }
 
     if (flags & crm_time_epoch) {
         snprintf(result, DATE_MAX, "%lld",
                  crm_time_get_seconds_since_epoch(date_time));
         goto done;
     }
 
     // As readable string
 
     if (flags & crm_time_log_date) {
         if (flags & crm_time_weeks) { // YYYY-WW-D
             uint32_t y, w, d;
 
             if (crm_time_get_isoweek(dt, &y, &w, &d)) {
                 offset += snprintf(result + offset, DATE_MAX - offset,
                                    "%u-W%.2u-%u", y, w, d);
             }
 
         } else if (flags & crm_time_ordinal) { // YYYY-DDD
             uint32_t y, d;
 
             if (crm_time_get_ordinal(dt, &y, &d)) {
                 offset += snprintf(result + offset, DATE_MAX - offset,
                                    "%u-%.3u", y, d);
             }
 
         } else { // YYYY-MM-DD
             uint32_t y, m, d;
 
             if (crm_time_get_gregorian(dt, &y, &m, &d)) {
                 offset += snprintf(result + offset, DATE_MAX - offset,
                                    "%.4u-%.2u-%.2u", y, m, d);
             }
         }
     }
 
     if (flags & crm_time_log_timeofday) {
         uint32_t h = 0, m = 0, s = 0;
 
         if (offset > 0) {
             offset += snprintf(result + offset, DATE_MAX - offset, " ");
         }
 
         if (crm_time_get_timeofday(dt, &h, &m, &s)) {
             offset += snprintf(result + offset, DATE_MAX - offset,
                                "%.2u:%.2u:%.2u", h, m, s);
         }
 
         if ((flags & crm_time_log_with_timezone) && (dt->offset != 0)) {
             crm_time_get_sec(dt->offset, &h, &m, &s);
             offset += snprintf(result + offset, DATE_MAX - offset,
                                " %c%.2u:%.2u",
                                ((dt->offset < 0)? '-' : '+'), h, m);
         } else {
             offset += snprintf(result + offset, DATE_MAX - offset, "Z");
         }
     }
 
   done:
     crm_time_free(utc);
 
     result_copy = strdup(result);
     CRM_ASSERT(result_copy != NULL);
     return result_copy;
 }
 
 /*!
  * \internal
  * \brief Determine number of seconds from an hour:minute:second string
  *
  * \param[in]  time_str  Time specification string
  * \param[out] result    Number of seconds equivalent to time_str
  *
  * \return TRUE if specification was valid, FALSE (and set errno) otherwise
  * \note This may return the number of seconds in a day (which is out of bounds
  *       for a time object) if given 24:00:00.
  */
 static bool
 crm_time_parse_sec(const char *time_str, int *result)
 {
     int rc;
     uint32_t hour = 0;
     uint32_t minute = 0;
     uint32_t second = 0;
 
     *result = 0;
 
     // Must have at least hour, but minutes and seconds are optional
     rc = sscanf(time_str, "%d:%d:%d", &hour, &minute, &second);
     if (rc == 1) {
         rc = sscanf(time_str, "%2d%2d%2d", &hour, &minute, &second);
     }
     if (rc == 0) {
         crm_err("%s is not a valid ISO 8601 time specification", time_str);
         errno = EINVAL;
         return FALSE;
     }
 
     crm_trace("Got valid time: %.2d:%.2d:%.2d", hour, minute, second);
 
     if ((hour == 24) && (minute == 0) && (second == 0)) {
         // Equivalent to 00:00:00 of next day, return number of seconds in day
     } else if (hour >= 24) {
         crm_err("%s is not a valid ISO 8601 time specification "
                 "because %d is not a valid hour", time_str, hour);
         errno = EINVAL;
         return FALSE;
     }
     if (minute >= 60) {
         crm_err("%s is not a valid ISO 8601 time specification "
                 "because %d is not a valid minute", time_str, minute);
         errno = EINVAL;
         return FALSE;
     }
     if (second >= 60) {
         crm_err("%s is not a valid ISO 8601 time specification "
                 "because %d is not a valid second", time_str, second);
         errno = EINVAL;
         return FALSE;
     }
 
     *result = (hour * HOUR_SECONDS) + (minute * 60) + second;
     return TRUE;
 }
 
 static bool
 crm_time_parse_offset(const char *offset_str, int *offset)
 {
     tzset();
 
     if (offset_str == NULL) {
         // Use local offset
 #if defined(HAVE_STRUCT_TM_TM_GMTOFF)
         time_t now = time(NULL);
         struct tm *now_tm = localtime(&now);
 #endif
         int h_offset = GMTOFF(now_tm) / HOUR_SECONDS;
         int m_offset = (GMTOFF(now_tm) - (HOUR_SECONDS * h_offset)) / 60;
 
         if (h_offset < 0 && m_offset < 0) {
             m_offset = 0 - m_offset;
         }
         *offset = (HOUR_SECONDS * h_offset) + (60 * m_offset);
         return TRUE;
     }
 
     if (offset_str[0] == 'Z') { // @TODO invalid if anything after?
         *offset = 0;
         return TRUE;
     }
 
     *offset = 0;
     if ((offset_str[0] == '+') || (offset_str[0] == '-')
         || isdigit((int)offset_str[0])) {
 
         gboolean negate = FALSE;
 
         if (offset_str[0] == '+') {
             offset_str++;
         } else if (offset_str[0] == '-') {
             negate = TRUE;
             offset_str++;
         }
         if (crm_time_parse_sec(offset_str, offset) == FALSE) {
             return FALSE;
         }
         if (negate) {
             *offset = 0 - *offset;
         }
     } // @TODO else invalid?
     return TRUE;
 }
 
 /*!
  * \internal
  * \brief Parse the time portion of an ISO 8601 date/time string
  *
  * \param[in]     time_str  Time portion of specification (after any 'T')
  * \param[in,out] a_time    Time object to parse into
  *
  * \return TRUE if valid time was parsed, FALSE (and set errno) otherwise
  * \note This may add a day to a_time (if the time is 24:00:00).
  */
 static bool
 crm_time_parse(const char *time_str, crm_time_t *a_time)
 {
     uint32_t h, m, s;
     char *offset_s = NULL;
 
     tzset();
 
     if (time_str) {
         if (crm_time_parse_sec(time_str, &(a_time->seconds)) == FALSE) {
             return FALSE;
         }
         offset_s = strstr(time_str, "Z");
         if (offset_s == NULL) {
             offset_s = strstr(time_str, " ");
             if (offset_s) {
                 while (isspace(offset_s[0])) {
                     offset_s++;
                 }
             }
         }
     }
 
     if (crm_time_parse_offset(offset_s, &(a_time->offset)) == FALSE) {
         return FALSE;
     }
     crm_time_get_sec(a_time->offset, &h, &m, &s);
     crm_trace("Got tz: %c%2.d:%.2d", ((a_time->offset < 0)? '-' : '+'), h, m);
 
     if (a_time->seconds == DAY_SECONDS) {
         // 24:00:00 == 00:00:00 of next day
         a_time->seconds = 0;
         crm_time_add_days(a_time, 1);
     }
     return TRUE;
 }
 
 /*
  * \internal
  * \brief Parse a time object from an ISO 8601 date/time specification
  *
  * \param[in] date_str  ISO 8601 date/time specification (or "epoch")
  *
  * \return New time object on success, NULL (and set errno) otherwise
  */
 static crm_time_t *
 parse_date(const char *date_str)
 {
     const char *time_s = NULL;
     crm_time_t *dt = NULL;
 
     int year = 0;
     int month = 0;
     int week = 0;
     int day = 0;
     int rc = 0;
 
     if (pcmk__str_empty(date_str)) {
         crm_err("No ISO 8601 date/time specification given");
         goto invalid;
     }
 
     if ((date_str[0] == 'T') || (date_str[2] == ':')) {
         /* Just a time supplied - Infer current date */
         dt = crm_time_new(NULL);
         if (date_str[0] == 'T') {
             time_s = date_str + 1;
         } else {
             time_s = date_str;
         }
         goto parse_time;
     }
 
     dt = crm_time_new_undefined();
 
     if (!strncasecmp("epoch", date_str, 5)
         && ((date_str[5] == '\0') || (date_str[5] == '/') || isspace(date_str[5]))) {
         dt->days = 1;
         dt->years = 1970;
         crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
         return dt;
     }
 
     /* YYYY-MM-DD */
     rc = sscanf(date_str, "%d-%d-%d", &year, &month, &day);
     if (rc == 1) {
         /* YYYYMMDD */
         rc = sscanf(date_str, "%4d%2d%2d", &year, &month, &day);
     }
     if (rc == 3) {
         if (month > 12) {
             crm_err("'%s' is not a valid ISO 8601 date/time specification "
                     "because '%d' is not a valid month", date_str, month);
             goto invalid;
         } else if (day > crm_time_days_in_month(month, year)) {
             crm_err("'%s' is not a valid ISO 8601 date/time specification "
                     "because '%d' is not a valid day of the month",
                     date_str, day);
             goto invalid;
         } else {
             dt->years = year;
             dt->days = get_ordinal_days(year, month, day);
             crm_trace("Parsed Gregorian date '%.4d-%.3d' from date string '%s'",
                       year, dt->days, date_str);
         }
         goto parse_time;
     }
 
     /* YYYY-DDD */
     rc = sscanf(date_str, "%d-%d", &year, &day);
     if (rc == 2) {
         if (day > year_days(year)) {
             crm_err("'%s' is not a valid ISO 8601 date/time specification "
                     "because '%d' is not a valid day of the year (max %d)",
                     date_str, day, year_days(year));
             goto invalid;
         }
         crm_trace("Parsed ordinal year %d and days %d from date string '%s'",
                   year, day, date_str);
         dt->days = day;
         dt->years = year;
         goto parse_time;
     }
 
     /* YYYY-Www-D */
     rc = sscanf(date_str, "%d-W%d-%d", &year, &week, &day);
     if (rc == 3) {
         if (week > crm_time_weeks_in_year(year)) {
             crm_err("'%s' is not a valid ISO 8601 date/time specification "
                     "because '%d' is not a valid week of the year (max %d)",
                     date_str, week, crm_time_weeks_in_year(year));
             goto invalid;
         } else if (day < 1 || day > 7) {
             crm_err("'%s' is not a valid ISO 8601 date/time specification "
                     "because '%d' is not a valid day of the week",
                     date_str, day);
             goto invalid;
         } else {
             /*
              * See https://en.wikipedia.org/wiki/ISO_week_date
              *
              * Monday 29 December 2008 is written "2009-W01-1"
              * Sunday 3 January 2010 is written "2009-W53-7"
              * Saturday 27 September 2008 is written "2008-W37-6"
              *
              * If 1 January is on a Monday, Tuesday, Wednesday or Thursday, it is in week 01.
              * If 1 January is on a Friday, Saturday or Sunday, it is in week 52 or 53 of the previous year.
              */
             int jan1 = crm_time_january1_weekday(year);
 
             crm_trace("Got year %d (Jan 1 = %d), week %d, and day %d from date string '%s'",
                       year, jan1, week, day, date_str);
 
             dt->years = year;
             crm_time_add_days(dt, (week - 1) * 7);
 
             if (jan1 <= 4) {
                 crm_time_add_days(dt, 1 - jan1);
             } else {
                 crm_time_add_days(dt, 8 - jan1);
             }
 
             crm_time_add_days(dt, day);
         }
         goto parse_time;
     }
 
     crm_err("'%s' is not a valid ISO 8601 date/time specification", date_str);
     goto invalid;
 
   parse_time:
 
     if (time_s == NULL) {
         time_s = date_str + strspn(date_str, "0123456789-W");
         if ((time_s[0] == ' ') || (time_s[0] == 'T')) {
             ++time_s;
         } else {
             time_s = NULL;
         }
     }
     if ((time_s != NULL) && (crm_time_parse(time_s, dt) == FALSE)) {
         goto invalid;
     }
 
     crm_time_log(LOG_TRACE, "Unpacked", dt, crm_time_log_date | crm_time_log_timeofday);
     if (crm_time_check(dt) == FALSE) {
         crm_err("'%s' is not a valid ISO 8601 date/time specification",
                 date_str);
         goto invalid;
     }
     return dt;
 
 invalid:
     crm_time_free(dt);
     errno = EINVAL;
     return NULL;
 }
 
 // Parse an ISO 8601 numeric value and return number of characters consumed
 // @TODO This cannot handle >INT_MAX int values
 // @TODO Fractions appear to be not working
 // @TODO Error out on invalid specifications
 static int
 parse_int(const char *str, int field_width, int upper_bound, int *result)
 {
     int lpc = 0;
     int offset = 0;
     int intermediate = 0;
     gboolean fraction = FALSE;
     gboolean negate = FALSE;
 
     *result = 0;
     if (*str == '\0') {
         return 0;
     }
 
     if (str[offset] == 'T') {
         offset++;
     }
 
     if (str[offset] == '.' || str[offset] == ',') {
         fraction = TRUE;
         field_width = -1;
         offset++;
     } else if (str[offset] == '-') {
         negate = TRUE;
         offset++;
     } else if (str[offset] == '+' || str[offset] == ':') {
         offset++;
     }
 
     for (; (fraction || lpc < field_width) && isdigit((int)str[offset]); lpc++) {
         if (fraction) {
             intermediate = (str[offset] - '0') / (10 ^ lpc);
         } else {
             *result *= 10;
             intermediate = str[offset] - '0';
         }
         *result += intermediate;
         offset++;
     }
     if (fraction) {
         *result = (int)(*result * upper_bound);
 
     } else if (upper_bound > 0 && *result > upper_bound) {
         *result = upper_bound;
     }
     if (negate) {
         *result = 0 - *result;
     }
     if (lpc > 0) {
         crm_trace("Found int: %d.  Stopped at str[%d]='%c'", *result, lpc, str[lpc]);
         return offset;
     }
     return 0;
 }
 
 /*!
  * \brief Parse a time duration from an ISO 8601 duration specification
  *
  * \param[in] period_s  ISO 8601 duration specification (optionally followed by
  *                      whitespace, after which the rest of the string will be
  *                      ignored)
  *
  * \return New time object on success, NULL (and set errno) otherwise
  * \note It is the caller's responsibility to return the result using
  *       crm_time_free().
  */
 crm_time_t *
 crm_time_parse_duration(const char *period_s)
 {
     gboolean is_time = FALSE;
     crm_time_t *diff = NULL;
 
     if (pcmk__str_empty(period_s)) {
         crm_err("No ISO 8601 time duration given");
         goto invalid;
     }
     if (period_s[0] != 'P') {
         crm_err("'%s' is not a valid ISO 8601 time duration "
                 "because it does not start with a 'P'", period_s);
         goto invalid;
     }
     if ((period_s[1] == '\0') || isspace(period_s[1])) {
         crm_err("'%s' is not a valid ISO 8601 time duration "
                 "because nothing follows 'P'", period_s);
         goto invalid;
     }
 
     diff = crm_time_new_undefined();
     diff->duration = TRUE;
 
     for (const char *current = period_s + 1;
          current[0] && (current[0] != '/') && !isspace(current[0]);
          ++current) {
 
         int an_int = 0, rc;
 
         if (current[0] == 'T') {
             /* A 'T' separates year/month/day from hour/minute/seconds. We don't
              * require it strictly, but just use it to differentiate month from
              * minutes.
              */
             is_time = TRUE;
             continue;
         }
 
         // An integer must be next
         rc = parse_int(current, 10, 0, &an_int);
         if (rc == 0) {
             crm_err("'%s' is not a valid ISO 8601 time duration "
                     "because no integer at '%s'", period_s, current);
             goto invalid;
         }
         current += rc;
 
         // A time unit must be next (we're not strict about the order)
         switch (current[0]) {
             case 'Y':
                 diff->years = an_int;
                 break;
             case 'M':
                 if (is_time) {
                     /* Minutes */
                     diff->seconds += an_int * 60;
                 } else {
                     diff->months = an_int;
                 }
                 break;
             case 'W':
                 diff->days += an_int * 7;
                 break;
             case 'D':
                 diff->days += an_int;
                 break;
             case 'H':
                 diff->seconds += an_int * HOUR_SECONDS;
                 break;
             case 'S':
                 diff->seconds += an_int;
                 break;
             case '\0':
                 crm_err("'%s' is not a valid ISO 8601 time duration "
                         "because no units after %d", period_s, an_int);
                 goto invalid;
             default:
                 crm_err("'%s' is not a valid ISO 8601 time duration "
                         "because '%c' is not a valid time unit",
                         period_s, current[0]);
                 goto invalid;
         }
     }
 
     if (!crm_time_is_defined(diff)) {
         crm_err("'%s' is not a valid ISO 8601 time duration "
                 "because no amounts and units given", period_s);
         goto invalid;
     }
     return diff;
 
 invalid:
     crm_time_free(diff);
     errno = EINVAL;
     return NULL;
 }
 
 /*!
  * \brief Parse a time period from an ISO 8601 interval specification
  *
  * \param[in] period_str  ISO 8601 interval specification (start/end,
  *                        start/duration, or duration/end)
  *
  * \return New time period object on success, NULL (and set errno) otherwise
  * \note The caller is responsible for freeing the result using
  *       crm_time_free_period().
  */
 crm_time_period_t *
 crm_time_parse_period(const char *period_str)
 {
     const char *original = period_str;
     crm_time_period_t *period = NULL;
 
     if (pcmk__str_empty(period_str)) {
         crm_err("No ISO 8601 time period given");
         goto invalid;
     }
 
     tzset();
     period = calloc(1, sizeof(crm_time_period_t));
     CRM_ASSERT(period != NULL);
 
     if (period_str[0] == 'P') {
         period->diff = crm_time_parse_duration(period_str);
         if (period->diff == NULL) {
             goto error;
         }
     } else {
         period->start = parse_date(period_str);
         if (period->start == NULL) {
             goto error;
         }
     }
 
     period_str = strstr(original, "/");
     if (period_str) {
         ++period_str;
         if (period_str[0] == 'P') {
             if (period->diff != NULL) {
                 crm_err("'%s' is not a valid ISO 8601 time period "
                         "because it has two durations",
                         original);
                 goto invalid;
             }
             period->diff = crm_time_parse_duration(period_str);
             if (period->diff == NULL) {
                 goto error;
             }
         } else {
             period->end = parse_date(period_str);
             if (period->end == NULL) {
                 goto error;
             }
         }
 
     } else if (period->diff != NULL) {
         // Only duration given, assume start is now
         period->start = crm_time_new(NULL);
 
     } else {
         // Only start given
         crm_err("'%s' is not a valid ISO 8601 time period "
                 "because it has no duration or ending time",
                 original);
         goto invalid;
     }
 
     if (period->start == NULL) {
         period->start = crm_time_subtract(period->end, period->diff);
 
     } else if (period->end == NULL) {
         period->end = crm_time_add(period->start, period->diff);
     }
 
     if (crm_time_check(period->start) == FALSE) {
         crm_err("'%s' is not a valid ISO 8601 time period "
                 "because the start is invalid", period_str);
         goto invalid;
     }
     if (crm_time_check(period->end) == FALSE) {
         crm_err("'%s' is not a valid ISO 8601 time period "
                 "because the end is invalid", period_str);
         goto invalid;
     }
     return period;
 
 invalid:
     errno = EINVAL;
 error:
     crm_time_free_period(period);
     return NULL;
 }
 
 /*!
  * \brief Free a dynamically allocated time period object
  *
  * \param[in] period  Time period to free
  */
 void
 crm_time_free_period(crm_time_period_t *period)
 {
     if (period) {
         crm_time_free(period->start);
         crm_time_free(period->end);
         crm_time_free(period->diff);
         free(period);
     }
 }
 
 void
 crm_time_set(crm_time_t *target, const crm_time_t *source)
 {
     crm_trace("target=%p, source=%p", target, source);
 
     CRM_CHECK(target != NULL && source != NULL, return);
 
     target->years = source->years;
     target->days = source->days;
     target->months = source->months;    /* Only for durations */
     target->seconds = source->seconds;
     target->offset = source->offset;
 
     crm_time_log(LOG_TRACE, "source", source,
                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
     crm_time_log(LOG_TRACE, "target", target,
                  crm_time_log_date | crm_time_log_timeofday | crm_time_log_with_timezone);
 }
 
 static void
 ha_set_tm_time(crm_time_t *target, const struct tm *source)
 {
     int h_offset = 0;
     int m_offset = 0;
 
     /* Ensure target is fully initialized */
     target->years = 0;
     target->months = 0;
     target->days = 0;
     target->seconds = 0;
     target->offset = 0;
     target->duration = FALSE;
 
     if (source->tm_year > 0) {
         /* years since 1900 */
         target->years = 1900 + source->tm_year;
     }
 
     if (source->tm_yday >= 0) {
         /* days since January 1 [0-365] */
         target->days = 1 + source->tm_yday;
     }
 
     if (source->tm_hour >= 0) {
         target->seconds += HOUR_SECONDS * source->tm_hour;
     }
     if (source->tm_min >= 0) {
         target->seconds += 60 * source->tm_min;
     }
     if (source->tm_sec >= 0) {
         target->seconds += source->tm_sec;
     }
 
     /* tm_gmtoff == offset from UTC in seconds */
     h_offset = GMTOFF(source) / HOUR_SECONDS;
     m_offset = (GMTOFF(source) - (HOUR_SECONDS * h_offset)) / 60;
     crm_trace("Time offset is %lds (%.2d:%.2d)",
               GMTOFF(source), h_offset, m_offset);
 
     target->offset += HOUR_SECONDS * h_offset;
     target->offset += 60 * m_offset;
 }
 
 void
 crm_time_set_timet(crm_time_t *target, const time_t *source)
 {
     ha_set_tm_time(target, localtime(source));
 }
 
 crm_time_t *
 pcmk_copy_time(const crm_time_t *source)
 {
     crm_time_t *target = crm_time_new_undefined();
 
     crm_time_set(target, source);
     return target;
 }
 
 crm_time_t *
 crm_time_add(const crm_time_t *dt, const crm_time_t *value)
 {
     crm_time_t *utc = NULL;
     crm_time_t *answer = NULL;
 
     if ((dt == NULL) || (value == NULL)) {
         errno = EINVAL;
         return NULL;
     }
 
     answer = pcmk_copy_time(dt);
 
     utc = crm_get_utc_time(value);
     if (utc == NULL) {
         crm_time_free(answer);
         return NULL;
     }
 
     answer->years += utc->years;
     crm_time_add_months(answer, utc->months);
     crm_time_add_days(answer, utc->days);
     crm_time_add_seconds(answer, utc->seconds);
 
     crm_time_free(utc);
     return answer;
 }
 
 crm_time_t *
-crm_time_calculate_duration(crm_time_t * dt, crm_time_t * value)
+crm_time_calculate_duration(const crm_time_t *dt, const crm_time_t *value)
 {
     crm_time_t *utc = NULL;
     crm_time_t *answer = NULL;
 
     if ((dt == NULL) || (value == NULL)) {
         errno = EINVAL;
         return NULL;
     }
 
     utc = crm_get_utc_time(value);
     if (utc == NULL) {
         return NULL;
     }
 
     answer = crm_get_utc_time(dt);
     if (answer == NULL) {
         crm_time_free(utc);
         return NULL;
     }
     answer->duration = TRUE;
 
     answer->years -= utc->years;
     if(utc->months != 0) {
         crm_time_add_months(answer, -utc->months);
     }
     crm_time_add_days(answer, -utc->days);
     crm_time_add_seconds(answer, -utc->seconds);
 
     crm_time_free(utc);
     return answer;
 }
 
 crm_time_t *
 crm_time_subtract(crm_time_t * dt, crm_time_t * value)
 {
     crm_time_t *utc = NULL;
     crm_time_t *answer = NULL;
 
     if ((dt == NULL) || (value == NULL)) {
         errno = EINVAL;
         return NULL;
     }
 
     utc = crm_get_utc_time(value);
     if (utc == NULL) {
         return NULL;
     }
 
     answer = pcmk_copy_time(dt);
     answer->years -= utc->years;
     if(utc->months != 0) {
         crm_time_add_months(answer, -utc->months);
     }
     crm_time_add_days(answer, -utc->days);
     crm_time_add_seconds(answer, -utc->seconds);
 
     return answer;
 }
 
 /*!
  * \brief Check whether a time object represents a sensible date/time
  *
  * \param[in] dt  Date/time object to check
  *
  * \return TRUE if years, days, and seconds are sensible, FALSE otherwise
  */
 bool
 crm_time_check(crm_time_t * dt)
 {
     return (dt != NULL)
            && (dt->days > 0) && (dt->days <= year_days(dt->years))
            && (dt->seconds >= 0) && (dt->seconds < DAY_SECONDS);
 }
 
 #define do_cmp_field(l, r, field)					\
     if(rc == 0) {                                                       \
 		if(l->field > r->field) {				\
 			crm_trace("%s: %d > %d",			\
 				    #field, l->field, r->field);	\
 			rc = 1;                                         \
 		} else if(l->field < r->field) {			\
 			crm_trace("%s: %d < %d",			\
 				    #field, l->field, r->field);	\
 			rc = -1;					\
 		}							\
     }
 
 int
 crm_time_compare(crm_time_t *a, crm_time_t *b)
 {
     int rc = 0;
     crm_time_t *t1 = crm_get_utc_time(a);
     crm_time_t *t2 = crm_get_utc_time(b);
 
     if ((t1 == NULL) && (t2 == NULL)) {
         rc = 0;
     } else if (t1 == NULL) {
         rc = -1;
     } else if (t2 == NULL) {
         rc = 1;
     } else {
         do_cmp_field(t1, t2, years);
         do_cmp_field(t1, t2, days);
         do_cmp_field(t1, t2, seconds);
     }
 
     crm_time_free(t1);
     crm_time_free(t2);
     return rc;
 }
 
 /*!
  * \brief Add a given number of seconds to a date/time or duration
  *
  * \param[in] a_time  Date/time or duration to add seconds to
  * \param[in] extra   Number of seconds to add
  */
 void
 crm_time_add_seconds(crm_time_t *a_time, int extra)
 {
     int days = 0;
 
     crm_trace("Adding %d seconds to %d (max=%d)",
               extra, a_time->seconds, DAY_SECONDS);
     a_time->seconds += extra;
     days = a_time->seconds / DAY_SECONDS;
     a_time->seconds %= DAY_SECONDS;
 
     // Don't have negative seconds
     if (a_time->seconds < 0) {
         a_time->seconds += DAY_SECONDS;
         --days;
     }
 
     crm_time_add_days(a_time, days);
 }
 
 void
 crm_time_add_days(crm_time_t * a_time, int extra)
 {
     int lower_bound = 1;
     int ydays = crm_time_leapyear(a_time->years) ? 366 : 365;
 
     crm_trace("Adding %d days to %.4d-%.3d", extra, a_time->years, a_time->days);
 
     a_time->days += extra;
     while (a_time->days > ydays) {
         a_time->years++;
         a_time->days -= ydays;
         ydays = crm_time_leapyear(a_time->years) ? 366 : 365;
     }
 
     if(a_time->duration) {
         lower_bound = 0;
     }
 
     while (a_time->days < lower_bound) {
         a_time->years--;
         a_time->days += crm_time_leapyear(a_time->years) ? 366 : 365;
     }
 }
 
 void
 crm_time_add_months(crm_time_t * a_time, int extra)
 {
     int lpc;
     uint32_t y, m, d, dmax;
 
     crm_time_get_gregorian(a_time, &y, &m, &d);
     crm_trace("Adding %d months to %.4d-%.2d-%.2d", extra, y, m, d);
 
     if (extra > 0) {
         for (lpc = extra; lpc > 0; lpc--) {
             m++;
             if (m == 13) {
                 m = 1;
                 y++;
             }
         }
     } else {
         for (lpc = -extra; lpc > 0; lpc--) {
             m--;
             if (m == 0) {
                 m = 12;
                 y--;
             }
         }
     }
 
     dmax = crm_time_days_in_month(m, y);
     if (dmax < d) {
         /* Preserve day-of-month unless the month doesn't have enough days */
         d = dmax;
     }
 
     crm_trace("Calculated %.4d-%.2d-%.2d", y, m, d);
 
     a_time->years = y;
     a_time->days = get_ordinal_days(y, m, d);
 
     crm_time_get_gregorian(a_time, &y, &m, &d);
     crm_trace("Got %.4d-%.2d-%.2d", y, m, d);
 }
 
 void
 crm_time_add_minutes(crm_time_t * a_time, int extra)
 {
     crm_time_add_seconds(a_time, extra * 60);
 }
 
 void
 crm_time_add_hours(crm_time_t * a_time, int extra)
 {
     crm_time_add_seconds(a_time, extra * HOUR_SECONDS);
 }
 
 void
 crm_time_add_weeks(crm_time_t * a_time, int extra)
 {
     crm_time_add_days(a_time, extra * 7);
 }
 
 void
 crm_time_add_years(crm_time_t * a_time, int extra)
 {
     a_time->years += extra;
 }
 
 static void
 ha_get_tm_time( struct tm *target, crm_time_t *source)
 {
     *target = (struct tm) {
         .tm_year = source->years - 1900,
         .tm_mday = source->days,
         .tm_sec = source->seconds % 60,
         .tm_min = ( source->seconds / 60 ) % 60,
         .tm_hour = source->seconds / HOUR_SECONDS,
         .tm_isdst = -1, /* don't adjust */
 
 #if defined(HAVE_STRUCT_TM_TM_GMTOFF)
         .tm_gmtoff = source->offset
 #endif
     };
     mktime(target);
 }
 
 /* The high-resolution variant of time object was added to meet an immediate
  * need, and is kept internal API.
  *
  * @TODO The long-term goal is to come up with a clean, unified design for a
  *       time type (or types) that meets all the various needs, to replace
  *       crm_time_t, pcmk__time_hr_t, and struct timespec (in lrmd_cmd_t).
  *       Using glib's GDateTime is a possibility (if we are willing to require
  *       glib >= 2.26).
  */
 
 pcmk__time_hr_t *
 pcmk__time_hr_convert(pcmk__time_hr_t *target, crm_time_t *dt)
 {
     pcmk__time_hr_t *hr_dt = NULL;
 
     if (dt) {
         hr_dt = target?target:calloc(1, sizeof(pcmk__time_hr_t));
         CRM_ASSERT(hr_dt != NULL);
         *hr_dt = (pcmk__time_hr_t) {
             .years = dt->years,
             .months = dt->months,
             .days = dt->days,
             .seconds = dt->seconds,
             .offset = dt->offset,
             .duration = dt->duration
         };
     }
 
     return hr_dt;
 }
 
 void
 pcmk__time_set_hr_dt(crm_time_t *target, pcmk__time_hr_t *hr_dt)
 {
     CRM_ASSERT((hr_dt) && (target));
     *target = (crm_time_t) {
         .years = hr_dt->years,
         .months = hr_dt->months,
         .days = hr_dt->days,
         .seconds = hr_dt->seconds,
         .offset = hr_dt->offset,
         .duration = hr_dt->duration
     };
 }
 
 /*!
  * \internal
  * \brief Return the current time as a high-resolution time
  *
  * \param[out] epoch  If not NULL, this will be set to seconds since epoch
  *
  * \return Newly allocated high-resolution time set to the current time
  */
 pcmk__time_hr_t *
 pcmk__time_hr_now(time_t *epoch)
 {
     struct timespec tv;
     crm_time_t dt;
     pcmk__time_hr_t *hr;
 
     qb_util_timespec_from_epoch_get(&tv);
     if (epoch != NULL) {
         *epoch = tv.tv_sec;
     }
     crm_time_set_timet(&dt, &(tv.tv_sec));
     hr = pcmk__time_hr_convert(NULL, &dt);
     if (hr != NULL) {
         hr->useconds = tv.tv_nsec / QB_TIME_NS_IN_USEC;
     }
     return hr;
 }
 
 pcmk__time_hr_t *
 pcmk__time_hr_new(const char *date_time)
 {
     pcmk__time_hr_t *hr_dt = NULL;
 
     if (date_time == NULL) {
         hr_dt = pcmk__time_hr_now(NULL);
     } else {
         crm_time_t *dt;
 
         dt = parse_date(date_time);
         hr_dt = pcmk__time_hr_convert(NULL, dt);
         crm_time_free(dt);
     }
     return hr_dt;
 }
 
 void
 pcmk__time_hr_free(pcmk__time_hr_t * hr_dt)
 {
     free(hr_dt);
 }
 
 char *
 pcmk__time_format_hr(const char *format, pcmk__time_hr_t * hr_dt)
 {
     const char *mark_s;
     int max = 128, scanned_pos = 0, printed_pos = 0, fmt_pos = 0,
         date_len = 0, nano_digits = 0;
     char nano_s[10], date_s[max+1], nanofmt_s[5] = "%", *tmp_fmt_s;
     struct tm tm;
     crm_time_t dt;
 
     if (!format) {
         return NULL;
     }
     pcmk__time_set_hr_dt(&dt, hr_dt);
     ha_get_tm_time(&tm, &dt);
     sprintf(nano_s, "%06d000", hr_dt->useconds);
 
     while ((format[scanned_pos]) != '\0') {
         mark_s = strchr(&format[scanned_pos], '%');
         if (mark_s) {
             int fmt_len = 1;
 
             fmt_pos = mark_s - format;
             while ((format[fmt_pos+fmt_len] != '\0') &&
                 (format[fmt_pos+fmt_len] >= '0') &&
                 (format[fmt_pos+fmt_len] <= '9')) {
                 fmt_len++;
             }
             scanned_pos = fmt_pos + fmt_len + 1;
             if (format[fmt_pos+fmt_len] == 'N') {
                 nano_digits = atoi(&format[fmt_pos+1]);
                 nano_digits = (nano_digits > 6)?6:nano_digits;
                 nano_digits = (nano_digits < 0)?0:nano_digits;
                 sprintf(&nanofmt_s[1], ".%ds", nano_digits);
             } else {
                 if (format[scanned_pos] != '\0') {
                     continue;
                 }
                 fmt_pos = scanned_pos; /* print till end */
             }
         } else {
             scanned_pos = strlen(format);
             fmt_pos = scanned_pos; /* print till end */
         }
         tmp_fmt_s = strndup(&format[printed_pos], fmt_pos - printed_pos);
 #ifdef GCC_FORMAT_NONLITERAL_CHECKING_ENABLED
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
 #endif
         date_len += strftime(&date_s[date_len], max-date_len, tmp_fmt_s, &tm);
 #ifdef GCC_FORMAT_NONLITERAL_CHECKING_ENABLED
 #pragma GCC diagnostic pop
 #endif
         printed_pos = scanned_pos;
         free(tmp_fmt_s);
         if (nano_digits) {
 #ifdef GCC_FORMAT_NONLITERAL_CHECKING_ENABLED
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Wformat-nonliteral"
 #endif
             date_len += snprintf(&date_s[date_len], max-date_len,
                                  nanofmt_s, nano_s);
 #ifdef GCC_FORMAT_NONLITERAL_CHECKING_ENABLED
 #pragma GCC diagnostic pop
 #endif
             nano_digits = 0;
         }
     }
 
     return (date_len == 0)?NULL:strdup(date_s);
 }
 
 /*!
  * \internal
  * \brief Return human-friendly string corresponding to a time
  *
  * \param[in] when   Pointer to epoch time value (or NULL for current time)
  *
  * \return Current time as string (as by ctime() but without newline) on
  *         success, NULL otherwise
  * \note The return value points to a statically allocated string which might be
  *       overwritten by subsequent calls to any of the C library date and time
  *       functions.
  */
 const char *
 pcmk__epoch2str(time_t *when)
 {
     char *since_epoch = NULL;
 
     if (when == NULL) {
         time_t a_time = time(NULL);
 
         if (a_time == (time_t) -1) {
             return NULL;
         } else {
             since_epoch = ctime(&a_time);
         }
     } else {
         since_epoch = ctime(when);
     }
 
     if (since_epoch == NULL) {
         return NULL;
     } else {
         return pcmk__trim(since_epoch);
     }
 }
 
 /*!
  * \internal
  * \brief Given a millisecond interval, return a log-friendly string
  *
  * \param[in] interval_ms  Interval in milliseconds
  *
  * \return Readable version of \p interval_ms
  *
  * \note The return value is a pointer to static memory that will be
  *       overwritten by later calls to this function.
  */
 const char *
 pcmk__readable_interval(guint interval_ms)
 {
 #define MS_IN_S (1000)
 #define MS_IN_M (MS_IN_S * 60)
 #define MS_IN_H (MS_IN_M * 60)
 #define MS_IN_D (MS_IN_H * 24)
 #define MAXSTR sizeof("..d..h..m..s...ms")
     static char str[MAXSTR] = { '\0', };
     int offset = 0;
 
     if (interval_ms > MS_IN_D) {
         offset += snprintf(str + offset, MAXSTR - offset, "%ud",
                            interval_ms / MS_IN_D);
         interval_ms -= (interval_ms / MS_IN_D) * MS_IN_D;
     }
     if (interval_ms > MS_IN_H) {
         offset += snprintf(str + offset, MAXSTR - offset, "%uh",
                            interval_ms / MS_IN_H);
         interval_ms -= (interval_ms / MS_IN_H) * MS_IN_H;
     }
     if (interval_ms > MS_IN_M) {
         offset += snprintf(str + offset, MAXSTR - offset, "%um",
                            interval_ms / MS_IN_M);
         interval_ms -= (interval_ms / MS_IN_M) * MS_IN_M;
     }
 
     // Ns, N.NNNs, or NNNms
     if (interval_ms > MS_IN_S) {
         offset += snprintf(str + offset, MAXSTR - offset, "%u",
                            interval_ms / MS_IN_S);
         interval_ms -= (interval_ms / MS_IN_S) * MS_IN_S;
         if (interval_ms > 0) {
             offset += snprintf(str + offset, MAXSTR - offset, ".%03u",
                                interval_ms);
         }
         (void) snprintf(str + offset, MAXSTR - offset, "s");
 
     } else if (interval_ms > 0) {
         (void) snprintf(str + offset, MAXSTR - offset, "%ums", interval_ms);
 
     } else if (str[0] == '\0') {
         strcpy(str, "0s");
     }
     return str;
 }