delorie.com/archives/browse.cgi   search  
Mail Archives: djgpp-workers/2008/06/07/08:51:18

X-Authentication-Warning: delorie.com: mail set sender to djgpp-workers-bounces using -f
X-Recipient: djgpp-workers AT delorie DOT com
X-Authenticated: #27081556
X-Provags-ID: V01U2FsdGVkX1/MlwlAJTfM517k1g5fJ74URTY4rhGRZgm5xq7fgM
ASwn75aHqQSc/w
From: Juan Manuel Guerrero <juan DOT guerrero AT gmx DOT de>
To: djgpp-workers AT delorie DOT com
Subject: Implementation of some conversion specifiers for strftime().
Date: Sat, 7 Jun 2008 14:54:13 +0200
User-Agent: KMail/1.9.5
MIME-Version: 1.0
Message-Id: <200806071454.15316.juan.guerrero@gmx.de>
X-Y-GMX-Trusted: 0
Reply-To: djgpp-workers AT delorie DOT com

According to:
  <http://www.opengroup.org/onlinepubs/000095399/functions/strftime.html>
the following conversion specifiers are missed in the current implementation of
strftime(): %F, %G, %g and %V.  They all concern the year, week and date
representation according to ISO 8601:2000.

A comparision with GNU libc shows that also the %P and %s specifiers are missed.
The %s specifier computes the seconds since 1970-01-01 using mktime().  For some
reason the result is different from what I get on my linux box, but it matches the
result that was expected by the test file.  I have not investigated this further.
Also the GNU specific # flag, that changes the case for certain conversion specifiers
has been implemented.
The parsing of the format string now allows any order of the flags.

The documentation has been adjusted accordingly.  Also all still not documented flags
has been explained.

The test file has been modified to check the flags, the new modifiers E and O implemented
in a previous patch and the new conversion specifiers.  The result produced by the test
file has been compared with the result produced by the test file if compiled on my linux box.
The only difference is the result produced by %s.

Please inspect wording of the texi file.
Comments, objections, suggestions are welcome.


Regards,
Juan M. Guerrero




2008-06-07  Juan Manuel Guerrero  <juan DOT guerrero AT gmx DOT de>
	Diffs against djgpp CVS head of 2008-05-30.


	* src/libc/ansi/time/strftime.c: Added the following specifiers: %F, %G, %g, %P, %s and %V.
	 Added the following flag: #.

	* src/libc/ansi/time/strftime.txi: Document the specifiers: %F, %G, %g, %P, %s and %V and
	flag: #.

	* src/docs/kb/wc204.txh: Info about %F, %G, %g, %P, %s and %V specifiers and # flag added.

	* tests/libc/ansi/time/strftime.c: Tests for %F, %G, %g, %P, %s and %V specifiers, E and O
	modifiers and # flag added.






Index: src/docs/kb/wc204.txi
===================================================================
RCS file: /cvs/djgpp/djgpp/src/docs/kb/wc204.txi,v
retrieving revision 1.185
diff -p -U3 -r1.185 wc204.txi
--- src/docs/kb/wc204.txi	30 May 2008 20:57:19 -0000	1.185
+++ src/docs/kb/wc204.txi	7 Jun 2008 12:43:26 -0000
@@ -1145,5 +1145,9 @@ are now supported by @code{_doprnt} and 
 family of functions.
 
 @findex strftime AT r{, and C99 conversion modifiers}
-The modifiers @code{%}@code{E} and @code{%}@code{O} of the conversion specifiers
-are ignored because djgpp only supports C/POSIX locale.
+The modifiers @code{%E} and @code{%O} of the conversion specifiers are ignored
+because djgpp only supports C/POSIX locale.
+
+@findex strftime AT r{, and C99 conversion specifiers}
+The conversion specifiers @code{%F}, @code{%G}, @code{%g}, @code{%P}, @code{%s}, and @code{%V} have been added.
+Also the @code{#} flag has been added.
Index: src/libc/ansi/time/strftime.c
===================================================================
RCS file: /cvs/djgpp/djgpp/src/libc/ansi/time/strftime.c,v
retrieving revision 1.7
diff -p -U3 -r1.7 strftime.c
--- src/libc/ansi/time/strftime.c	30 May 2008 20:57:22 -0000	1.7
+++ src/libc/ansi/time/strftime.c	7 Jun 2008 12:43:28 -0000
@@ -8,7 +8,17 @@
 #include <time.h>
 #include <ctype.h>
 
-#define TM_YEAR_BASE 1900
+#undef  FALSE
+#define FALSE          0
+#undef  TRUE
+#define TRUE           1
+
+#define THURSDAY       4
+#define SATURDAY       6
+#define SUNDAY         7
+
+#define TM_YEAR_BASE   1900
+#define IS_LEAP(year)  ((((year) % 4) == 0) && ((((year) % 100) != 0) || (((year) % 400) == 0)))
 
 static const char *afmt[] = {
   "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
@@ -25,12 +35,97 @@ static const char *Bfmt[] = {
   "January", "February", "March", "April", "May", "June", "July",
   "August", "September", "October", "November", "December",
 };
+static const char __ISO8601_date_format[] = "%Y-%m-%d";
 char __dj_date_format[10] = "%m/%d/%y";
 char __dj_time_format[16] = "%H:%M:%S";
 
 static size_t gsize;
 static char *pt;
 
+static __inline__ int
+_compute_iso_wday_of_jan_01(const struct tm *t)
+{
+  /*
+   *  ISO week starts with Monday = 1 and ends with Sunday = 7.
+   */
+
+  int wday_jan_01;
+
+
+  wday_jan_01 = (7 + t->tm_wday - t->tm_yday % 7) % 7;
+  if (wday_jan_01 == 0)
+    wday_jan_01 = 7;
+
+  return wday_jan_01;
+}
+
+static int
+_compute_iso_standard_week(const struct tm *t)
+{
+  /*
+   *  In ISO 8601:2000 standard week-based year system,
+   *  weeks begin on a Monday and week 1 of the year ismore 
+   *  the week that includes January 4th, which is also
+   *  the week that includes the first Thursday of the
+   *  year, and is also the first week that contains at
+   *  least four days in the year.
+   */
+
+  int iso_wday_of_jan_01, iso_week;
+
+
+  iso_wday_of_jan_01 = _compute_iso_wday_of_jan_01(t);  /*  Mon = 1, ..., Sun = 7.  */
+  iso_week = (6 + t->tm_yday - (6 + t->tm_wday) % 7) / 7;
+  if (iso_week == 0 && iso_wday_of_jan_01 > THURSDAY)   /*  Week belongs to the previous year.  */
+  {
+    if ((iso_wday_of_jan_01 == SUNDAY) ||
+        (iso_wday_of_jan_01 == SATURDAY && !IS_LEAP(t->tm_year - 1 + TM_YEAR_BASE)))
+      iso_week = 52;
+    else
+      iso_week = 53;
+  }
+  else
+  {
+    int is_leap_year = IS_LEAP(t->tm_year + TM_YEAR_BASE);
+    int iso_wday_of_dec_31 = (365 + is_leap_year - t->tm_yday + (6 + t->tm_wday) % 7) % 7;  /*  Mon = 1, ..., Sun = 7.  */
+
+    if (t->tm_yday > (360 + is_leap_year) && iso_wday_of_dec_31 < THURSDAY)  /*  Belongs to the following year.  */
+      iso_week = 1;
+    else  /*  Belongs to the current year.  */
+      iso_week++;
+  }
+
+  return iso_week;
+}
+
+static int
+_compute_iso_week_based_year(const struct tm *t)
+{
+  /*
+   *  ISO 8601:2000 standard week-based year system.
+   */
+
+  int iso_wday_of_jan_01, iso_year, week;
+
+
+  iso_wday_of_jan_01 = _compute_iso_wday_of_jan_01(t);  /*  Mon = 1, ..., Sun = 7.  */
+  week = (6 + t->tm_yday - (6 + t->tm_wday) % 7) / 7;
+  if (week == 0 && iso_wday_of_jan_01 > THURSDAY)  /*  Belongs to the previous year.  */
+    iso_year = t->tm_year - 1 + TM_YEAR_BASE;
+  else
+  {
+    int is_leap_year = IS_LEAP(t->tm_year + TM_YEAR_BASE);
+    int iso_wday_of_dec_31 = (365 + is_leap_year - t->tm_yday + (6 + t->tm_wday) % 7) % 7;  /*  Mon = 1, ..., Sun = 7.  */
+
+    if (t->tm_yday > (360 + is_leap_year) && iso_wday_of_dec_31 < THURSDAY)  /*  Belongs to the following year.  */
+      iso_year = t->tm_year + 1 + TM_YEAR_BASE;
+    else  /*  Belongs to the current year.  */
+      iso_year = t->tm_year + TM_YEAR_BASE;
+  }
+
+  return iso_year;
+}
+
 static int
 _add(const char *str, int upcase)
 {
@@ -70,15 +165,24 @@ _fmt(const char *format, const struct tm
   {
     if (*format == '%')
     {
-      int pad = '0', space=' ';
-      if (format[1] == '_')
-	pad = space = ' ', format++;
-      if (format[1] == '-')
-	pad = space = 0, format++;
-      if (format[1] == '0')
-	pad = space = '0', format++;
-      if (format[1] == '^')
-	upcase = 1, format++;
+      int flag_seen, pad = '0', space=' ', swap_case = FALSE;
+
+      /*  Parse flags.  */
+      do {
+        flag_seen = FALSE;
+        if (format[1] == '_')
+          flag_seen = TRUE, pad = space = ' ', format++;
+        if (format[1] == '-')
+          flag_seen = TRUE, pad = space = 0, format++;
+        if (format[1] == '0')
+          flag_seen = TRUE, pad = space = '0', format++;
+        if (format[1] == '^')more 
+          flag_seen = TRUE, upcase = TRUE, format++;
+        if (format[1] == '#')
+          flag_seen = TRUE, swap_case = TRUE, format++;
+      } while (flag_seen);
+
+      /*  Parse modifiers.  */
       if (format[1] == 'E' || format[1] == 'O')
 	format++;  /*  Only C/POSIX locale is supported.  */
 
@@ -88,18 +192,24 @@ _fmt(const char *format, const struct tm
 	--format;
 	break;
       case 'A':
+	if (swap_case)
+	  upcase = TRUE;
 	if (t->tm_wday < 0 || t->tm_wday > 6)
 	  return 0;
 	if (!_add(Afmt[t->tm_wday], upcase))
 	  return 0;
 	continue;
       case 'a':
+	if (swap_case)
+	  upcase = TRUE;
 	if (t->tm_wday < 0 || t->tm_wday > 6)
 	  return 0;
 	if (!_add(afmt[t->tm_wday], upcase))
 	  return 0;
 	continue;
       case 'B':
+	if (swap_case)
+	  upcase = TRUE;
 	if (t->tm_mon < 0 || t->tm_mon > 11)
 	  return 0;
 	if (!_add(Bfmt[t->tm_mon], upcase))
@@ -107,6 +217,8 @@ _fmt(const char *format, const struct tm
 	continue;
       case 'b':
       case 'h':
+	if (swap_case)
+	  upcase = TRUE;
 	if (t->tm_mon < 0 || t->tm_mon > 11)
 	  return 0;
 	if (!_add(bfmt[t->tm_mon], upcase))
@@ -132,6 +244,18 @@ _fmt(const char *format, const struct tm
 	if (!_conv(t->tm_mday, 2, pad))
 	  return 0;
 	continue;
+      case 'F':
+	if (!_fmt(__ISO8601_date_format, t, upcase))
+	  return 0;
+	continue;
+      case 'G':
+	if (!_conv(_compute_iso_week_based_year(t), 4, pad))
+	  return 0;
+	continue;
+      case 'g':
+	if (!_conv(_compute_iso_week_based_year(t) % 100, 2, pad))
+	  return 0;
+	continue;
       case 'H':
 	if (!_conv(t->tm_hour, 2, pad))
 	  return 0;
@@ -166,8 +290,13 @@ _fmt(const char *format, const struct tm
 	if (!_add("\n", upcase))
 	  return 0;
 	continue;
+      case 'P':
+	if (!_add(t->tm_hour >= 12 ? "pm" : "am", upcase))
+	  return 0;
+	continue;
       case 'p':
-	if (!_add(t->tm_hour >= 12 ? "PM" : "AM", upcase))
+	upcase = swap_case ? FALSE : TRUE;
+	if (!_add(t->tm_hour >= 12 ? "pm" : "am", upcase))
 	  return 0;
 	continue;
       case 'R':
@@ -182,6 +311,17 @@ _fmt(const char *format, const struct tm
 	if (!_conv(t->tm_sec, 2, pad))
 	  return 0;
 	continue;
+      case 's':
+	{
+	  struct tm _t;
+	  time_t _time;
+
+	  _t = *t;
+	  _time = mktime(&_t);
+          if (_time == (time_t)-1 || !_conv(_time, -1, pad))
+	    return 0;
+	}
+	continue;
       case 'T':
 	if (!_fmt("%H:%M:%S", t, upcase))
 	  return 0;
@@ -199,6 +339,10 @@ _fmt(const char *format, const struct tm
 		   2, pad))
 	  return 0;
 	continue;
+      case 'V':
+	if (!_conv(_compute_iso_standard_week(t), 2, pad))
+	  return 0;
+	continue;
       case 'W':
 	if (!_conv((t->tm_yday + 7 -
 		    (t->tm_wday ? (t->tm_wday - 1) : 6))
@@ -218,12 +362,10 @@ _fmt(const char *format, const struct tm
 	  return 0;
 	continue;
       case 'y':
-      case 'g':
 	if (!_conv((t->tm_year + TM_YEAR_BASE) % 100, 2, pad))
 	  return 0;
 	continue;
       case 'Y':
-      case 'G':
 	if (!_conv(t->tm_year + TM_YEAR_BASE, 4, pad))
 	  return 0;
 	continue;
@@ -234,9 +376,27 @@ _fmt(const char *format, const struct tm
 	  return 0;
 	continue;
       case 'Z':
-	if (!t->tm_zone || !_add(t->tm_zone, upcase))
+	if (t->tm_zone)
+	{
+	  char tm_zone[32];
+
+	  strcpy(tm_zone, t->tm_zone);
+	  if (swap_case)
+	  {
+	    upcase = FALSE;
+	    strlwr(tm_zone);
+	  }
+	  if (!_add(tm_zone, upcase))
+	    return 0;
+	}
+	else
 	  return 0;
 	continue;
+      case '+':
+	/*
+	 *  The date and time in date(1) format.  An extension introduced
+	 *  with Olson's timezone package and still not supported.
+	 */
       case '%':
 	/*
 	 * X311J/88-090 (4.12.3.5): if conversion char is
@@ -260,7 +420,7 @@ strftime(char *s, size_t maxsize, const 
   pt = s;
   if ((gsize = maxsize) < 1)
     return 0;
-  if (_fmt(format, t, 0))
+  if (_fmt(format, t, FALSE))
   {
     *pt = '\0';
     return maxsize - gsize;
Index: src/libc/ansi/time/strftime.txh
===================================================================
RCS file: /cvs/djgpp/djgpp/src/libc/ansi/time/strftime.txh,v
retrieving revision 1.7
diff -p -U3 -r1.7 strftime.txh
--- src/libc/ansi/time/strftime.txh	30 May 2008 20:57:23 -0000	1.7
+++ src/libc/ansi/time/strftime.txh	7 Jun 2008 12:43:28 -0000
@@ -41,12 +41,20 @@ The abbreviated month name (@code{Oct})
 
 @item %C
 
-Short for @code{%a %b %e %H:%M:%S %Y} (@code{Fri Oct  1 15:30:34 1993})
+The century number (year/100) as a 2-digit integer (@code{19})
 
 @item %c
 
 Short for @code{%m/%d/%y %H:%M:%S} (@code{10/01/93 15:30:34})
 
+@item %D
+
+Short for @code{%m/%d/%y} (@code{10/01/93})
+
+@item %d
+
+The day of the month, zero padded to two characters (@code{02})
+
 @item %Ex
 In some locales, the @code{E} modifier selects alternative representations
 of certain conversion specifiers @code{x}.  But in the "C" locale supported
@@ -57,13 +65,20 @@ mapped to @code{%C}.
 
 The day of the month, blank padded to two characters (@code{ 2})
 
-@item %D
+@item %F
 
-Short for @code{%m/%d/%y} (@code{10/01/93})
+The ISO 8601:2000 date format, in the form @code{%Y-%m-%d} (@code{1993-10-01})
 
-@item %d
+@item %G
 
-The day of the month, zero padded to two characters (@code{02})
+The ISO 8601:2000 standard week-based year with century as a decimal number.
+The 4-digit year corresponding to the ISO week number (see @code{%V}).  This has the
+same format and value as @code{%Y}, except that if the ISO week number belongs to the
+previous or next year, that year is used instead (@code{1993})
+
+@item %g
+
+Like @code{%G}, but without century, i.e., with a 2-digit year (@code{93})
 
 @item %H
 
@@ -75,7 +90,7 @@ The hour (1-12), zero padded to two char
 
 @item %j
 
-The Julian day, zero padded to three characters (@code{275})
+The Julian day (1-366), zero padded to three characters (@code{275})
 
 @item %k
 
@@ -87,7 +102,7 @@ The hour (1-12), space padded to two cha
 
 @item %M
 
-The minutes, zero padded to two characters (@code{30})
+The minutes (0-59), zero padded to two characters (@code{30})
 
 @item %m
 
@@ -107,6 +122,10 @@ mapped to @code{%H}.
 
 AM or PM (@code{PM})
 
+@item %P
+
+Like @code{%p} but in lowercase: am or pm (@code{pm})
+
 @item %R
 
 Short for @code{%H:%M} (@code{15:30})
@@ -119,6 +138,10 @@ Short for @code{%I:%M:%S %p} (@code{03:3
 
 The seconds, zero padded to two characters (@code{35})
 
+@item %s
+
+The seconds since the Epoch, i.e., since 1970-01-01 00:00:00 UTC (@code{})
+
 @item %T
 
 Short for @code{%H:%M:%S} (@code{15:30:35})
@@ -134,7 +157,12 @@ the year, zero padded to two characters 
 
 @item %u
 
-The day of the week (1-7) (@code{6})
+The day of the week (1-7), Monday being 1 (@code{6})
+
+@item %V
+
+The ISO week of the year (01-53), where weeks start on Monday, with the first
+week defined by the first Thursday of the year, zero padded to two characters (@code{39})
 
 @item %W
 
@@ -143,7 +171,7 @@ the year, zero padded to two characters 
 
 @item %w
 
-The day of the week (0-6) (@code{5})
+The day of the week (0-6), Sunday being 0 (@code{5})
 
 @item %x
 
@@ -165,12 +193,53 @@ The year, zero padded to four digits (@c
 
 The timezone abbreviation (@code{EDT})
 
+@item %z
+
+The time-zone as hour offset from GMT in the ISO 8601:2000 standard format (@code{+hhmm} or @code{-hhmm}),
+or by no characters if no timezone is determinable.  Required to emit RFC 822-conformant
+dates using @code{%a, %d %b %Y  %H:%M:%S  %z} (@code{Fri, 01 Oct 1993  03:30:34  +})
+
+@item %+
+
+The date and time in date(1) format.  Not supported in djgpp
+
 @item %%
 
 A percent symbol (@code{%})
 
 @end table
 
+
+The following flag characters, preceding the conversion specifier characters described
+above, eare permitted:
+
+@table @code
+
+@item _
+
+(underscore) Pad a numeric result string with spaces
+
+
+@item -
+
+(dash) Do not pad a numeric result string
+
+@item 0
+
+Pad a numeric result string with zeros even if the conversion specifier character
+uses space-padding by default
+
+@item ^
+
+Convert alphabetic characters in result string to upper case
+
+@item #
+
+Swap the case of the result string.  (This flag only works with certain conversion specifier characters,
+and of these, it is only really useful with %Z).
+
+@end table
+
 @subheading Return Value
 
 The number of characters stored.
Index: tests/libc/ansi/time/strftime.c
===================================================================
RCS file: /cvs/djgpp/djgpp/tests/libc/ansi/time/strftime.c,v
retrieving revision 1.3
diff -p -U3 -r1.3 strftime.c
--- tests/libc/ansi/time/strftime.c	26 May 2002 16:12:46 -0000	1.3
+++ tests/libc/ansi/time/strftime.c	7 Jun 2008 12:43:34 -0000
@@ -1,7 +1,6 @@
 /* this is the Autoconf test program that GNU programs use
    to detect if strftime is working.
-   Apart from missing formats (which probably can be ignored)
-   it seems that the results from "%c" and "%C" are wrong.
+   The results are checked against strftime() from GNU libc.
 */
 
 #include <stdlib.h>
@@ -9,7 +8,10 @@
 #include <string.h>
 #include <sys/time.h>
 #include <time.h>
+#ifdef DJGPP
+/*  Only to be able to compile the code with linux too.  */
 #include <libc/unconst.h>
+#endif
 
 static int
 compare (const char *fmt, const struct tm *tm, const char *expected)
@@ -37,7 +39,9 @@ main (void)
 
   /* This is necessary to make strftime give consistent zone strings and
      e.g., seconds since the epoch (%s).  */
+#ifdef DJGPP
   putenv (unconst("TZ=GMT0", char *));
+#endif
 
 #undef CMP
 #define CMP(Fmt, Expected) n_fail += compare ((Fmt), tm, (Expected))
@@ -53,7 +57,7 @@ main (void)
   CMP ("%H", "13");
   CMP ("%I", "01");
   CMP ("%M", "06");
-  CMP ("%M", "06");
+  CMP ("%P", "pm");
   CMP ("%R", "13:06");		/* POSIX.2 */
   CMP ("%S", "07");
   CMP ("%T", "13:06:07");	/* POSIX.2 */
@@ -90,7 +94,74 @@ main (void)
   CMP ("%y", "70");
   CMP ("%z", "+0000");		/* GNU */
 
-  exit (n_fail ? 1 : 0);
-}
+  /*  Check GNU flag #. Inverts the case.  */
+  CMP ("%#A", "FRIDAY");
+  CMP ("%#^A", "FRIDAY");
+  CMP ("%^#A", "FRIDAY");
+  CMP ("%#a", "FRI");
+  CMP ("%#^a", "FRI");
+  CMP ("%^#a", "FRI");
+  CMP ("%#B", "JANUARY");
+  CMP ("%#^B", "JANUARY");
+  CMP ("%^#B", "JANUARY");
+  CMP ("%#b", "JAN");
+  CMP ("%#^b", "JAN");
+  CMP ("%^#b", "JAN");
+  CMP ("%p", "PM");
+  CMP ("%#p", "pm");
+  CMP ("%#Z", "gmt");
 
+  /*  Check E and O mofifier. Ignore it.  */
+  CMP ("%EC", "19");
+  CMP ("%Ec", "Fri Jan  9 13:06:07 1970");
+  CMP ("%EX", "13:06:07");
+  CMP ("%Ex", "01/09/70");
+  CMP ("%EY", "1970");
+  CMP ("%Ey", "70");
+  CMP ("%Od", "09");
+  CMP ("%Oe", " 9");
+  CMP ("%OH", "13");
+  CMP ("%OI", "01");
+  CMP ("%OM", "06");
+  CMP ("%Om", "01");
+  CMP ("%OS", "07");
+  CMP ("%Ou", "5");
+  CMP ("%OU", "01");
+  CMP ("%OV", "02");
+  CMP ("%OW", "01");
+  CMP ("%Ow", "5");
+  CMP ("%Oy", "70");
+
+  /*  Check G, g and V specifiers.
+      Examples from:
+        <http://www.opengroup.org/onlinepubs/000095399/functions/strftime.html>
+  */
+  t = 883441421;  /*  Tue Dec 30 0:23:41 1997.  */
+  tm = gmtime (&t);
+  CMP ("%V", "01");
+  CMP ("%W", "52");
+  CMP ("%G", "1998");
+  CMP ("%Y", "1997");
+  CMP ("%g", "98");
+  CMP ("%y", "97");
 
+  t = 915245172;  /*  Sat Jan  2 02:46:12 1999.  */
+  tm = gmtime (&t);
+  CMP ("%V", "53");
+  CMP ("%W", "00");
+  CMP ("%G", "1998");
+  CMP ("%Y", "1999");
+  CMP ("%g", "98");
+  CMP ("%y", "99");
+
+  t = 1212808584;  /*  Sat Jun  7 03:16:24 2008.  */
+  tm = gmtime (&t);
+  CMP ("%V", "23");
+  CMP ("%W", "22");
+  CMP ("%G", "2008");
+  CMP ("%Y", "2008");
+  CMP ("%g", "08");
+  CMP ("%y", "08");
+
+  exit (n_fail ? 1 : 0);
+}

- Raw text -


  webmaster     delorie software   privacy  
  Copyright © 2019   by DJ Delorie     Updated Jul 2019