]> git.ipfire.org Git - thirdparty/ntp.git/commitdiff
[Bug 2888] streamline calendar functions.
authorJuergen Perlinger <perlinger@ntp.org>
Mon, 3 Aug 2015 18:34:01 +0000 (20:34 +0200)
committerJuergen Perlinger <perlinger@ntp.org>
Mon, 3 Aug 2015 18:34:01 +0000 (20:34 +0200)
bk: 55bfb419goVzMarlNiJhByxwkYd6mA

ChangeLog
include/ntp_calendar.h
libntp/ntp_calendar.c
tests/libntp/calendar.c
tests/libntp/run-calendar.c

index 7b6f46d4f130f754846cc2b56ee1db8b6be22e36..9a0360844b21373cb58c0c91c3072081ddffbc5c 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,4 @@
 ---
-
 * [Bug 2823] ntpsweep with recursive peers option doesn't work.  H.Stenn.
 * [Bug 2849] Systems with more than one default route may never
   synchronize.  Brian Utterback.  Note that this patch might need to
@@ -12,6 +11,7 @@
 * [Bug 2875] sntp/Makefile.am: Get rid of DIST_SUBDIRS.  libevent must
   be configured for the distribution targets.  Harlan Stenn.
 * [Bug 2883] ntpd crashes on exit with empty driftfile.  Miroslav Lichvar.
+* [Bug 2888] streamline calendar functions.  perlinger@ntp.org
 * libntp/emalloc.c: Remove explicit include of stdint.h.  Harlan Stenn.
 * Put Unity CPPFLAGS items in unity_config.h.  Harlan Stenn.
 * tests/ntpd/g_leapsec.cpp typo fix.  Harlan Stenn.
index 3afb627d25944bd5999b095fbad8fc4f64638ef5..6f36c0777aba6dce02b7303fbae5d13c96890aff 100644 (file)
@@ -157,6 +157,12 @@ ntpcal_daysplit(const vint64 *);
 extern vint64
 ntpcal_dayjoin(int32_t /* days */, int32_t /* seconds */);
 
+/* Get the number of leap years since epoch for the number of elapsed
+ * full years
+ */
+extern int32_t
+ntpcal_leapyears_in_years(int32_t /* years */);
+
 /*
  * Convert elapsed years in Era into elapsed days in Era.
  */
@@ -220,6 +226,9 @@ ntpcal_date_to_rd(const struct calendar * /* jt */);
  *
  * if 'isleapyear' is not NULL, it will receive an integer that is 0
  * for regular years and a non-zero value for leap years.
+ *
+ * The input is limited to [-2^30, 2^30-1]. If the days exceed this
+ * range, errno is set to EDOM and the result is saturated.
  */
 extern ntpcal_split
 ntpcal_split_eradays(int32_t /* days */, int/*BOOL*/ * /* isleapyear */);
@@ -330,6 +339,10 @@ ntpcal_date_to_time(const struct calendar * /* jd */);
 extern int32_t
 isocal_weeks_in_years(int32_t  /* years */);
 
+/*
+ * The input is limited to [-2^30, 2^30-1]. If the weeks exceed this
+ * range, errno is set to EDOM and the result is saturated.
+ */
 extern ntpcal_split
 isocal_split_eraweeks(int32_t /* weeks */);
 
index ff91fcfc678d7a672b96a066d10a53de854feac3..c543a00ad484c5766a8ff1fcc383b0e18f8b59e7 100644 (file)
@@ -3,7 +3,55 @@
  *
  * Written by Juergen Perlinger (perlinger@ntp.org) for the NTP project.
  * The contents of 'html/copyright.html' apply.
+ *
+ * --------------------------------------------------------------------
+ * Some notes on the implementation:
+ *
+ * Calendar algorithms thrive on the division operation, which is one of
+ * the slowest numerical operations in any CPU. What saves us here from
+ * abysmal performance is the fact that all divisions are divisions by
+ * constant numbers, and most compilers can do this by a multiplication
+ * operation.  But this might not work when using the div/ldiv/lldiv
+ * function family, because many compilers are not able to do inline
+ * expansion of the code with following optimisation for the
+ * constant-divider case.
+ *
+ * Also div/ldiv/lldiv are defined in terms of int/long/longlong, which
+ * are inherently target dependent. Nothing that could not be cured with
+ * autoconf, but still a mess...
+ *
+ * Furthermore, we need floor division in many places. C either leaves
+ * the division behaviour undefined (< C99) or demands truncation to
+ * zero (>= C99), so additional steps are required to make sure the
+ * algorithms work. The {l,ll}div function family is requested to
+ * truncate towards zero, which is also the wrong direction for our
+ * purpose.
+ *
+ * For all this, all divisions by constant are coded manually, even when
+ * there is a joined div/mod operation: The optimiser should sort that
+ * out, if possible. Most of the calculations are done with unsigned
+ * types, explicitely using two's complement arithmetics where
+ * necessary. This minimises the dependecies to compiler and target,
+ * while still giving reasonable to good performance.
+ *
+ * The implementation uses a few tricks that exploit properties of the
+ * two's complement: Floor division on negative dividents can be
+ * executed by using the one's complement of the divident. One's
+ * complement can be easily created using XOR and a mask.
+ *
+ * Finally, check for overflow conditions is minimal. There are only two
+ * calculation steps in the whole calendar that suffer from an internal
+ * overflow, and these conditions are checked: errno is set to EDOM and
+ * the results are clamped/saturated in this case.  All other functions
+ * do not suffer from internal overflow and simply return the result
+ * truncated to 32 bits.
+ *
+ * This is a sacrifice made for execution speed.  Since a 32-bit day
+ * counter covers +/- 5,879,610 years and the clamp limits the effective
+ * range to +/-2.9 million years, this should not pose a problem here.
+ *
  */
+
 #include <config.h>
 #include <sys/types.h>
 
 #include "ntp_fp.h"
 #include "ntp_unixtime.h"
 
+/* For now, let's take the conservative approach: if the target property
+ * macros are not defined, check a few well-known compiler/architecture
+ * settings. Default is to assume that the representation of signed
+ * integers is unknown and shift-arithmetic-right is not available.
+ */
+#ifndef TARGET_HAS_2CPL
+# if defined(__GNUC__)
+#  if defined(__i386__) || defined(__x86_64__) || defined(__arm__)
+#   define TARGET_HAS_2CPL 1
+#  else
+#   define TARGET_HAS_2CPL 0
+#  endif
+# elif defined(_MSC_VER)
+#  if defined(_M_IX86) || defined(_M_X64) || defined(_M_ARM)
+#   define TARGET_HAS_2CPL 1
+#  else
+#   define TARGET_HAS_2CPL 0
+#  endif
+# endif
+#endif
+
+#ifndef TARGET_HAS_SAR
+# define TARGET_HAS_SAR 0
+#endif
+
 /*
  *---------------------------------------------------------------------
  * replacing the 'time()' function
@@ -45,6 +118,117 @@ now(void)
        return (*systime_func)(NULL);
 }
 
+/*
+ *---------------------------------------------------------------------
+ * Get sign extension mask and unsigned 2cpl rep for a signed integer
+ *---------------------------------------------------------------------
+ */
+
+static inline uint32_t
+int32_sflag(
+       const int32_t v)
+{
+#   if TARGET_HAS_2CPL && TARGET_HAS_SAR && SIZEOF_INT >= 4
+
+       /* Let's assume that shift is the fastest way to get the sign
+        * extension of of a signed integer. This might not always be
+        * true, though -- On 8bit CPUs or machines without barrel
+        * shifter this will kill the performance. So we make sure
+        * we do this only if 'int' has at least 4 bytes.
+        */
+       return (uint32_t)(v >> 31);
+       
+#   else
+
+       /* This should be a rather generic approach for getting a sign
+        * extension mask...
+        */
+       return UINT32_C(0) - (uint32_t)(v < 0);
+       
+#   endif
+}
+
+static inline uint32_t
+int32_to_uint32_2cpl(
+       const int32_t v)
+{
+       uint32_t vu;
+       
+#   if TARGET_HAS_2CPL
+
+       /* Just copy through the 32 bits from the signed value if we're
+        * on a two's complement target.
+        */
+       vu = (uint32_t)v;
+       
+#   else
+
+       /* Convert from signed int to unsigned int two's complement. Do
+        * not make any assumptions about the representation of signed
+        * integers, but make sure signed integer overflow cannot happen
+        * here. A compiler on a two's complement target *might* find
+        * out that this is just a complicated cast (as above), but your
+        * mileage might vary.
+        */
+       if (v < 0)
+               vu = ~(uint32_t)(-(v + 1));
+       else
+               vu = (uint32_t)v;
+       
+#   endif
+       
+       return vu;
+}
+
+static inline int32_t
+uint32_2cpl_to_int32(
+       const uint32_t vu)
+{
+       int32_t v;
+       
+#   if TARGET_HAS_2CPL
+
+       /* Just copy through the 32 bits from the unsigned value if
+        * we're on a two's complement target.
+        */
+       v = (int32_t)vu;
+
+#   else
+
+       /* Convert to signed integer, making sure signed integer
+        * overflow cannot happen. Again, the optimiser might or might
+        * not find out that this is just a copy of 32 bits on a target
+        * with two's complement representation for signed integers.
+        */
+       if (vu > INT32_MAX)
+               v = -(int32_t)(~vu) - 1;
+       else
+               v = (int32_t)vu;
+       
+#   endif
+       
+       return v;
+}
+
+/* Some of the calculations need to multiply the input by 4 before doing
+ * a division. This can cause overflow and strange results. Therefore we
+ * clamp / saturate the input operand. And since we do the calculations
+ * in unsigned int with an extra sign flag/mask, we only loose one bit
+ * of the input value range.
+ */
+static inline uint32_t
+uint32_saturate(
+       uint32_t vu,
+       uint32_t mu)
+{
+       static const uint32_t limit = UINT32_MAX/4u;
+       if ((mu ^ vu) > limit) {
+               vu    = mu ^ limit;
+               errno = EDOM;
+       }
+       return vu;
+}
+
 /*
  *---------------------------------------------------------------------
  * Convert between 'time_t' and 'vint64'
@@ -60,7 +244,7 @@ time_to_vint64(
 
        tt = *ptt;
 
-#if SIZEOF_TIME_T <= 4
+#   if SIZEOF_TIME_T <= 4
 
        res.D_s.hi = 0;
        if (tt < 0) {
@@ -70,11 +254,11 @@ time_to_vint64(
                res.D_s.lo = (uint32_t)tt;
        }
 
-#elif defined(HAVE_INT64)
+#   elif defined(HAVE_INT64)
 
        res.q_s = tt;
 
-#else
+#   else
        /*
         * shifting negative signed quantities is compiler-dependent, so
         * we better avoid it and do it all manually. And shifting more
@@ -90,7 +274,7 @@ time_to_vint64(
                res.D_s.hi = (uint32_t)(tt >> 32);
        }
 
-#endif
+#   endif
 
        return res;
 }
@@ -103,19 +287,19 @@ vint64_to_time(
 {
        time_t res;
 
-#if SIZEOF_TIME_T <= 4
+#   if SIZEOF_TIME_T <= 4
 
        res = (time_t)tv->D_s.lo;
 
-#elif defined(HAVE_INT64)
+#   elif defined(HAVE_INT64)
 
        res = (time_t)tv->q_s;
 
-#else
+#   else
 
        res = ((time_t)tv->d_s.hi << 32) | tv->D_s.lo;
 
-#endif
+#   endif
 
        return res;
 }
@@ -153,11 +337,11 @@ ntpcal_get_build_date(
         * problem.
         *
         */
-#ifdef MKREPRO_DATE
+#   ifdef MKREPRO_DATE
        static const char build[] = MKREPRO_TIME "/" MKREPRO_DATE;
-#else
+#   else
        static const char build[] = __TIME__ "/" __DATE__;
-#endif
+#   endif
        static const char mlist[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
 
        char              monstr[4];
@@ -167,16 +351,16 @@ ntpcal_get_build_date(
         * so using 'uint16_t' is contra-indicated!
         */
 
-#ifdef DEBUG
+#   ifdef DEBUG
        static int        ignore  = 0;
-#endif
+#   endif
 
        ZERO(*jd);
        jd->year     = 1970;
        jd->month    = 1;
        jd->monthday = 1;
 
-#ifdef DEBUG
+#   ifdef DEBUG
        /* check environment if build date should be ignored */
        if (0 == ignore) {
            const char * envstr;
@@ -185,7 +369,7 @@ ntpcal_get_build_date(
        }
        if (ignore > 1)
            return FALSE;
-#endif
+#   endif
 
        if (6 == sscanf(build, "%hu:%hu:%hu/%3s %hu %hu",
                        &hour, &minute, &second, monstr, &day, &year)) {
@@ -254,37 +438,8 @@ static const uint16_t real_month_table[2][13] = {
  * (day number).  This is the number of days elapsed since 0000-12-31
  * in the proleptic Gregorian calendar. The begin of the Christian Era
  * (0001-01-01) is RD(1).
- *
- *
- * Some notes on the implementation:
- *
- * Calendar algorithms thrive on the division operation, which is one of
- * the slowest numerical operations in any CPU. What saves us here from
- * abysmal performance is the fact that all divisions are divisions by
- * constant numbers, and most compilers can do this by a multiplication
- * operation.  But this might not work when using the div/ldiv/lldiv
- * function family, because many compilers are not able to do inline
- * expansion of the code with following optimisation for the
- * constant-divider case.
- *
- * Also div/ldiv/lldiv are defined in terms of int/long/longlong, which
- * are inherently target dependent. Nothing that could not be cured with
- * autoconf, but still a mess...
- *
- * Furthermore, we need floor division while C demands truncation to
- * zero, so additional steps are required to make sure the algorithms
- * work.
- *
- * For all this, all divisions by constant are coded manually, even when
- * there is a joined div/mod operation: The optimiser should sort that
- * out, if possible.
- *
- * Finally, the functions do not check for overflow conditions. This
- * is a sacrifice made for execution speed; since a 32-bit day counter
- * covers +/- 5,879,610 years, this should not pose a problem here.
  */
 
-
 /*
  * ==================================================================
  *
@@ -363,22 +518,23 @@ ntpcal_periodic_extend(
                 * Get absolute difference as unsigned quantity and
                 * the complement flag. This is done by always
                 * subtracting the smaller value from the bigger
-                * one. This implementation works only on a two's
-                * complement machine!
+                * one.
                 */
                if (value >= pivot) {
-                       diff = (uint32_t)value - (uint32_t)pivot;
+                       diff = int32_to_uint32_2cpl(value)
+                            - int32_to_uint32_2cpl(pivot);
                } else {
-                       diff = (uint32_t)pivot - (uint32_t)value;
+                       diff = int32_to_uint32_2cpl(pivot)
+                            - int32_to_uint32_2cpl(value);
                        cpl ^= 1;
                }
                diff %= (uint32_t)cycle;
                if (diff) {
                        if (cpl)
-                               diff = cycle - diff;
+                               diff = (uint32_t)cycle - diff;
                        if (neg)
                                diff = ~diff + 1;
-                       pivot += diff;
+                       pivot += uint32_2cpl_to_int32(diff);
                }
        }
        return pivot;
@@ -405,7 +561,7 @@ ntpcal_ntp_to_time(
 {
        vint64 res;
 
-#ifdef HAVE_INT64
+#   if defined(HAVE_INT64)
 
        res.q_s = (pivot != NULL)
                      ? *pivot
@@ -415,7 +571,7 @@ ntpcal_ntp_to_time(
        ntp     -= res.D_s.lo;          /* cycle difference      */
        res.Q_s += (uint64_t)ntp;       /* get expanded time     */
 
-#else /* no 64bit scalars */
+#   else /* no 64bit scalars */
 
        time_t tmp;
 
@@ -428,7 +584,7 @@ ntpcal_ntp_to_time(
        ntp -= res.D_s.lo;              /* cycle difference      */
        M_ADD(res.D_s.hi, res.D_s.lo, 0, ntp);
 
-#endif /* no 64bit scalars */
+#   endif /* no 64bit scalars */
 
        return res;
 }
@@ -454,7 +610,7 @@ ntpcal_ntp_to_ntp(
 {
        vint64 res;
 
-#ifdef HAVE_INT64
+#   if defined(HAVE_INT64)
 
        res.q_s = (pivot)
                      ? *pivot
@@ -464,7 +620,7 @@ ntpcal_ntp_to_ntp(
        ntp     -= res.D_s.lo;          /* cycle difference      */
        res.Q_s += (uint64_t)ntp;       /* get expanded time     */
 
-#else /* no 64bit scalars */
+#   else /* no 64bit scalars */
 
        time_t tmp;
 
@@ -477,7 +633,7 @@ ntpcal_ntp_to_ntp(
        ntp -= res.D_s.lo;              /* cycle difference      */
        M_ADD(res.D_s.hi, res.D_s.lo, 0, ntp);
 
-#endif /* no 64bit scalars */
+#   endif /* no 64bit scalars */
 
        return res;
 }
@@ -505,78 +661,75 @@ ntpcal_daysplit(
        )
 {
        ntpcal_split res;
+       uint32_t Q;
+
+#   if defined(HAVE_INT64)
+       
+       /* Manual floor division by SECSPERDAY. This uses the one's
+        * complement trick, too, but without an extra flag value: The
+        * flag would be 64bit, and that's a bit of overkill on a 32bit
+        * target that has to use a register pair for a 64bit number.
+        */
+       if (ts->q_s < 0)
+               Q = ~(uint32_t)(~ts->Q_s / SECSPERDAY);
+       else
+               Q = (uint32_t)(ts->Q_s / SECSPERDAY);
 
-#ifdef HAVE_INT64
+#   else
 
-       /* manual floor division by SECSPERDAY */
-       res.hi = (int32_t)(ts->q_s / SECSPERDAY);
-       res.lo = (int32_t)(ts->q_s % SECSPERDAY);
-       if (res.lo < 0) {
-               res.hi -= 1;
-               res.lo += SECSPERDAY;
-       }
+       uint32_t ah, al, sflag, A;
 
-#else
+       /* get operand into ah/al (either ts or ts' one's complement,
+        * for later floor division)
+        */
+       sflag = int32_sflag(ts->d_s.hi);
+       ah = sflag ^ ts->D_s.hi;
+       al = sflag ^ ts->D_s.lo;
+
+       /* Since 86400 == 128*675 we can drop the least 7 bits and
+        * divide by 675 instead of 86400. Then the maximum remainder
+        * after each devision step is 674, and we need 10 bits for
+        * that. So in the next step we can shift in 22 bits from the
+        * numerator.
+        *
+        * Therefore we load the accu with the top 13 bits (51..63) in
+        * the first shot. We don't have to remember the quotient -- it
+        * would be shifted out anyway.
+        */
+       A = ah >> 19;
+       if (A >= 675)
+               A = (A % 675u);
+
+       /* Now assemble the remainder with bits 29..50 from the
+        * numerator and divide. This creates the upper ten bits of the
+        * quotient. (Well, the top 22 bits of a 44bit result. But that
+        * will be truncated to 32 bits anyway.)
+        */
+       A = (A << 19) | (ah & 0x0007FFFFu);
+       A = (A <<  3) | (al >> 29);
+       Q = A / 675u;
+       A = A % 675u;
 
-       /*
-        * since we do not have 64bit ops, we have to this by hand.
-        * Luckily SECSPERDAY is 86400 is 675*128, so we do the division
-        * using chained 32/16 bit divisions and shifts.
+       /* Now assemble the remainder with bits 7..28 from the numerator
+        * and do a final division step.
         */
-       vint64   op;
-       uint32_t q, r, a;
-       int      isneg;
+       A = (A << 22) | ((al >> 7) & 0x003FFFFFu);
+       Q = (Q << 22) | (A / 675u);
 
-       memcpy(&op, ts, sizeof(op));
-       /* fix sign */
-       isneg = M_ISNEG(op.D_s.hi);
-       if (isneg)
-               M_NEG(op.D_s.hi, op.D_s.lo);
-
-       /* save remainder of DIV 128, shift for divide */
-       r  = op.D_s.lo & 127; /* save remainder bits */
-       op.D_s.lo = (op.D_s.lo >> 7) | (op.D_s.hi << 25);
-       op.D_s.hi = (op.D_s.hi >> 7);
-
-       /* now do a mnual division, trying to remove as many ops as
-        * possible -- division is always slow! An since we do not have
-        * the advantage of a specific 64/32 bit or even a specific 32/16
-        * bit division op, but must use the general 32/32bit division
-        * even if we *know* the divider fits into unsigned 16 bits, the
-        * exra code pathes should pay off.
+       /* The last 7 bits get simply dropped, as they have no affect on
+        * the quotient when dividing by 86400.
         */
-       a = op.D_s.hi;
-       if (a > 675u)
-               a = a % 675u;
-       if (a) {
-               a = (a << 16) | op.W_s.lh;
-               q = a / 675u;
-               a = a % 675u;
-
-               a = (a << 16) | op.W_s.ll;
-               q = (q << 16) | (a / 675u);
-       } else {
-               a = op.D_s.lo;
-               q = a / 675u;
-       }
-       a = a % 675u;
-
-       /* assemble remainder */
-       r |= a << 7;
-
-       /* fix sign of result */
-       if (isneg) {
-               if (r) {
-                       r = SECSPERDAY - r;
-                       q = ~q;
-               } else
-                       q = ~q + 1;
-       }
 
-       res.hi = q;
-       res.lo = r;
+       /* apply sign correction and calculate the true floor
+        * remainder.
+        */
+       Q ^= sflag;
+       
+#   endif
+       
+       res.hi = uint32_2cpl_to_int32(Q);
+       res.lo = ts->D_s.lo - Q * SECSPERDAY;
 
-#endif
        return res;
 }
 
@@ -593,25 +746,28 @@ priv_timesplit(
        int32_t ts
        )
 {
-       int32_t days = 0;
-
-       /* make sure we have a positive offset into a day */
-       if (ts < 0 || ts >= SECSPERDAY) {
-               days = ts / SECSPERDAY;
-               ts   = ts % SECSPERDAY;
-               if (ts < 0) {
-                       days -= 1;
-                       ts   += SECSPERDAY;
-               }
-       }
+       /* Do 3 chained floor divisions by positive constants, using the
+        * one's complement trick and factoring out the intermediate XOR
+        * ops to reduce the number of operations.
+        */
+       uint32_t us, um, uh, ud, sflag;
 
-       /* get secs, mins, hours */
-       split[2] = (uint8_t)(ts % SECSPERMIN);
-       ts /= SECSPERMIN;
-       split[1] = (uint8_t)(ts % MINSPERHR);
-       split[0] = (uint8_t)(ts / MINSPERHR);
+       sflag = int32_sflag(ts);
+       us    = int32_to_uint32_2cpl(ts);
 
-       return days;
+       um = (sflag ^ us) / SECSPERMIN;
+       uh = um / MINSPERHR;
+       ud = uh / HRSPERDAY;
+
+       um ^= sflag;
+       uh ^= sflag;
+       ud ^= sflag;
+
+       split[0] = (int32_t)(uh - ud * HRSPERDAY );
+       split[1] = (int32_t)(um - uh * MINSPERHR );
+       split[2] = (int32_t)(us - um * SECSPERMIN);
+       
+       return uint32_2cpl_to_int32(ud);
 }
 
 /*
@@ -630,46 +786,45 @@ ntpcal_split_eradays(
        int  *isleapyear
        )
 {
-       ntpcal_split res;
-       int32_t      n400, n100, n004, n001, yday; /* calendar year cycles */
-
-       /*
-        * Split off calendar cycles, using floor division in the first
-        * step. After that first step, simple division does it because
-        * all operands are positive; alas, we have to be aware of the
-        * possibe cycle overflows for 100 years and 1 year, caused by
-        * the additional leap day.
+       /* Use the fast cyclesplit algorithm here, to calculate the
+        * centuries and years in a century with one division each. This
+        * reduces the number of division operations to two, but is
+        * susceptible to internal range overflow. We make sure the
+        * input operands are in the safe range; this still gives us
+        * approx +/-2.9 million years.
         */
-       n400 = days / GREGORIAN_CYCLE_DAYS;
-       yday = days % GREGORIAN_CYCLE_DAYS;
-       if (yday < 0) {
-               n400 -= 1;
-               yday += GREGORIAN_CYCLE_DAYS;
-       }
-       n100 = yday / GREGORIAN_NORMAL_CENTURY_DAYS;
-       yday = yday % GREGORIAN_NORMAL_CENTURY_DAYS;
-       n004 = yday / GREGORIAN_NORMAL_LEAP_CYCLE_DAYS;
-       yday = yday % GREGORIAN_NORMAL_LEAP_CYCLE_DAYS;
-       n001 = yday / DAYSPERYEAR;
-       yday = yday % DAYSPERYEAR;
-
-       /*
-        * check for leap cycle overflows and calculate the leap flag
-        * if needed
+       ntpcal_split res;
+       int32_t  n100, n001; /* calendar year cycles */
+       uint32_t uday, Q, sflag;
+
+       /* split off centuries first */
+       sflag = int32_sflag(days);
+       uday  = uint32_saturate(int32_to_uint32_2cpl(days), sflag);
+       uday  = (4u * uday) | 3u;
+       Q    = sflag ^ ((sflag ^ uday) / GREGORIAN_CYCLE_DAYS);
+       uday = uday - Q * GREGORIAN_CYCLE_DAYS;
+       n100 = uint32_2cpl_to_int32(Q);
+       
+       /* Split off years in century -- days >= 0 here, and we're far
+        * away from integer overflow trouble now. */
+       uday |= 3;
+       n001 = uday / GREGORIAN_NORMAL_LEAP_CYCLE_DAYS;
+       uday = uday % GREGORIAN_NORMAL_LEAP_CYCLE_DAYS;
+
+       /* Assemble the year and day in year */
+       res.hi = n100 * 100 + n001;
+       res.lo = uday / 4u;
+
+       /* Eventually set the leap year flag. Note: 0 <= n001 <= 99 and
+        * Q is still the two's complement representation of the
+        * centuries: The modulo 4 ops can be done with masking here.
+        * We also shift the year and the century by one, so the tests
+        * can be done against zero instead of 3.
         */
-       if ((n001 | n100) > 3) {
-               /* hit last day of leap year */
-               n001 -= 1;
-               yday += DAYSPERYEAR;
-               if (isleapyear)
-                       *isleapyear = 1;
-       } else if (isleapyear)
-               *isleapyear = (n001 == 3) && ((n004 != 24) || (n100 == 3));
-
-       /* now merge the cycles to elapsed years, using horner scheme */
-       res.hi = ((4*n400 + n100)*25 + n004)*4 + n001;
-       res.lo = yday;
-
+       if (isleapyear)
+               *isleapyear = !((n001+1) & 3)
+                   && ((n001 != 99) || !((Q+1) & 3));
+       
        return res;
 }
 
@@ -719,11 +874,9 @@ ntpcal_rd_to_date(
        )
 {
        ntpcal_split split;
-       int          leaps;
-       int          retv;
+       int          leapy;
+       uint         ymask;
 
-       leaps = 0;
-       retv = 0;
        /* Get day-of-week first. Since rd is signed, the remainder can
         * be in the range [-6..+6], but the assignment to an unsigned
         * variable maps the negative values to positive values >=7.
@@ -731,26 +884,28 @@ ntpcal_rd_to_date(
         * causes the needed wrap-around into the desired value range of
         * zero to six, both inclusive.
         */
-       jd->weekday = rd % 7;
-       if (jd->weekday >= 7)   /* unsigned! */
-               jd->weekday += 7;
-
-       split = ntpcal_split_eradays(rd - 1, &leaps);
-       retv  = leaps;
-       /* get year and day-of-year */
-       jd->year = (uint16_t)split.hi + 1;
-       if (jd->year != split.hi + 1) {
-               jd->year = 0;
-               retv     = -1;  /* bletch. overflow trouble. */
-       }
+       jd->weekday = rd % DAYSPERWEEK;
+       if (jd->weekday >= DAYSPERWEEK) /* weekday is unsigned! */
+               jd->weekday += DAYSPERWEEK;
+
+       split = ntpcal_split_eradays(rd - 1, &leapy);
+       /* Get year and day-of-year, with overflow check. If any of the
+        * upper 16 bits is set after shifting to unity-based years, we
+        * will have an overflow when converting to an unsigned 16bit
+        * year. Shifting to the right is OK here, since it does not
+        * matter if the shift is logic or arithmetic.
+        */
+       split.hi += 1;
+       ymask = 0u - ((split.hi >> 16) == 0);
+       jd->year = (uint16_t)(split.hi & ymask);
        jd->yearday = (uint16_t)split.lo + 1;
 
        /* convert to month and mday */
-       split = ntpcal_split_yeardays(split.lo, leaps);
+       split = ntpcal_split_yeardays(split.lo, leapy);
        jd->month    = (uint8_t)split.hi + 1;
        jd->monthday = (uint8_t)split.lo + 1;
 
-       return retv ? retv : leaps;
+       return ymask ? leapy : -1;
 }
 
 /*
@@ -765,25 +920,24 @@ ntpcal_rd_to_tm(
        )
 {
        ntpcal_split split;
-       int          leaps;
+       int          leapy;
 
-       leaps = 0;
        /* get day-of-week first */
-       utm->tm_wday = rd % 7;
+       utm->tm_wday = rd % DAYSPERWEEK;
        if (utm->tm_wday < 0)
-               utm->tm_wday += 7;
+               utm->tm_wday += DAYSPERWEEK;
 
        /* get year and day-of-year */
-       split = ntpcal_split_eradays(rd - 1, &leaps);
+       split = ntpcal_split_eradays(rd - 1, &leapy);
        utm->tm_year = split.hi - 1899;
        utm->tm_yday = split.lo;        /* 0-based */
 
        /* convert to month and mday */
-       split = ntpcal_split_yeardays(split.lo, leaps);
+       split = ntpcal_split_yeardays(split.lo, leapy);
        utm->tm_mon  = split.hi;        /* 0-based */
        utm->tm_mday = split.lo + 1;    /* 1-based */
 
-       return leaps;
+       return leapy;
 }
 
 /*
@@ -918,13 +1072,13 @@ ntpcal_dayjoin(
 {
        vint64 res;
 
-#ifdef HAVE_INT64
+#   if defined(HAVE_INT64)
 
        res.q_s  = days;
        res.q_s *= SECSPERDAY;
        res.q_s += secs;
 
-#else
+#   else
 
        uint32_t p1, p2;
        int      isneg;
@@ -963,47 +1117,57 @@ ntpcal_dayjoin(
        }
        M_ADD(res.D_s.hi, res.D_s.lo, p2, p1);
 
-#endif
+#   endif
 
        return res;
 }
 
 /*
  *---------------------------------------------------------------------
- * Convert elapsed years in Era into elapsed days in Era.
- *
- * To accomodate for negative values of years, floor division would be
- * required for all division operations. This can be eased by first
- * splitting the years into full 400-year cycles and years in the
- * cycle. Only this operation must be coded as a full floor division; as
- * the years in the cycle is a non-negative number, all other divisions
- * can be regular truncated divisions.
+ * get leap years since epoch in elapsed years
  *---------------------------------------------------------------------
  */
 int32_t
-ntpcal_days_in_years(
+ntpcal_leapyears_in_years(
        int32_t years
        )
 {
-       int32_t cycle; /* full gregorian cycle */
-
-       /* split off full calendar cycles, using floor division */
-       cycle = years / 400;
-       years = years % 400;
-       if (years < 0) {
-               cycle -= 1;
-               years += 400;
-       }
+       /* We use the in-out-in algorithm here, using the one's
+        * complement division trick for negative numbers. The chained
+        * division sequence by 4/25/4 gives the compiler the chance to
+        * get away with only one true division and doing shifts otherwise.
+        */
 
-       /*
-        * Calculate days in cycle. years now is a non-negative number,
-        * holding the number of years in the 400-year cycle.
+       uint32_t sflag, sum, uyear;
+
+       sflag = int32_sflag(years);
+       uyear = int32_to_uint32_2cpl(years);
+       uyear ^= sflag;
+
+       sum  = (uyear /=  4u);  /*   4yr rule --> IN  */
+       sum -= (uyear /= 25u);  /* 100yr rule --> OUT */
+       sum += (uyear /=  4u);  /* 400yr rule --> IN  */
+
+       /* Thanks to the alternation of IN/OUT/IN we can do the sum
+        * directly and have a single one's complement operation
+        * here. (Only if the years are negative, of course.) Otherwise
+        * the one's complement would have to be done when
+        * adding/subtracting the terms.
         */
-       return cycle * GREGORIAN_CYCLE_DAYS
-            + years * DAYSPERYEAR      /* days inregular years */
-            + years / 4                /* 4 year leap rule     */
-            - years / 100;             /* 100 year leap rule   */
-       /* the 400-year rule does not apply due to full-cycle split-off */
+       return uint32_2cpl_to_int32(sflag ^ sum);
+}
+
+/*
+ *---------------------------------------------------------------------
+ * Convert elapsed years in Era into elapsed days in Era.
+ *---------------------------------------------------------------------
+ */
+int32_t
+ntpcal_days_in_years(
+       int32_t years
+       )
+{
+       return years * DAYSPERYEAR + ntpcal_leapyears_in_years(years);
 }
 
 /*
@@ -1029,26 +1193,22 @@ ntpcal_days_in_months(
 {
        ntpcal_split res;
 
-       /* normalize month into range */
-       res.hi = 0;
-       res.lo = m;
-       if (res.lo < 0 || res.lo >= 12) {
-               res.hi = res.lo / 12;
-               res.lo = res.lo % 12;
-               if (res.lo < 0) {
-                       res.hi -= 1;
-                       res.lo += 12;
-               }
-       }
+       /* Add ten months and correct if needed. (It likely is...) */
+       res.lo  = m + 10;
+       res.hi  = (res.lo >= 12);
+       if (res.hi)
+               res.lo -= 12;
 
-       /* add 10 month for year starting with march */
-       if (res.lo < 2)
-               res.lo += 10;
-       else {
-               res.hi += 1;
-               res.lo -= 2;
+       /* if still out of range, normalise by floor division ... */
+       if (res.lo < 0 || res.lo >= 12) {
+               uint32_t mu, Q, sflag;
+               sflag = int32_sflag(res.lo);
+               mu    = int32_to_uint32_2cpl(res.lo);
+               Q     = sflag ^ ((sflag ^ mu) / 12u);
+               res.hi += uint32_2cpl_to_int32(Q);
+               res.lo  = mu - Q * 12u;
        }
-
+       
        /* get cummulated days in year with unshift */
        res.lo = shift_month_table[res.lo] - 306;
 
@@ -1451,37 +1611,42 @@ int32_t
 isocal_weeks_in_years(
        int32_t years
        )
-{
+{      
        /*
         * use: w = (y * 53431 + b[c]) / 1024 as interpolation
         */
-       static const int32_t bctab[4] = { 449, 157, 889, 597 };
-       int32_t cycle; /* full gregorian cycle */
-       int32_t cents; /* full centuries           */
-       int32_t weeks; /* accumulated weeks        */
-
-       /* split off full calendar cycles, using floor division */
-       cycle = years / 400;
-       years = years % 400;
-       if (years < 0) {
-               cycle -= 1;
-               years += 400;
-       }
-
-       /* split off full centuries */
-       cents = years / 100;
-       years = years % 100;
-
-       /*
-        * calculate elapsed weeks, taking into account that the
-        * first, third and fourth century have 5218 weeks but the
-        * second century falls short by one week.
+       static const uint16_t bctab[4] = { 157, 449, 597, 889 };
+
+       int32_t  cs, cw;
+       uint32_t cc, ci, yu, sflag;
+
+       sflag = int32_sflag(years);
+       yu    = int32_to_uint32_2cpl(years);
+       
+       /* split off centuries, using floor division */
+       cc  = sflag ^ ((sflag ^ yu) / 100u);
+       yu -= cc * 100u;
+
+       /* calculate century cycles shift and cycle index:
+        * Assuming a century is 5217 weeks, we have to add a cycle
+        * shift that is 3 for every 4 centuries, because 3 of the four
+        * centuries have 5218 weeks. So '(cc*3 + 1) / 4' is the actual
+        * correction, and the second century is the defective one.
+        *
+        * Needs floor division by 4, which is done with masking and
+        * shifting.
+        */
+       ci = cc * 3u + 1;
+       cs = uint32_2cpl_to_int32(sflag ^ ((sflag ^ ci) / 4u));
+       ci = ci % 4u;
+       
+       /* Get weeks in century. Can use plain division here as all ops
+        * are >= 0,  and let the compiler sort out the possible
+        * optimisations.
         */
-       weeks = (years * 53431 + bctab[cents]) / 1024;
+       cw = (yu * 53431u + bctab[ci]) / 1024u;
 
-       return cycle * GREGORIAN_CYCLE_WEEKS
-            + cents * 5218 - (cents > 1)
-            + weeks;
+       return uint32_2cpl_to_int32(cc) * 5217 + cs + cw;
 }
 
 /*
@@ -1498,35 +1663,41 @@ isocal_split_eraweeks(
        /*
         * use: y = (w * 157 + b[c]) / 8192 as interpolation
         */
-       static const int32_t bctab[4] = { 85, 131, 17, 62 };
+
+       static const uint16_t bctab[4] = { 85, 130, 17, 62 };
+
        ntpcal_split res;
-       int32_t      cents;
+       int32_t  cc, ci;
+       uint32_t sw, cy, Q, sflag;
 
-       /*
-        * split off 400-year cycles, using the fact that a 400-year
-        * cycle has 146097 days, which is exactly 20871 weeks.
+       /* Use two fast cycle-split divisions here. This is again
+        * susceptible to internal overflow, so we check the range. This
+        * still permits more than +/-20 million years, so this is
+        * likely a pure academical problem.
+        *
+        * We want to execute '(weeks * 4 + 2) /% 20871' under floor
+        * division rules in the first step.
         */
-       res.hi = weeks / GREGORIAN_CYCLE_WEEKS;
-       res.lo = weeks % GREGORIAN_CYCLE_WEEKS;
-       if (res.lo < 0) {
-               res.hi -= 1;
-               res.lo += GREGORIAN_CYCLE_WEEKS;
-       }
-       res.hi *= 400;
-
-       /*
-        * split off centuries, taking into account that the first,
-        * third and fourth century have 5218 weeks but that the
-        * second century falls short by one week.
+       sflag = int32_sflag(weeks);
+       sw  = uint32_saturate(int32_to_uint32_2cpl(weeks), sflag);
+       sw  = 4u * sw + 2;
+       Q   = sflag ^ ((sflag ^ sw) / GREGORIAN_CYCLE_WEEKS);
+       sw -= Q * GREGORIAN_CYCLE_WEEKS;
+       ci  = Q % 4u;
+       cc  = uint32_2cpl_to_int32(Q);
+
+       /* Split off years; sw >= 0 here! The scaled weeks in the years
+        * are scaled up by 157 afterwards.
+        */ 
+       sw  = (sw / 4u) * 157u + bctab[ci];
+       cy  = sw / 8192u;       /* ws >> 13 , let the compiler sort it out */
+       sw  = sw % 8192u;       /* ws & 8191, let the compiler sort it out */
+
+       /* assemble elapsed years and downscale the elapsed weeks in
+        * the year.
         */
-       res.lo += (res.lo >= 10435);
-       cents   = res.lo / 5218;
-       res.lo %= 5218;         /* res.lo is weeks in century now */
-
-       /* convert elapsed weeks in century to elapsed years and weeks */
-       res.lo  = res.lo * 157 + bctab[cents];
-       res.hi += cents * 100 + res.lo / 8192;
-       res.lo  = (res.lo % 8192) / 157;
+       res.hi = 100*cc + cy;
+       res.lo = sw / 157u;
 
        return res;
 }
@@ -1544,6 +1715,7 @@ isocal_ntp64_to_date(
 {
        ntpcal_split ds;
        int32_t      ts[3];
+       uint32_t     uw, ud, sflag;
 
        /*
         * Split NTP time into days and seconds, shift days into CE
@@ -1557,16 +1729,18 @@ isocal_ntp64_to_date(
        id->minute = (uint8_t)ts[1];
        id->second = (uint8_t)ts[2];
 
-       /* split date part */
-       ds.lo = ds.hi + DAY_NTP_STARTS - 1;     /* elapsed era days  */
-       ds.hi = ds.lo / 7;                      /* elapsed era weeks */
-       ds.lo = ds.lo % 7;                      /* elapsed week days */
-       if (ds.lo < 0) {                        /* floor division!   */
-               ds.hi -= 1;
-               ds.lo += 7;
-       }
+       /* split days into days and weeks, using floor division in unsigned */
+       ds.hi += DAY_NTP_STARTS - 1; /* shift from NTP to RDN */
+       sflag = int32_sflag(ds.hi);
+       ud  = int32_to_uint32_2cpl(ds.hi);
+       uw  = sflag ^ ((sflag ^ ud) / DAYSPERWEEK);
+       ud -= uw * DAYSPERWEEK;
+       ds.hi = uint32_2cpl_to_int32(uw);
+       ds.lo = ud;
+
        id->weekday = (uint8_t)ds.lo + 1;       /* weekday result    */
 
+       /* get year and week in year */
        ds = isocal_split_eraweeks(ds.hi);      /* elapsed years&week*/
        id->year = (uint16_t)ds.hi + 1;         /* shift to current  */
        id->week = (uint8_t )ds.lo + 1;
index 80fcd2c377df18276aa5e56fce2abbfc53f1b5ba..c702a60f8a016bbe87640d78e6d9c89b68e6fd16 100644 (file)
@@ -8,31 +8,16 @@
 
 static int leapdays(int year);
 
-char * CalendarFromCalToString(const struct calendar cal); 
-char * CalendarFromIsoToString(const struct isodate iso);
-
-// technically, booleans
-int IsEqualCal(const struct calendar expected, const struct calendar actual);
-int IsEqualIso(const struct isodate expected, const struct isodate actual);
-
-char * DateFromCalToStringCal(const struct calendar cal);
-char * DateFromIsoToStringIso(const struct isodate iso);
-
-// technically, booleans
-int sEqualDateCal(const struct calendar expected, const struct calendar actual);
-int IsEqualDateIso(const struct isodate expected, const struct isodate actual);
-
-
 int isGT(int first, int second);
 int leapdays(int year);
-char * CalendarFromCalToString(const struct calendar cal);
-char * CalendarFromIsoToString(const struct isodate iso); 
-int IsEqualCal(const struct calendar expected, const struct calendar actual);
-int IsEqualIso(const struct isodate expected, const struct isodate actual);
-char * DateFromCalToString(const struct calendar cal);
-char * DateFromIsoToString(const struct isodate iso);
-int IsEqualDateCal(const struct calendar expected, const struct calendar actual);
-int IsEqualDateIso(const struct isodate expected, const struct isodate actual);
+char * CalendarFromCalToString(const struct calendar *cal);
+char * CalendarFromIsoToString(const struct isodate *iso); 
+int IsEqualCal(const struct calendar *expected, const struct calendar *actual);
+int IsEqualIso(const struct isodate *expected, const struct isodate *actual);
+char * DateFromCalToString(const struct calendar *cal);
+char * DateFromIsoToString(const struct isodate *iso);
+int IsEqualDateCal(const struct calendar *expected, const struct calendar *actual);
+int IsEqualDateIso(const struct isodate *expected, const struct isodate *actual);
 void test_DaySplitMerge(void);
 void test_SplitYearDays1(void);
 void test_SplitYearDays2(void);
@@ -44,8 +29,10 @@ void test_RoundTripYearStart(void);
 void test_RoundTripMonthStart(void);
 void test_RoundTripWeekStart(void);
 void test_RoundTripDayStart(void);
-
-
+void test_IsoCalYearsToWeeks(void);
+void test_IsoCalWeeksToYearStart(void);
+void test_IsoCalWeeksToYearEnd(void);
+void test_DaySecToDate(void);
 
 // ---------------------------------------------------------------------
 // test support stuff
@@ -74,154 +61,129 @@ leapdays(int year)
 }
 
 char *
-CalendarFromCalToString(const struct calendar cal) {
-       char * str = malloc (sizeof (char) * 100);
-       
-       char buffer[100] ="";
-       snprintf(buffer, 100, "%u", cal.year);
-       strcat(str, buffer);
-       strcat(str, "-");
-       snprintf(buffer, 100, "%u", (u_int)cal.month);
-       strcat(str, buffer);
-       strcat(str, "-");
-       snprintf(buffer, 100, "%u", (u_int)cal.monthday);
-       strcat(str, buffer);
-       strcat(str, " (");
-       snprintf(buffer, 100, "%u", cal.yearday);
-       strcat(str, buffer);
-       strcat(str, ") ");
-       snprintf(buffer, 100, "%u", (u_int)cal.hour);
-       strcat(str, buffer);
-       strcat(str, ":");
-       snprintf(buffer, 100, "%u", (u_int)cal.minute);
-       strcat(str, buffer);
-       strcat(str, ":");
-       snprintf(buffer, 100, "%u", (u_int)cal.second);
-       strcat(str, buffer);
-
+CalendarFromCalToString(
+    const struct calendar *cal)
+{
+       char * str = malloc(sizeof (char) * 100);
+       snprintf(str, 100, "%u-%02u-%02u (%u) %02u:%02u:%02u",
+                cal->year, (u_int)cal->month, (u_int)cal->monthday,
+                cal->yearday,
+                (u_int)cal->hour, (u_int)cal->minute, (u_int)cal->second);
+       str[99] = '\0'; /* paranoia rulez! */
        return str;
 }
 
 char *
-CalendarFromIsoToString(const struct isodate iso) {
-
+CalendarFromIsoToString(
+       const struct isodate *iso)
+{
        char * str = malloc (sizeof (char) * 100);
-       
-       char buffer[100] ="";
-       snprintf(buffer, 100, "%u", iso.year);
-       strcat(str, buffer);
-       strcat(str, "-");
-       snprintf(buffer, 100, "%u", (u_int)iso.week);
-       strcat(str, buffer);
-       strcat(str, "-");
-       snprintf(buffer, 100, "%u", (u_int)iso.weekday);
-       strcat(str, buffer);
-       snprintf(buffer, 100, "%u", (u_int)iso.hour);
-       strcat(str, buffer);
-       strcat(str, ":");
-       snprintf(buffer, 100, "%u", (u_int)iso.minute);
-       strcat(str, buffer);
-       strcat(str, ":");
-       snprintf(buffer, 100, "%u", (u_int)iso.second);
-       strcat(str, buffer);
-
+       snprintf(str, 100, "%u-W%02u-%02u %02u:%02u:%02u",
+                iso->year, (u_int)iso->week, (u_int)iso->weekday,
+                (u_int)iso->hour, (u_int)iso->minute, (u_int)iso->second);
+       str[99] = '\0'; /* paranoia rulez! */
        return str;
 }
 
 int
-IsEqualCal(const struct calendar expected, const struct calendar actual) {
-       if (expected.year == actual.year &&
-           (!expected.yearday || expected.yearday == actual.yearday) &&
-           expected.month == actual.month &&
-           expected.monthday == actual.monthday &&
-           expected.hour == actual.hour &&
-           expected.minute == actual.minute &&
-           expected.second == actual.second) {
+IsEqualCal(
+       const struct calendar *expected,
+       const struct calendar *actual)
+{
+       if (expected->year == actual->year &&
+           (!expected->yearday || expected->yearday == actual->yearday) &&
+           expected->month == actual->month &&
+           expected->monthday == actual->monthday &&
+           expected->hour == actual->hour &&
+           expected->minute == actual->minute &&
+           expected->second == actual->second) {
                return TRUE;
        } else {
-               printf("expected: %s but was %s", CalendarFromCalToString(expected) , CalendarFromCalToString(actual));
+               printf("expected: %s but was %s",
+                      CalendarFromCalToString(expected),
+                      CalendarFromCalToString(actual));
                return FALSE;             
        }
 }
 
 int
-IsEqualIso(const struct isodate expected, const struct isodate actual) {
-       if (expected.year == actual.year &&
-           expected.week == actual.week &&
-           expected.weekday == actual.weekday &&
-           expected.hour == actual.hour &&
-           expected.minute == actual.minute &&
-           expected.second == actual.second) {
+IsEqualIso(
+       const struct isodate *expected,
+       const struct isodate *actual)
+{
+       if (expected->year == actual->year &&
+           expected->week == actual->week &&
+           expected->weekday == actual->weekday &&
+           expected->hour == actual->hour &&
+           expected->minute == actual->minute &&
+           expected->second == actual->second) {
                return TRUE;
        } else {
-               printf("expected: %s but was %s", CalendarFromIsoToString(expected) , CalendarFromIsoToString(actual));
+               printf("expected: %s but was %s",
+                      CalendarFromIsoToString(expected),
+                      CalendarFromIsoToString(actual));
                return FALSE;      
        }
 }
 
 char *
-DateFromCalToString(const struct calendar cal) {
+DateFromCalToString(
+       const struct calendar *cal)
+{
 
        char * str = malloc (sizeof (char) * 100);
-       
-       char buffer[100] ="";
-       snprintf(buffer, 100, "%u", cal.year);
-       strcat(str, buffer);
-       strcat(str, "-");
-       snprintf(buffer, 100, "%u", (u_int)cal.month);
-       strcat(str, buffer);
-       strcat(str, "-");
-       snprintf(buffer, 100, "%u", (u_int)cal.monthday);
-       strcat(str, buffer);
-       strcat(str, " (");
-       snprintf(buffer, 100, "%u", cal.yearday);
-       strcat(str, buffer);
-       strcat(str, ")");
-       
+       snprintf(str, 100, "%u-%02u-%02u (%u)",
+                cal->year, (u_int)cal->month, (u_int)cal->monthday,
+                cal->yearday);
+       str[99] = '\0'; /* paranoia rulez! */
        return str;
 }
 
 char *
-DateFromIsoToString(const struct isodate iso) {
+DateFromIsoToString(
+       const struct isodate *iso)
+{
 
        char * str = malloc (sizeof (char) * 100);
-       
-       char buffer[100] ="";
-       snprintf(buffer, 100, "%u", iso.year);
-       strcat(str, buffer);
-       strcat(str, "-");
-       snprintf(buffer, 100, "%u", (u_int)iso.week);
-       strcat(str, buffer);
-       strcat(str, "-");
-       snprintf(buffer, 100, "%u", (u_int)iso.weekday);
-       strcat(str, buffer);
-
+       snprintf(str, 100, "%u-W%02u-%02u",
+                iso->year, (u_int)iso->week, (u_int)iso->weekday);
+       str[99] = '\0'; /* paranoia rulez! */
        return str;
 }
 
 // boolean 
 int
-IsEqualDateCal(const struct calendar expected, const struct calendar actual) {
-       if (expected.year == actual.year &&
-           (!expected.yearday || expected.yearday == actual.yearday) &&
-           expected.month == actual.month &&
-           expected.monthday == actual.monthday) {
+IsEqualDateCal(
+       const struct calendar *expected,
+       const struct calendar *actual)
+{
+       if (expected->year == actual->year &&
+           (!expected->yearday || expected->yearday == actual->yearday) &&
+           expected->month == actual->month &&
+           expected->monthday == actual->monthday) {
                return TRUE;
        } else {
-               printf("expected: %s but was %s", DateFromCalToString(expected) ,DateFromCalToString(actual));
+               printf("expected: %s but was %s",
+                      DateFromCalToString(expected),
+                      DateFromCalToString(actual));
                return FALSE;
        }
 }
 
 // boolean
 int
-IsEqualDateIso(const struct isodate expected, const struct isodate actual) {
-       if (expected.year == actual.year &&
-           expected.week == actual.week &&
-           expected.weekday == actual.weekday) {
+IsEqualDateIso(
+       const struct isodate *expected,
+       const struct isodate *actual)
+{
+       if (expected->year == actual->year &&
+           expected->week == actual->week &&
+           expected->weekday == actual->weekday) {
                return TRUE;
        } else {
-               printf("expected: %s but was %s", DateFromIsoToString(expected) ,DateFromIsoToString(actual));
+               printf("expected: %s but was %s",
+                      DateFromIsoToString(expected),
+                      DateFromIsoToString(actual));
                return FALSE;       
        }
 }
@@ -316,7 +278,7 @@ test_RataDie1(void) {
        struct calendar actual;
 
        ntpcal_rd_to_date(&actual, testDate);
-       TEST_ASSERT_TRUE(IsEqualDateCal(expected, actual));
+       TEST_ASSERT_TRUE(IsEqualDateCal(&expected, &actual));
 }
 
 // check last day of february for first 10000 years
@@ -331,7 +293,7 @@ test_LeapYears1(void) {
 
                ntpcal_rd_to_date(&dateOut, ntpcal_date_to_rd(&dateIn));
 
-               TEST_ASSERT_TRUE(IsEqualDateCal(dateIn, dateOut));
+               TEST_ASSERT_TRUE(IsEqualDateCal(&dateIn, &dateOut));
        }
 }
 
@@ -346,7 +308,7 @@ test_LeapYears2(void) {
                dateIn.yearday  = 60 + leapdays(dateIn.year);
 
                ntpcal_rd_to_date(&dateOut, ntpcal_date_to_rd(&dateIn));
-               TEST_ASSERT_TRUE(IsEqualDateCal(dateIn, dateOut));
+               TEST_ASSERT_TRUE(IsEqualDateCal(&dateIn, &dateOut));
        }
 }
 
@@ -378,7 +340,7 @@ test_RoundTripDate(void) {
                                TEST_ASSERT_EQUAL(expRdn, truRdn);
 
                                ntpcal_rd_to_date(&truDate, truRdn);
-                               TEST_ASSERT_TRUE(IsEqualDateCal(expDate, truDate));
+                               TEST_ASSERT_TRUE(IsEqualDateCal(&expDate, &truDate));
                        }
                }
        }
@@ -449,4 +411,121 @@ test_RoundTripDayStart(void) {
                expds = ntpcal_date_to_ntp(&date);
                TEST_ASSERT_EQUAL(expds, truds);
        }
-}      
+}
+
+/* ---------------------------------------------------------------------
+ * ISO8601 week calendar internals
+ *
+ * The ISO8601 week calendar implementation is simple in the terms of
+ * the math involved, but the implementation of the calculations must
+ * take care of a few things like overflow, floor division, and sign
+ * corrections.
+ *
+ * Most of the functions are straight forward, but converting from years
+ * to weeks and from weeks to years warrants some extra tests. These use
+ * an independent reference implementation of the conversion from years
+ * to weeks.
+ * ---------------------------------------------------------------------
+ */
+
+/* helper / reference implementation for the first week of year in the
+ * ISO8601 week calendar. This is based on the reference definition of
+ * the ISO week calendar start: The Monday closest to January,1st of the
+ * corresponding year in the Gregorian calendar.
+ */
+static int32_t
+refimpl_WeeksInIsoYears(
+       int32_t years)
+{
+       int32_t days, weeks;
+       days = ntpcal_weekday_close(
+               ntpcal_days_in_years(years) + 1,
+               CAL_MONDAY) - 1;
+       /* the weekday functions operate on RDN, while we want elapsed
+        * units here -- we have to add / sub 1 in the midlle / at the
+        * end of the operation that gets us the first day of the ISO
+        * week calendar day.
+        */
+       weeks = days / 7;
+       days  = days % 7;
+       TEST_ASSERT_EQUAL(0, days); /* paranoia check... */
+       return weeks;
+}
+
+/* The next tests loop over 5000yrs, but should still be very fast. If
+ * they are not, the calendar needs a better implementation...
+ */
+void test_IsoCalYearsToWeeks(void) {
+       int32_t years;
+       int32_t wref, wcal;
+       for (years = -1000; years < 4000; ++years) {
+               /* get number of weeks before years (reference) */
+               wref = refimpl_WeeksInIsoYears(years);
+               /* get number of weeks before years (object-under-test) */
+               wcal = isocal_weeks_in_years(years);
+               TEST_ASSERT_EQUAL(wref, wcal);
+       }
+}
+
+void test_IsoCalWeeksToYearStart(void) {
+       int32_t years;
+       int32_t wref;
+       ntpcal_split ysplit;
+       for (years = -1000; years < 4000; ++years) {
+               /* get number of weeks before years (reference) */
+               wref = refimpl_WeeksInIsoYears(years);
+               /* reverse split */
+               ysplit = isocal_split_eraweeks(wref);
+               /* check invariants: same year, week 0 */
+               TEST_ASSERT_EQUAL(years, ysplit.hi);
+               TEST_ASSERT_EQUAL(0, ysplit.lo);
+       }
+}
+
+void test_IsoCalWeeksToYearEnd(void) {
+       int32_t years;
+       int32_t wref;
+       ntpcal_split ysplit;
+       for (years = -1000; years < 4000; ++years) {
+               /* get last week of previous year */
+               wref = refimpl_WeeksInIsoYears(years) - 1;
+               /* reverse split */
+               ysplit = isocal_split_eraweeks(wref);
+               /* check invariants: previous year, week 51 or 52 */
+               TEST_ASSERT_EQUAL(years-1, ysplit.hi);
+               TEST_ASSERT(ysplit.lo == 51 || ysplit.lo == 52);
+       }
+}
+
+void test_DaySecToDate(void) {
+       struct calendar cal;
+       int32_t days;
+
+       days = ntpcal_daysec_to_date(&cal, -86400);
+       TEST_ASSERT_MESSAGE((days==-1 && cal.hour==0 && cal.minute==0 && cal.second==0),
+               "failed for -86400");
+
+       days = ntpcal_daysec_to_date(&cal, -86399);
+       TEST_ASSERT_MESSAGE((days==-1 && cal.hour==0 && cal.minute==0 && cal.second==1),
+               "failed for -86399");
+
+       days = ntpcal_daysec_to_date(&cal, -1);
+       TEST_ASSERT_MESSAGE((days==-1 && cal.hour==23 && cal.minute==59 && cal.second==59),
+               "failed for -1");
+
+       days = ntpcal_daysec_to_date(&cal, 0);
+       TEST_ASSERT_MESSAGE((days==0 && cal.hour==0 && cal.minute==0 && cal.second==0),
+               "failed for 0");
+
+       days = ntpcal_daysec_to_date(&cal, 1);
+       TEST_ASSERT_MESSAGE((days==0 && cal.hour==0 && cal.minute==0 && cal.second==1),
+               "failed for 1");
+
+       days = ntpcal_daysec_to_date(&cal, 86399);
+       TEST_ASSERT_MESSAGE((days==0 && cal.hour==23 && cal.minute==59 && cal.second==59),
+               "failed for 86399");
+
+       days = ntpcal_daysec_to_date(&cal, 86400);
+       TEST_ASSERT_MESSAGE((days==1 && cal.hour==0 && cal.minute==0 && cal.second==0),
+               "failed for 86400");
+}
index 743eed4d5fa0e1edf1ffedb4fc6b72608192d005..bb240d35e8b52ab5bb8ad4f508bc55bba9e28aa1 100644 (file)
@@ -41,6 +41,10 @@ extern void test_RoundTripYearStart(void);
 extern void test_RoundTripMonthStart(void);
 extern void test_RoundTripWeekStart(void);
 extern void test_RoundTripDayStart(void);
+extern void test_IsoCalYearsToWeeks(void);
+extern void test_IsoCalWeeksToYearStart(void);
+extern void test_IsoCalWeeksToYearEnd(void);
+extern void test_DaySecToDate(void);
 
 
 //=======Test Reset Option=====
@@ -59,17 +63,21 @@ int main(int argc, char *argv[])
 {
   progname = argv[0];
   UnityBegin("calendar.c");
-  RUN_TEST(test_DaySplitMerge, 36);
-  RUN_TEST(test_SplitYearDays1, 37);
-  RUN_TEST(test_SplitYearDays2, 38);
-  RUN_TEST(test_RataDie1, 39);
-  RUN_TEST(test_LeapYears1, 40);
-  RUN_TEST(test_LeapYears2, 41);
-  RUN_TEST(test_RoundTripDate, 42);
-  RUN_TEST(test_RoundTripYearStart, 43);
-  RUN_TEST(test_RoundTripMonthStart, 44);
-  RUN_TEST(test_RoundTripWeekStart, 45);
-  RUN_TEST(test_RoundTripDayStart, 46);
+  RUN_TEST(test_DaySplitMerge, 21);
+  RUN_TEST(test_SplitYearDays1, 22);
+  RUN_TEST(test_SplitYearDays2, 23);
+  RUN_TEST(test_RataDie1, 24);
+  RUN_TEST(test_LeapYears1, 25);
+  RUN_TEST(test_LeapYears2, 26);
+  RUN_TEST(test_RoundTripDate, 27);
+  RUN_TEST(test_RoundTripYearStart, 28);
+  RUN_TEST(test_RoundTripMonthStart, 29);
+  RUN_TEST(test_RoundTripWeekStart, 30);
+  RUN_TEST(test_RoundTripDayStart, 31);
+  RUN_TEST(test_IsoCalYearsToWeeks, 32);
+  RUN_TEST(test_IsoCalWeeksToYearStart, 33);
+  RUN_TEST(test_IsoCalWeeksToYearEnd, 34);
+  RUN_TEST(test_DaySecToDate, 35);
 
   return (UnityEnd());
 }