Implement DATESUM, DATEDIFF functions.
authorBen Pfaff <blp@gnu.org>
Thu, 14 Dec 2006 03:36:00 +0000 (03:36 +0000)
committerBen Pfaff <blp@gnu.org>
Thu, 14 Dec 2006 03:36:00 +0000 (03:36 +0000)
Patch #5637.

configure.ac
doc/expressions.texi
src/data/ChangeLog
src/data/calendar.c
src/data/calendar.h
src/language/expressions/ChangeLog
src/language/expressions/helpers.c
src/language/expressions/helpers.h
src/language/expressions/operations.def
tests/ChangeLog
tests/expressions/expressions.sh

index 0295d7faa48fe487a9ada7a46f0049d46e52e7f4..38eee6915410f2bfdaeae1a3a41bf2665ee72bd2 100644 (file)
@@ -73,7 +73,7 @@ AC_DEFINE(FPREP_IEEE754, 1,
 AC_C_BIGENDIAN
 
 AC_FUNC_VPRINTF
-AC_CHECK_FUNCS([__setfpucw isinf isnan finite getpid feholdexcept round])
+AC_CHECK_FUNCS([__setfpucw isinf isnan finite getpid feholdexcept round trunc])
 
 AC_PROG_LN_S
 
index c6c96dfb931d8f33e8b0c56f15b8ceb35fe61af4..540cad220ef4492e2b65fca73f02ebb80f283302 100644 (file)
@@ -260,6 +260,7 @@ The sections below describe each function in detail.
 * String Functions::            CONCAT INDEX LENGTH LOWER LPAD LTRIM NUMBER 
                                 RINDEX RPAD RTRIM STRING SUBSTR UPCASE
 * Time & Date::                 CTIME.xxx DATE.xxx TIME.xxx XDATE.xxx
+                                DATEDIFF DATESUM
 * Miscellaneous Functions::     LAG YRMODA VALUELABEL
 * Statistical Distribution Functions::  PDF CDF SIG IDF RV NPDF NCDF
 @end menu
@@ -708,6 +709,7 @@ Most time and date functions will not accept earlier dates.
 * Date Extraction::             XDATE.@{DATE HOUR JDAY MDAY MINUTE MONTH
                                        QUARTER SECOND TDAY TIME WEEK
                                        WKDAY YEAR@}
+* Time & Date Arithmetic::      DATEDIFF DATESUM
 @end menu
 
 @node Time & Date Concepts
@@ -741,27 +743,6 @@ given below correspond with the numeric PSPP dates given:
               24 Aug 1995        13,028,601,600
 @end example
 
-@cindex time, mathematical properties of
-@cindex mathematics, applied to times & dates
-@cindex dates, mathematical properties of
-@noindent
-Ordinary arithmetic operations on dates and times often produce
-sensible results.  Adding a time to, or subtracting one from, a date
-produces a new date that much earlier or later.  The difference of two
-dates yields the time between those dates.  Adding two times produces
-the combined time.  Multiplying a time by a scalar produces a time
-that many times longer.  Since times and dates are just numbers, the
-ordinary addition and subtraction operators are employed for these
-purposes.
-
-Adding two dates does not produce a useful result.
-
-As the table shows, dates and times may have very large values.  Thus,
-it is not a good idea to take powers of these values; also, the
-accuracy of some procedures may be affected.  If necessary, convert
-times or dates in seconds to some other unit, like days or years,
-before performing analysis.
-
 @node Time Construction
 @subsubsection Functions that Produce Times
 @cindex times, constructing
@@ -1020,6 +1001,68 @@ Returns the year (as an integer 1582 or greater) corresponding to
 @var{date}.
 @end deftypefn
 
+@node Time & Date Arithmetic
+@subsubsection Time and Date Arithmetic
+
+@cindex time, mathematical properties of
+@cindex mathematics, applied to times & dates
+@cindex dates, mathematical properties of
+@noindent
+Ordinary arithmetic operations on dates and times often produce
+sensible results.  Adding a time to, or subtracting one from, a date
+produces a new date that much earlier or later.  The difference of two
+dates yields the time between those dates.  Adding two times produces
+the combined time.  Multiplying a time by a scalar produces a time
+that many times longer.  Since times and dates are just numbers, the
+ordinary addition and subtraction operators are employed for these
+purposes.
+
+Adding two dates does not produce a useful result.
+
+Dates and times may have very large values.  Thus,
+it is not a good idea to take powers of these values; also, the
+accuracy of some procedures may be affected.  If necessary, convert
+times or dates in seconds to some other unit, like days or years,
+before performing analysis.
+
+PSPP supplies a few functions for date arithmetic:
+
+@deftypefn {Function} {} DATEDIFF (@var{date1}, @var{date2}, @var{unit})
+Returns the span of time from @var{date1} to @var{date2} in terms of
+@var{unit}, which must be a quoted string, one of @samp{years},
+@samp{quarters}, @samp{months}, @samp{weeks}, @samp{days},
+@samp{hours}, @samp{minutes}, and @samp{seconds}.  The result is an
+integer, truncated toward zero.
+
+One year is considered to span from a given date to the same month,
+day, and time of day the next year.  Thus, from Jan.@tie{}1 of one
+year to Jan.@tie{}1 the next year is considered to be a full year, but
+Feb.@tie{}29 of a leap year to the following Feb.@tie{}28 is not.
+Similarly, one month spans from a given day of the month to the same
+day of the following month.  Thus, there is never a full month from
+Jan.@tie{}31 of a given year to any day in the following February.
+@end deftypefn
+
+@deftypefn {Function} {} DATESUM (@var{date}, @var{quantity}, @var{unit}[, @var{method}])
+Returns @var{date} advanced by the given @var{quantity} of the
+specified @var{unit}, which must be one of the strings @samp{years},
+@samp{quarters}, @samp{months}, @samp{weeks}, @samp{days},
+@samp{hours}, @samp{minutes}, and @samp{seconds}.
+
+When @var{unit} is @samp{years}, @samp{quarters}, or @samp{months},
+only the integer part of @var{quantity} is considered.  Adding one of
+these units can cause the day of the month to exceed the number of
+days in the month.  In this case, the @var{method} comes into
+play: if it is omitted or specified as @samp{closest} (as a quoted
+string), then the resulting day is the last day of the month;
+otherwise, if it is specified as @samp{rollover}, then the extra days
+roll over into the following month.
+
+When @var{unit} is @samp{weeks}, @samp{days}, @samp{hours},
+@samp{minutes}, or @samp{seconds}, the @var{quantity} is not rounded
+to an integer and @var{method}, if specified, is ignored.
+@end deftypefn
+
 @node Miscellaneous Functions
 @subsection Miscellaneous Functions
 @cindex functions, miscellaneous
index dacd90ac39fc5028e84f4f19df396df53e1f5b40..e88749621d7193141dc1f7031eff29a581617c6f 100644 (file)
@@ -1,3 +1,7 @@
+Wed Dec 13 19:30:11 2006  Ben Pfaff  <blp@gnu.org>
+
+       * calendar.c (calendar_days_in_month): New function.
+
 Mon Dec 11 07:53:39 2006  Ben Pfaff  <blp@gnu.org>
 
        * value-labels.c (hash_int_val_lab): Only hash as many bytes as
index 3a38f1265fae0b3013d68840a8ea62aaae609b4d..c3d9d8e6ea00728cca253c4be18dbc01527dc5f6 100644 (file)
@@ -210,3 +210,14 @@ calendar_offset_to_mday (int ofs)
   calendar_offset_to_gregorian (ofs, &y, &m, &d, &yd);
   return d;
 }
+
+/* Returns the number of days in the specified month. */
+int
+calendar_days_in_month (int y, int m) 
+{
+  static const int days_per_month[12]
+    = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+
+  assert (m >= 1 && m <= 12);
+  return m == 2 && is_leap_year (y) ? 29 : days_per_month[m - 1];
+}
index f46e71fec0b8c32fb97b763598176a8a63bfe533..06d2fd3a49b0f350b1015e05fc8fa41223421304 100644 (file)
@@ -12,4 +12,6 @@ int calendar_offset_to_mday (int ofs);
 int calendar_offset_to_yday (int ofs);
 int calendar_offset_to_wday (int ofs);
 
+int calendar_days_in_month (int y, int m);
+
 #endif /* calendar.h */
index cd1ef1239faf31ad04d6186636c4727173ad27e3..a55b977f863d8c1ccefc8ea275e0b941edd90490 100644 (file)
@@ -1,3 +1,26 @@
+Wed Dec 13 19:33:52 2006  Ben Pfaff  <blp@gnu.org>
+
+Wed Dec 13 19:30:26 2006  Ben Pfaff  <blp@gnu.org>
+
+       Implement support for DATESUM, DATEDIFF expression functions.  See
+       patch #5637.
+       
+       * helpers.c (enum date_unit): New enum.
+       [!HAVE_TRUNC] (trunc): New function.
+       (recognize_unit): New function.
+       (year_diff): New function.
+       (month_diff): New function.
+       (quarter_diff): New function.
+       (date_unit_duration): New function.
+       (expr_date_difference): New function.
+       (enum date_sum_method): New function.
+       (recognize_method): New function.
+       (add_months): New function.
+       (expr_date_sum): New function.
+       
+       * operations.def: Implement DATESUM, DATEDIFF functions.  Mark
+       VALUELABEL no_abbrev.
+
 Sun Dec 10 16:49:33 2006  Ben Pfaff  <blp@gnu.org>
 
        * operations.def: Implement VALUELABEL function.  Add DATEDIFF,
index bddc71269f3c65b2fd070bb271667e0ff2e8b783..a07282be16d836a21a6744d6573583c9ce9dc2d3 100644 (file)
@@ -2,6 +2,7 @@
 #include "helpers.h"
 #include <gsl/gsl_roots.h>
 #include <gsl/gsl_sf.h>
+#include <libpspp/assertion.h>
 #include <libpspp/pool.h>
 #include "private.h"
 
@@ -116,6 +117,284 @@ expr_yrmoda (double year, double month, double day)
 
   return expr_ymd_to_ofs (year, month, day);
 }
+\f
+/* A date unit. */
+enum date_unit 
+  {
+    DATE_YEARS,
+    DATE_QUARTERS,
+    DATE_MONTHS,
+    DATE_WEEKS,
+    DATE_DAYS,
+    DATE_HOURS,
+    DATE_MINUTES,
+    DATE_SECONDS
+  };
+
+#ifndef HAVE_TRUNC
+/* Return X rounded toward zero. */
+static double
+trunc (double x) 
+{
+  return x >= 0.0 ? floor (x) : ceil (x);
+}
+#endif /* !HAVE_TRUNC */
+
+/* Stores in *UNIT the unit whose name is NAME.
+   Return success. */
+static enum date_unit
+recognize_unit (struct substring name, enum date_unit *unit) 
+{
+  struct unit_name
+    {
+      enum date_unit unit;
+      const struct substring name;
+    };
+  static const struct unit_name unit_names[] = 
+    {
+      { DATE_YEARS, SS_LITERAL_INITIALIZER ("years") },
+      { DATE_QUARTERS, SS_LITERAL_INITIALIZER ("quarters") },
+      { DATE_MONTHS, SS_LITERAL_INITIALIZER ("months") },
+      { DATE_WEEKS, SS_LITERAL_INITIALIZER ("weeks") },
+      { DATE_DAYS, SS_LITERAL_INITIALIZER ("days") },
+      { DATE_HOURS, SS_LITERAL_INITIALIZER ("hours") },
+      { DATE_MINUTES, SS_LITERAL_INITIALIZER ("minutes") },
+      { DATE_SECONDS, SS_LITERAL_INITIALIZER ("seconds") },
+    };
+  const int unit_name_cnt = sizeof unit_names / sizeof *unit_names;
+
+  const struct unit_name *un;
+
+  for (un = unit_names; un < &unit_names[unit_name_cnt]; un++)
+    if (ss_equals_case (un->name, name)) 
+      {
+        *unit = un->unit;
+        return true;
+      }
+
+  msg (SE, _("Unrecognized date unit \"%.*s\".  "
+             "Valid date units are \"years\", \"quarters\", \"months\", "
+             "\"weeks\", \"days\", \"hours\", \"minutes\", and \"seconds\"."),
+       (int) ss_length (name), ss_data (name));
+  return false;
+}
+
+/* Returns the number of whole years from DATE1 to DATE2,
+   where a year is defined as the same or later month, day, and
+   time of day. */
+static int
+year_diff (double date1, double date2) 
+{
+  int y1, m1, d1, yd1;
+  int y2, m2, d2, yd2;
+  int diff;
+
+  assert (date2 >= date1);
+  calendar_offset_to_gregorian (date1 / DAY_S, &y1, &m1, &d1, &yd1);
+  calendar_offset_to_gregorian (date2 / DAY_S, &y2, &m2, &d2, &yd2);
+
+  diff = y2 - y1;
+  if (diff > 0) 
+    {
+      int yd1 = 32 * m1 + d1;
+      int yd2 = 32 * m2 + d2;
+      if (yd2 < yd1
+          || (yd2 == yd1 && fmod (date2, DAY_S) < fmod (date1, DAY_S)))
+        diff--;
+    }
+  return diff;
+}
+
+/* Returns the number of whole months from DATE1 to DATE2,
+   where a month is defined as the same or later day and time of
+   day. */
+static int
+month_diff (double date1, double date2) 
+{
+  int y1, m1, d1, yd1;
+  int y2, m2, d2, yd2;
+  int diff;
+
+  assert (date2 >= date1);
+  calendar_offset_to_gregorian (date1 / DAY_S, &y1, &m1, &d1, &yd1);
+  calendar_offset_to_gregorian (date2 / DAY_S, &y2, &m2, &d2, &yd2);
+
+  diff = ((y2 * 12) + m2) - ((y1 * 12) + m1);
+  if (diff > 0
+      && (d2 < d1
+          || (d2 == d1 && fmod (date2, DAY_S) < fmod (date1, DAY_S))))
+    diff--;
+  return diff;
+}
+
+/* Returns the number of whole quarter from DATE1 to DATE2,
+   where a quarter is defined as three months. */
+static int
+quarter_diff (double date1, double date2)
+{
+  return month_diff (date1, date2) / 3;
+}
+
+/* Returns the number of seconds in the given UNIT. */
+static int
+date_unit_duration (enum date_unit unit) 
+{
+  switch (unit) 
+    {
+    case DATE_WEEKS:
+      return WEEK_S;
+
+    case DATE_DAYS:
+      return DAY_S;
+
+    case DATE_HOURS:
+      return H_S;
+
+    case DATE_MINUTES:
+      return MIN_S;
+
+    case DATE_SECONDS:
+      return 1;
+
+    default:
+      NOT_REACHED ();
+    }
+}
+
+/* Returns the span from DATE to DATE2 in terms of UNIT_NAME. */
+double
+expr_date_difference (double date1, double date2, struct substring unit_name) 
+{
+  enum date_unit unit;
+  
+  if (!recognize_unit (unit_name, &unit))
+    return SYSMIS;
+  
+  switch (unit) 
+    {
+    case DATE_YEARS:
+      return (date2 >= date1
+              ? year_diff (date1, date2)
+              : -year_diff (date2, date1));
+
+    case DATE_QUARTERS:
+      return (date2 >= date1
+              ? quarter_diff (date1, date2)
+              : -quarter_diff (date2, date1));
+      
+    case DATE_MONTHS:
+      return (date2 >= date1
+              ? month_diff (date1, date2)
+              : -month_diff (date2, date1));
+
+    case DATE_WEEKS:
+    case DATE_DAYS:
+    case DATE_HOURS:
+    case DATE_MINUTES:
+    case DATE_SECONDS:
+      return trunc ((date2 - date1) / date_unit_duration (unit));
+    }
+
+  NOT_REACHED ();
+}
+
+/* How to deal with days out of range for a given month. */
+enum date_sum_method 
+  {
+    SUM_ROLLOVER,       /* Roll them over to the next month. */
+    SUM_CLOSEST         /* Use the last day of the month. */
+  };
+
+/* Stores in *METHOD the method whose name is NAME.
+   Return success. */
+static bool
+recognize_method (struct substring method_name, enum date_sum_method *method) 
+{
+  if (ss_equals_case (method_name, ss_cstr ("closest"))) 
+    {
+      *method = SUM_CLOSEST;
+      return true; 
+    }
+  else if (ss_equals_case (method_name, ss_cstr ("rollover"))) 
+    {
+      *method = SUM_ROLLOVER;
+      return true; 
+    }
+  else 
+    {
+      msg (SE, _("Invalid DATESUM method.  "
+                 "Valid choices are \"closest\" and \"rollover\"."));
+      return false;
+    }
+}
+
+/* Returns DATE advanced by the given number of MONTHS, with
+   day-of-month overflow resolved using METHOD. */
+static double
+add_months (double date, int months, enum date_sum_method method) 
+{
+  int y, m, d, yd;
+  double output;
+
+  calendar_offset_to_gregorian (date / DAY_S, &y, &m, &d, &yd);
+  y += months / 12;
+  m += months % 12;
+  if (m < 1) 
+    {
+      m += 12;
+      y--;
+    }
+  else if (m > 12) 
+    {
+      m -= 12;
+      y++;
+    }
+  assert (m >= 1 && m <= 12);
+
+  if (method == SUM_CLOSEST && d > calendar_days_in_month (y, m)) 
+    d = calendar_days_in_month (y, m);
+
+  output = calendar_gregorian_to_offset (y, m, d, expr_error, NULL);
+  if (output != SYSMIS)
+    output = (output * DAY_S) + fmod (date, DAY_S);
+  return output;
+}
+
+/* Returns DATE advanced by the given QUANTITY of units given in
+   UNIT_NAME, with day-of-month overflow resolved using
+   METHOD_NAME. */
+double
+expr_date_sum (double date, double quantity, struct substring unit_name,
+               struct substring method_name) 
+{
+  enum date_unit unit;
+  enum date_sum_method method;
+
+  if (!recognize_unit (unit_name, &unit)
+      || !recognize_method (method_name, &method))
+    return SYSMIS;
+  
+  switch (unit) 
+    {
+    case DATE_YEARS:
+      return add_months (date, trunc (quantity) * 12, method);
+
+    case DATE_QUARTERS:
+      return add_months (date, trunc (quantity) * 3, method);
+
+    case DATE_MONTHS:
+      return add_months (date, trunc (quantity), method);
+
+    case DATE_WEEKS:
+    case DATE_DAYS:
+    case DATE_HOURS:
+    case DATE_MINUTES:
+    case DATE_SECONDS:
+      return date + quantity * date_unit_duration (unit);
+    }
+
+  NOT_REACHED ();
+}
 
 int
 compare_string (const struct substring *a, const struct substring *b) 
index 46ed5e4a79e03b4a698b87475ce8275b70061793..b8fadc9d245daee3353660d15ec633751f929549 100644 (file)
@@ -45,6 +45,7 @@ static inline double check_errno (double x)
 #define H_MIN 60.                       /* Minutes per hour. */
 #define MIN_S 60.                       /* Seconds per minute. */
 #define WEEK_DAY 7.                     /* Days per week. */
+#define WEEK_S (WEEK_DAY * DAY_S)       /* Seconds per week. */
 
 extern const struct substring empty_string;
 
@@ -55,6 +56,10 @@ double expr_ymd_to_ofs (double year, double month, double day);
 double expr_wkyr_to_date (double wk, double yr);
 double expr_yrday_to_date (double yr, double day);
 double expr_yrmoda (double year, double month, double day);
+double expr_date_difference (double date1, double date2,
+                             struct substring unit);
+double expr_date_sum (double date, double quantity, struct substring unit_name,
+                      struct substring method_name);
 
 struct substring alloc_string (struct expression *, size_t length);
 struct substring copy_string (struct expression *,
index 6f0bd0e1430a3f4bcbf1a0742fef8303bb6918ab..35f5d3666ee721a2c0cc9a0cd30baaee9371e893 100644 (file)
@@ -319,10 +319,13 @@ function XDATE.WKDAY (date >= DAY_S) = calendar_offset_to_wday (date / DAY_S);
 function XDATE.YEAR (date >= DAY_S) = calendar_offset_to_year (date / DAY_S);
 
 // Date arithmetic functions.
-function DATEDIFF (date1, date2, string unit) = unimplemented;
-function DATESUM (date, quantity, string unit) = unimplemented;
-function DATESUM (date, quantity, string unit, string roll_over)
-     = unimplemented;
+no_abbrev function DATEDIFF (date1 >= DAY_S, date2 >= DAY_S, string unit)
+     = expr_date_difference (date1, date2, unit);
+no_abbrev function DATESUM (date, quantity, string unit)
+     = expr_date_sum (date, quantity, unit, ss_cstr ("closest"));
+no_abbrev function DATESUM (date, quantity, string unit, string method)
+     = expr_date_sum (date, quantity, unit, method);
+
 
 // String functions.
 string function CONCAT (string a[n])
@@ -613,7 +616,7 @@ absorb_miss string function SUBSTR (string s, ofs, cnt)
     return empty_string;
 }
 
-absorb_miss no_opt string function VALUELABEL (var v)
+absorb_miss no_opt no_abbrev string function VALUELABEL (var v)
      expression e;
      case c;
 {
index 6e13ff084e04dede648dbccaf1fb63498beb268d..fe424a1ee9d708ac75da0ab0031be924c1e13013 100644 (file)
@@ -1,3 +1,7 @@
+Wed Dec 13 19:34:29 2006  Ben Pfaff  <blp@gnu.org>
+
+       * expressions/expressions.sh: Test DATEDIFF, DATESUM functions.
+
 Sun Dec 10 16:52:04 2006  Ben Pfaff  <blp@gnu.org>
 
        * automake.mk: Add new test.
index 70049e13f8900bb2b50e409ec1d0091cd005e662..88c9bafae9b0b1d89d386d9ef884ec4469624ff5 100755 (executable)
@@ -1261,6 +1261,263 @@ xdate.year(date.mdy(2,25,96) + time.hms(21,30,57)) => 1996.00
 xdate.year(date.mdy(11,10,2038) + time.hms(22,30,4)) => 2038.00
 xdate.year(date.mdy(7,18,2094) + time.hms(1,56,51)) => 2094.00
 
+datediff(date.mdy(6,10,1648), date.mdy(6,30,1680), 'years') => 32.00
+datediff(date.mdy(6,30,1680), date.mdy(7,24,1716), 'years') => 36.00
+datediff(date.mdy(7,24,1716), date.mdy(6,19,1768), 'years') => 51.00
+datediff(date.mdy(6,19,1768), date.mdy(8,2,1819), 'years') => 51.00
+datediff(date.mdy(8,2,1819), date.mdy(3,27,1839), 'years') => 19.00
+datediff(date.mdy(3,27,1839), date.mdy(4,19,1903), 'years') => 64.00
+datediff(date.mdy(4,19,1903), date.mdy(8,25,1929), 'years') => 26.00
+datediff(date.mdy(8,25,1929), date.mdy(9,29,1941), 'years') => 12.00
+datediff(date.mdy(9,29,1941), date.mdy(4,19,1943), 'years') => 1.00
+datediff(date.mdy(4,19,1943), date.mdy(10,7,1943), 'years') => 0.00
+datediff(date.mdy(10,7,1943), date.mdy(3,17,1992), 'years') => 48.00
+datediff(date.mdy(3,17,1992), date.mdy(2,25,1996), 'years') => 3.00
+datediff(date.mdy(9,29,41), date.mdy(2,25,1996), 'years') => 54.00
+datediff(date.mdy(9,29,41), date.mdy(4,19,43), 'years') => 1.00
+datediff(date.mdy(4,19,43), date.mdy(10,7,43), 'years') => 0.00
+datediff(date.mdy(10,7,43), date.mdy(3,17,92), 'years') => 48.00
+datediff(date.mdy(3,17,92), date.mdy(2,25,96), 'years') => 3.00
+datediff(date.mdy(2,25,96), date.mdy(11,10,2038), 'years') => 42.00
+datediff(date.mdy(11,10,2038), date.mdy(7,18,2094), 'years') => 55.00
+datediff(date.mdy(2,29,1900), date.mdy(2,29,1904), 'years') => 3.00
+datediff(date.mdy(2,29,1904), date.mdy(2,29,1908), 'years') => 4.00
+datediff(date.mdy(2,29,1900), date.mdy(2,28,1903), 'years') => 2.00
+
+datediff(date.mdy(6,10,1648), date.mdy(6,30,1680), 'quarters') => 128.00
+datediff(date.mdy(6,30,1680), date.mdy(7,24,1716), 'quarters') => 144.00
+datediff(date.mdy(7,24,1716), date.mdy(6,19,1768), 'quarters') => 207.00
+datediff(date.mdy(6,19,1768), date.mdy(8,2,1819), 'quarters') => 204.00
+datediff(date.mdy(8,2,1819), date.mdy(3,27,1839), 'quarters') => 78.00
+datediff(date.mdy(3,27,1839), date.mdy(4,19,1903), 'quarters') => 256.00
+datediff(date.mdy(4,19,1903), date.mdy(8,25,1929), 'quarters') => 105.00
+datediff(date.mdy(8,25,1929), date.mdy(9,29,1941), 'quarters') => 48.00
+datediff(date.mdy(9,29,1941), date.mdy(4,19,1943), 'quarters') => 6.00
+datediff(date.mdy(4,19,1943), date.mdy(10,7,1943), 'quarters') => 1.00
+datediff(date.mdy(10,7,1943), date.mdy(3,17,1992), 'quarters') => 193.00
+datediff(date.mdy(3,17,1992), date.mdy(2,25,1996), 'quarters') => 15.00
+datediff(date.mdy(9,29,41), date.mdy(2,25,1996), 'quarters') => 217.00
+datediff(date.mdy(9,29,41), date.mdy(4,19,43), 'quarters') => 6.00
+datediff(date.mdy(4,19,43), date.mdy(10,7,43), 'quarters') => 1.00
+datediff(date.mdy(10,7,43), date.mdy(3,17,92), 'quarters') => 193.00
+datediff(date.mdy(3,17,92), date.mdy(2,25,96), 'quarters') => 15.00
+datediff(date.mdy(2,25,96), date.mdy(11,10,2038), 'quarters') => 170.00
+datediff(date.mdy(11,10,2038), date.mdy(7,18,2094), 'quarters') => 222.00
+datediff(date.mdy(2,29,1900), date.mdy(2,29,1904), 'quarters') => 15.00
+datediff(date.mdy(2,29,1904), date.mdy(2,29,1908), 'quarters') => 16.00
+datediff(date.mdy(2,29,1900), date.mdy(2,28,1903), 'quarters') => 11.00
+
+datediff(date.mdy(6,10,1648), date.mdy(6,30,1680), 'months') => 384.00
+datediff(date.mdy(6,30,1680), date.mdy(7,24,1716), 'months') => 432.00
+datediff(date.mdy(7,24,1716), date.mdy(6,19,1768), 'months') => 622.00
+datediff(date.mdy(6,19,1768), date.mdy(8,2,1819), 'months') => 613.00
+datediff(date.mdy(8,2,1819), date.mdy(3,27,1839), 'months') => 235.00
+datediff(date.mdy(3,27,1839), date.mdy(4,19,1903), 'months') => 768.00
+datediff(date.mdy(4,19,1903), date.mdy(8,25,1929), 'months') => 316.00
+datediff(date.mdy(8,25,1929), date.mdy(9,29,1941), 'months') => 145.00
+datediff(date.mdy(9,29,1941), date.mdy(4,19,1943), 'months') => 18.00
+datediff(date.mdy(4,19,1943), date.mdy(10,7,1943), 'months') => 5.00
+datediff(date.mdy(10,7,1943), date.mdy(3,17,1992), 'months') => 581.00
+datediff(date.mdy(3,17,1992), date.mdy(2,25,1996), 'months') => 47.00
+datediff(date.mdy(9,29,41), date.mdy(2,25,1996), 'months') => 652.00
+datediff(date.mdy(9,29,41), date.mdy(4,19,43), 'months') => 18.00
+datediff(date.mdy(4,19,43), date.mdy(10,7,43), 'months') => 5.00
+datediff(date.mdy(10,7,43), date.mdy(3,17,92), 'months') => 581.00
+datediff(date.mdy(3,17,92), date.mdy(2,25,96), 'months') => 47.00
+datediff(date.mdy(2,25,96), date.mdy(11,10,2038), 'months') => 512.00
+datediff(date.mdy(11,10,2038), date.mdy(7,18,2094), 'months') => 668.00
+datediff(date.mdy(2,29,1900), date.mdy(2,29,1904), 'months') => 47.00
+datediff(date.mdy(2,29,1904), date.mdy(2,29,1908), 'months') => 48.00
+datediff(date.mdy(2,29,1900), date.mdy(2,28,1903), 'months') => 35.00
+
+datediff(date.mdy(6,10,1648), date.mdy(6,30,1680), 'weeks') => 1672.00
+datediff(date.mdy(6,30,1680), date.mdy(7,24,1716), 'weeks') => 1881.00
+datediff(date.mdy(7,24,1716), date.mdy(6,19,1768), 'weeks') => 2708.00
+datediff(date.mdy(6,19,1768), date.mdy(8,2,1819), 'weeks') => 2667.00
+datediff(date.mdy(8,2,1819), date.mdy(3,27,1839), 'weeks') => 1025.00
+datediff(date.mdy(3,27,1839), date.mdy(4,19,1903), 'weeks') => 3342.00
+datediff(date.mdy(4,19,1903), date.mdy(8,25,1929), 'weeks') => 1375.00
+datediff(date.mdy(8,25,1929), date.mdy(9,29,1941), 'weeks') => 631.00
+datediff(date.mdy(9,29,1941), date.mdy(4,19,1943), 'weeks') => 81.00
+datediff(date.mdy(4,19,1943), date.mdy(10,7,1943), 'weeks') => 24.00
+datediff(date.mdy(10,7,1943), date.mdy(3,17,1992), 'weeks') => 2527.00
+datediff(date.mdy(3,17,1992), date.mdy(2,25,1996), 'weeks') => 205.00
+datediff(date.mdy(9,29,41), date.mdy(2,25,1996), 'weeks') => 2838.00
+datediff(date.mdy(9,29,41), date.mdy(4,19,43), 'weeks') => 81.00
+datediff(date.mdy(4,19,43), date.mdy(10,7,43), 'weeks') => 24.00
+datediff(date.mdy(10,7,43), date.mdy(3,17,92), 'weeks') => 2527.00
+datediff(date.mdy(3,17,92), date.mdy(2,25,96), 'weeks') => 205.00
+datediff(date.mdy(2,25,96), date.mdy(11,10,2038), 'weeks') => 2228.00
+datediff(date.mdy(11,10,2038), date.mdy(7,18,2094), 'weeks') => 2905.00
+datediff(date.mdy(2,29,1900), date.mdy(2,29,1904), 'weeks') => 208.00
+datediff(date.mdy(2,29,1904), date.mdy(2,29,1908), 'weeks') => 208.00
+datediff(date.mdy(2,29,1900), date.mdy(2,28,1903), 'weeks') => 156.00
+
+datediff(date.mdy(6,10,1648), date.mdy(6,30,1680), 'days') => 11708.00
+datediff(date.mdy(6,30,1680), date.mdy(7,24,1716), 'days') => 13172.00
+datediff(date.mdy(7,24,1716), date.mdy(6,19,1768), 'days') => 18958.00
+datediff(date.mdy(6,19,1768), date.mdy(8,2,1819), 'days') => 18670.00
+datediff(date.mdy(8,2,1819), date.mdy(3,27,1839), 'days') => 7177.00
+datediff(date.mdy(3,27,1839), date.mdy(4,19,1903), 'days') => 23398.00
+datediff(date.mdy(4,19,1903), date.mdy(8,25,1929), 'days') => 9625.00
+datediff(date.mdy(8,25,1929), date.mdy(9,29,1941), 'days') => 4418.00
+datediff(date.mdy(9,29,1941), date.mdy(4,19,1943), 'days') => 567.00
+datediff(date.mdy(4,19,1943), date.mdy(10,7,1943), 'days') => 171.00
+datediff(date.mdy(10,7,1943), date.mdy(3,17,1992), 'days') => 17694.00
+datediff(date.mdy(3,17,1992), date.mdy(2,25,1996), 'days') => 1440.00
+datediff(date.mdy(9,29,41), date.mdy(2,25,1996), 'days') => 19872.00
+datediff(date.mdy(9,29,41), date.mdy(4,19,43), 'days') => 567.00
+datediff(date.mdy(4,19,43), date.mdy(10,7,43), 'days') => 171.00
+datediff(date.mdy(10,7,43), date.mdy(3,17,92), 'days') => 17694.00
+datediff(date.mdy(3,17,92), date.mdy(2,25,96), 'days') => 1440.00
+datediff(date.mdy(2,25,96), date.mdy(11,10,2038), 'days') => 15599.00
+datediff(date.mdy(11,10,2038), date.mdy(7,18,2094), 'days') => 20339.00
+datediff(date.mdy(2,29,1900), date.mdy(2,29,1904), 'days') => 1460.00
+datediff(date.mdy(2,29,1904), date.mdy(2,29,1908), 'days') => 1461.00
+datediff(date.mdy(2,29,1900), date.mdy(2,28,1903), 'days') => 1094.00
+
+datediff(date.mdy(6,30,1680), date.mdy(6,10,1648), 'years') => -32.00
+datediff(date.mdy(7,24,1716), date.mdy(6,30,1680), 'years') => -36.00
+datediff(date.mdy(6,19,1768), date.mdy(7,24,1716), 'years') => -51.00
+datediff(date.mdy(8,2,1819), date.mdy(6,19,1768), 'years') => -51.00
+datediff(date.mdy(3,27,1839), date.mdy(8,2,1819), 'years') => -19.00
+datediff(date.mdy(4,19,1903), date.mdy(3,27,1839), 'years') => -64.00
+datediff(date.mdy(8,25,1929), date.mdy(4,19,1903), 'years') => -26.00
+datediff(date.mdy(9,29,1941), date.mdy(8,25,1929), 'years') => -12.00
+datediff(date.mdy(4,19,1943), date.mdy(9,29,1941), 'years') => -1.00
+datediff(date.mdy(10,7,1943), date.mdy(4,19,1943), 'years') => 0.00
+datediff(date.mdy(3,17,1992), date.mdy(10,7,1943), 'years') => -48.00
+datediff(date.mdy(2,25,1996), date.mdy(3,17,1992), 'years') => -3.00
+datediff(date.mdy(2,25,1996), date.mdy(9,29,41), 'years') => -54.00
+datediff(date.mdy(4,19,43), date.mdy(9,29,41), 'years') => -1.00
+datediff(date.mdy(10,7,43), date.mdy(4,19,43), 'years') => 0.00
+datediff(date.mdy(3,17,92), date.mdy(10,7,43), 'years') => -48.00
+datediff(date.mdy(2,25,96), date.mdy(3,17,92), 'years') => -3.00
+datediff(date.mdy(11,10,2038), date.mdy(2,25,96), 'years') => -42.00
+datediff(date.mdy(7,18,2094), date.mdy(11,10,2038), 'years') => -55.00
+datediff(date.mdy(2,29,1904), date.mdy(2,29,1900), 'years') => -3.00
+datediff(date.mdy(2,29,1908), date.mdy(2,29,1904), 'years') => -4.00
+datediff(date.mdy(2,28,1903), date.mdy(2,29,1900), 'years') => -2.00
+
+datediff(date.mdy(6,30,1680), date.mdy(6,10,1648), 'months') => -384.00
+datediff(date.mdy(7,24,1716), date.mdy(6,30,1680), 'months') => -432.00
+datediff(date.mdy(6,19,1768), date.mdy(7,24,1716), 'months') => -622.00
+datediff(date.mdy(8,2,1819), date.mdy(6,19,1768), 'months') => -613.00
+datediff(date.mdy(3,27,1839), date.mdy(8,2,1819), 'months') => -235.00
+datediff(date.mdy(4,19,1903), date.mdy(3,27,1839), 'months') => -768.00
+datediff(date.mdy(8,25,1929), date.mdy(4,19,1903), 'months') => -316.00
+datediff(date.mdy(9,29,1941), date.mdy(8,25,1929), 'months') => -145.00
+datediff(date.mdy(4,19,1943), date.mdy(9,29,1941), 'months') => -18.00
+datediff(date.mdy(10,7,1943), date.mdy(4,19,1943), 'months') => -5.00
+datediff(date.mdy(3,17,1992), date.mdy(10,7,1943), 'months') => -581.00
+datediff(date.mdy(2,25,1996), date.mdy(3,17,1992), 'months') => -47.00
+datediff(date.mdy(2,25,1996), date.mdy(9,29,41), 'months') => -652.00
+datediff(date.mdy(4,19,43), date.mdy(9,29,41), 'months') => -18.00
+datediff(date.mdy(10,7,43), date.mdy(4,19,43), 'months') => -5.00
+datediff(date.mdy(3,17,92), date.mdy(10,7,43), 'months') => -581.00
+datediff(date.mdy(2,25,96), date.mdy(3,17,92), 'months') => -47.00
+datediff(date.mdy(11,10,2038), date.mdy(2,25,96), 'months') => -512.00
+datediff(date.mdy(7,18,2094), date.mdy(11,10,2038), 'months') => -668.00
+datediff(date.mdy(2,29,1904), date.mdy(2,29,1900), 'months') => -47.00
+datediff(date.mdy(2,29,1908), date.mdy(2,29,1904), 'months') => -48.00
+datediff(date.mdy(2,28,1903), date.mdy(2,29,1900), 'months') => -35.00
+
+datediff(date.mdy(6,30,1680), date.mdy(6,10,1648), 'quarters') => -128.00
+datediff(date.mdy(7,24,1716), date.mdy(6,30,1680), 'quarters') => -144.00
+datediff(date.mdy(6,19,1768), date.mdy(7,24,1716), 'quarters') => -207.00
+datediff(date.mdy(8,2,1819), date.mdy(6,19,1768), 'quarters') => -204.00
+datediff(date.mdy(3,27,1839), date.mdy(8,2,1819), 'quarters') => -78.00
+datediff(date.mdy(4,19,1903), date.mdy(3,27,1839), 'quarters') => -256.00
+datediff(date.mdy(8,25,1929), date.mdy(4,19,1903), 'quarters') => -105.00
+datediff(date.mdy(9,29,1941), date.mdy(8,25,1929), 'quarters') => -48.00
+datediff(date.mdy(4,19,1943), date.mdy(9,29,1941), 'quarters') => -6.00
+datediff(date.mdy(10,7,1943), date.mdy(4,19,1943), 'quarters') => -1.00
+datediff(date.mdy(3,17,1992), date.mdy(10,7,1943), 'quarters') => -193.00
+datediff(date.mdy(2,25,1996), date.mdy(3,17,1992), 'quarters') => -15.00
+datediff(date.mdy(2,25,1996), date.mdy(9,29,41), 'quarters') => -217.00
+datediff(date.mdy(4,19,43), date.mdy(9,29,41), 'quarters') => -6.00
+datediff(date.mdy(10,7,43), date.mdy(4,19,43), 'quarters') => -1.00
+datediff(date.mdy(3,17,92), date.mdy(10,7,43), 'quarters') => -193.00
+datediff(date.mdy(2,25,96), date.mdy(3,17,92), 'quarters') => -15.00
+datediff(date.mdy(11,10,2038), date.mdy(2,25,96), 'quarters') => -170.00
+datediff(date.mdy(7,18,2094), date.mdy(11,10,2038), 'quarters') => -222.00
+datediff(date.mdy(2,29,1904), date.mdy(2,29,1900), 'quarters') => -15.00
+datediff(date.mdy(2,29,1908), date.mdy(2,29,1904), 'quarters') => -16.00
+datediff(date.mdy(2,28,1903), date.mdy(2,29,1900), 'quarters') => -11.00
+
+# DATESUM with non-leap year
+ctime.days(datesum(date.mdy(1,31,1900), 1, 'months') - date.mdy(1,1,1900)) => 58.00
+ctime.days(datesum(date.mdy(1,31,1900), 2, 'months') - date.mdy(1,1,1900)) => 89.00
+ctime.days(datesum(date.mdy(1,31,1900), 3, 'months') - date.mdy(1,1,1900)) => 119.00
+ctime.days(datesum(date.mdy(1,31,1900), 4, 'months') - date.mdy(1,1,1900)) => 150.00
+ctime.days(datesum(date.mdy(1,31,1900), 5.4, 'months') - date.mdy(1,1,1900)) => 180.00
+ctime.days(datesum(date.mdy(1,31,1900), 6, 'months') - date.mdy(1,1,1900)) => 211.00
+ctime.days(datesum(date.mdy(1,31,1900), 7, 'months') - date.mdy(1,1,1900)) => 242.00
+ctime.days(datesum(date.mdy(1,31,1900), 8, 'months') - date.mdy(1,1,1900)) => 272.00
+ctime.days(datesum(date.mdy(1,31,1900), 9, 'months') - date.mdy(1,1,1900)) => 303.00
+ctime.days(datesum(date.mdy(1,31,1900), 10, 'months') - date.mdy(1,1,1900)) => 333.00
+ctime.days(datesum(date.mdy(1,31,1900), 11, 'months') - date.mdy(1,1,1900)) => 364.00
+ctime.days(datesum(date.mdy(1,31,1900), 12, 'months') - date.mdy(1,1,1900)) => 395.00
+ctime.days(datesum(date.mdy(1,31,1900), 13.9, 'months') - date.mdy(1,1,1900)) => 423.00
+ctime.days(datesum(date.mdy(1,31,1900), 1, 'months', 'rollover') - date.mdy(1,1,1900)) => 61.00
+ctime.days(datesum(date.mdy(1,31,1900), 2, 'months', 'rollover') - date.mdy(1,1,1900)) => 89.00
+ctime.days(datesum(date.mdy(1,31,1900), 3.2, 'months', 'rollover') - date.mdy(1,1,1900)) => 120.00
+ctime.days(datesum(date.mdy(1,31,1900), 4, 'months', 'rollover') - date.mdy(1,1,1900)) => 150.00
+ctime.days(datesum(date.mdy(1,31,1900), 5, 'months', 'rollover') - date.mdy(1,1,1900)) => 181.00
+ctime.days(datesum(date.mdy(1,31,1900), 6, 'months', 'rollover') - date.mdy(1,1,1900)) => 211.00
+ctime.days(datesum(date.mdy(1,31,1900), 7, 'months', 'rollover') - date.mdy(1,1,1900)) => 242.00
+ctime.days(datesum(date.mdy(1,31,1900), 8, 'months', 'rollover') - date.mdy(1,1,1900)) => 273.00
+ctime.days(datesum(date.mdy(1,31,1900), 9, 'months', 'rollover') - date.mdy(1,1,1900)) => 303.00
+ctime.days(datesum(date.mdy(1,31,1900), 10, 'months', 'rollover') - date.mdy(1,1,1900)) => 334.00
+ctime.days(datesum(date.mdy(1,31,1900), 11, 'months', 'rollover') - date.mdy(1,1,1900)) => 364.00
+ctime.days(datesum(date.mdy(1,31,1900), 12, 'months', 'rollover') - date.mdy(1,1,1900)) => 395.00
+ctime.days(datesum(date.mdy(1,31,1900), 13, 'months', 'rollover') - date.mdy(1,1,1900)) => 426.00
+
+# DATESUM with leap year
+ctime.days(datesum(date.mdy(1,31,1904), 1, 'months') - date.mdy(1,1,1904)) => 59.00
+ctime.days(datesum(date.mdy(1,31,1904), 2.5, 'months') - date.mdy(1,1,1904)) => 90.00
+ctime.days(datesum(date.mdy(1,31,1904), 3, 'months') - date.mdy(1,1,1904)) => 120.00
+ctime.days(datesum(date.mdy(1,31,1904), 4.9, 'months') - date.mdy(1,1,1904)) => 151.00
+ctime.days(datesum(date.mdy(1,31,1904), 5.1, 'months') - date.mdy(1,1,1904)) => 181.00
+ctime.days(datesum(date.mdy(1,31,1904), 6, 'months') - date.mdy(1,1,1904)) => 212.00
+ctime.days(datesum(date.mdy(1,31,1904), 7, 'months') - date.mdy(1,1,1904)) => 243.00
+ctime.days(datesum(date.mdy(1,31,1904), 8, 'months') - date.mdy(1,1,1904)) => 273.00
+ctime.days(datesum(date.mdy(1,31,1904), 9, 'months') - date.mdy(1,1,1904)) => 304.00
+ctime.days(datesum(date.mdy(1,31,1904), 10, 'months') - date.mdy(1,1,1904)) => 334.00
+ctime.days(datesum(date.mdy(1,31,1904), 11, 'months') - date.mdy(1,1,1904)) => 365.00
+ctime.days(datesum(date.mdy(1,31,1904), 12, 'months') - date.mdy(1,1,1904)) => 396.00
+ctime.days(datesum(date.mdy(1,31,1904), 13, 'months') - date.mdy(1,1,1904)) => 424.00
+ctime.days(datesum(date.mdy(1,31,1904), 1, 'months', 'rollover') - date.mdy(1,1,1904)) => 61.00
+ctime.days(datesum(date.mdy(1,31,1904), 2, 'months', 'rollover') - date.mdy(1,1,1904)) => 90.00
+ctime.days(datesum(date.mdy(1,31,1904), 3, 'months', 'rollover') - date.mdy(1,1,1904)) => 121.00
+ctime.days(datesum(date.mdy(1,31,1904), 4, 'months', 'rollover') - date.mdy(1,1,1904)) => 151.00
+ctime.days(datesum(date.mdy(1,31,1904), 5, 'months', 'rollover') - date.mdy(1,1,1904)) => 182.00
+ctime.days(datesum(date.mdy(1,31,1904), 6, 'months', 'rollover') - date.mdy(1,1,1904)) => 212.00
+ctime.days(datesum(date.mdy(1,31,1904), 7, 'months', 'rollover') - date.mdy(1,1,1904)) => 243.00
+ctime.days(datesum(date.mdy(1,31,1904), 8, 'months', 'rollover') - date.mdy(1,1,1904)) => 274.00
+ctime.days(datesum(date.mdy(1,31,1904), 9, 'months', 'rollover') - date.mdy(1,1,1904)) => 304.00
+ctime.days(datesum(date.mdy(1,31,1904), 10, 'months', 'rollover') - date.mdy(1,1,1904)) => 335.00
+ctime.days(datesum(date.mdy(1,31,1904), 11, 'months', 'rollover') - date.mdy(1,1,1904)) => 365.00
+ctime.days(datesum(date.mdy(1,31,1904), 12, 'months', 'rollover') - date.mdy(1,1,1904)) => 396.00
+ctime.days(datesum(date.mdy(1,31,1904), 13, 'months', 'rollover') - date.mdy(1,1,1904)) => 427.00
+
+ctime.days(datesum(date.mdy(6,10,1648), 1, 'weeks') - date.mdy(6,10,1648)) => 7.00
+ctime.days(datesum(date.mdy(6,30,1680), 2.5, 'weeks') - date.mdy(6,30,1680)) => 17.50
+ctime.days(datesum(date.mdy(7,24,1716), -3, 'weeks') - date.mdy(7,24,1716)) => -21.00
+ctime.days(datesum(date.mdy(6,19,1768), 4, 'weeks') - date.mdy(6,19,1768)) => 28.00
+ctime.days(datesum(date.mdy(8,2,1819), 5, 'weeks') - date.mdy(8,2,1819)) => 35.00
+
+ctime.days(datesum(date.mdy(6,10,1648), 1, 'days') - date.mdy(6,10,1648)) => 1.00
+ctime.days(datesum(date.mdy(6,30,1680), 2.5, 'days') - date.mdy(6,30,1680)) => 2.50
+ctime.days(datesum(date.mdy(7,24,1716), -3, 'days') - date.mdy(7,24,1716)) => -3.00
+ctime.days(datesum(date.mdy(6,19,1768), 4, 'days') - date.mdy(6,19,1768)) => 4.00
+ctime.days(datesum(date.mdy(8,2,1819), 5, 'days') - date.mdy(8,2,1819)) => 5.00
+
+ctime.days(datesum(date.mdy(6,10,1648), 1, 'hours') - date.mdy(6,10,1648)) => 0.04
+ctime.days(datesum(date.mdy(6,30,1680), 2.5, 'hours') - date.mdy(6,30,1680)) => 0.10
+ctime.days(datesum(date.mdy(6,19,1768), -4, 'hours') - date.mdy(6,19,1768)) => -0.17
+ctime.days(datesum(date.mdy(8,2,1819), 5, 'hours') - date.mdy(8,2,1819)) => 0.21
+
 # These test values are from Applied Statistics, Algorithm AS 310.
 1000 * ncdf.beta(.868,10,20,150) => 937.66
 1000 * ncdf.beta(.9,10,10,120) => 730.68