date.java

/*
** Module   : date.java
** Abstract : Progress 4GL compatible date object
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 GES 20050413   @20788 Created initial version with full support for all Progress date
**                           processing including all functions and operators. Also includes
**                           string conversion (to/from) using Progress style format strings. Date
**                           processing has been made compatible including the implementation of
**                           controllable Y2K windowing (start year defaults to 1950 just like in
**                           Progress using the -yy startup parm or the session:year-offset 
**                           attribute). At this time, the order of the date fields in a format
**                           string is hard coded to 'mdy' format, which is something that can be
**                           changed per-process in Progress via the -d startup option or the 
**                           session:date-format attribute.
** 002 GES 20050421   @20814 Major updates to fix processing based on the default constructor and
**                           the constructor that accepts Date instances. In these cases the
**                           timezone processing was wrong (on input and when the resulting
**                           instance was used for accessing the day, month, weekday and year).
**                           Also added strict checks on (yy, mm, dd) and string constructors so
**                           that months and days are checked for validity.
** 003 GES 20050506   @21118 Implemented the comparable interface and also overrode the equals()
**                           method of Object to make this object easier to use.
** 004 GES 20050517   @21220 Rewrite the class to be mutable and to support an internal
**                           representation of the unknown value (as opposed to using null
**                           references). Awareness and integration with the Progress compatible
**                           primative wrappers was added.  Comparison operations were
**                           refactored into a parent class.
** 005 GES 20050606   @21429 Added static versions of the getters to make conversion easier.
** 007 GES 20050607   @21439 Final versions of static functions and some cleanup.
** 008 GES 20050621   @21531 Added an assign() method to modify this instance's state based on
**                           the result of an expression of compatible type.
** 009 SVG 20050720   @21763 Cloning support for BaseDataType added.
** 010 ECF 20050729   @21969 Added neutralMillis convenience method.
**                           Delegates work to dateValue().
** 011 GES 20050815   @22119 Added Undoable interface support.
** 012 GES 20050829   @22285 Split assignment of configuration and state from normal assign().
**                           deepAssign() is now  provided to set both configuration and state
**                           at the same time.
** 013 GES 20050829   @22365 Added processing to replace all '9' chars with '?' when there is any
**                           portion of the date which cannot be displayed using the
**                           given format.
** 014 GES 20050829   @22382 Added error condition processing.
** 015 ECF 20051004   @22967 Implement hashCode() method. Required to be consistent with
**                           BaseDataType's implementation of equals() method.
** 016 GES 20051024   @23123 Added toStringSQL() method.
** 017 GES 20060113   @23898 Added new constructor signatures.
** 018 GES 20060123   @24022 Move to directory for default windowingYear and context-local for
**                           storage of both windowingYear and baseMillis.
** 019 GES 20060206   @24298 Made compareTo() safe to process any object
**                           type (avoids ClassCastException).
** 020 GES 20060208   @24388 Added a default format string getter.
** 021 GES 20060405   @25371 Added increment/decrement helpers to simplify usage of dates
**                           as a loop control var.
** 022 GES 20060417   @25553 Added default value generation.
** 023 ECF 20060428   @25870 Fixed NPE in hashCode(). Timezone may be
**                           null and must be conditionally included in hashcode calculation.
** 024 GES 20060508   @26016 Minor documentation fix.
** 025 GES 20060531   @26760 Fix for plus and minus operators to properly honor unknown value
**                           (they now return unknown when any input is unknown).
** 026 GES 20060724   @28159 Added a forced assign() abstract method.
** 027 GES 20060730   @28226 Cache and reuse calendar instances since the cost of construction
**                           is high. This cache is timezone specific and kept in thread
**                           local vars.
** 028 GES 20060807   @28472 Moved to externalizable interface to optimize
**                           network performance.
** 029 ECF 20060908   @29385 Made constructor which accepts java.util.Date null-safe. A null date
**                           argument results in the instance being set to unknown value.
** 030 ECF 20060908   @29388 Fixed regression of default constructor.
** 031 GES 20070219   @32173 Added support for "999999" and "99999999" format strings
**                           (normally separators are required).
** 032 SVL 20080226   @37158 Additional constructors and yymmddWorker have been added to support
**                           years that have 2 or 1 digit(s).
** 033 GES 20080403   @37913 Added support for arbitrary ordering of the 3 date components,
**                           when reading from or writing to a string.
** 034 ECF 20090130   @41251 Minor performance improvement. Removed CachedCalendar and replaced it
**                           with ThreadLocal<GregorianCalendar>. Only set
**                           embedded calendar's time to current time when necessary.
** 035 ECF 20090212   @41297 Implemented a faster equals(). Handles most common cases without
**                           having to invoke the superclass' implementation.
** 036 SVL 20090213   @41271 Return "?" for the unknown value into toStringExport().
** 037 ECF 20090330   @41713 Fixed neutralMillisToCalendar(). In case of a non-null TimeZone,
**                           Daylight Savings offset must be queried after the Calendar has been
**                           updated with the base millisecond value. This allows the calendar
**                           to compute an appropriate DST offset for the target date. Removed
**                           unused method neutralMillis(). Reimplemented dateValue() to call
**                           dateValue(TimeZone) with internal zone value. Removed static modifier
**                           from WorkArea.dateFormat variable.
** 038 GES 20090422   @41907 Converted to standard string formatting.
** 039 GES 20090424   @41969 Import change.
** 040 GES 20090427   @41991 Rolled back the addition of some error message display code that
**                           was thought to be needed based on a TODO comment that was previously
**                           left behind. At a minimum, unknown value processing should not
**                           go through that path, so that addition was removed
**                           to eliminate regressions.
** 041 SIY 20090701   @43011 Added date components validation method.
** 042 GES 20090814   @43620 MONTH, DAY, YEAR and WEEKDAY must return unknown when the instance
**                           is unknown.
** 043 ECF 20090816   @43662 Removed final modifier from class.
** 044 SIY 20091105   @44317 Fixed handling of long (> 2 digits) years in parseWorker().
** 045 SIY 20091106   @44318 Return INVALID_DATE from parseWorker() if date 
**                           can't be handled and silent mode is active.
** 046 GES 20100312   @44707 secondsSinceMidnight() had a flaw on the day of a DST change, it
**                           always was comparing the current millis (which was already DST aware)
**                           with the "neutral" number of millis at midnight. On that day (twice
**                           a year), the neutral value will always be either DST adjusted
**                           (in fall) or DST unadjusted (in spring). This means that after 2am
**                           on that day, the seconds since midnight will always be calculated
**                           wrong (1 hour later in fall and 1 hour earlier in spring). Changed 
**                           the algorithm to directly use the fields of the Gregorian calendar
**                           and calculate the seconds off that. This has the advantage of being
**                           much simpler and is probably much faster too.
** 047 GES 20100617   @44909 Added a version of secondsSinceMidnight() which takes a calendar as
**                           a parameter. This allows the method to be used to calculate for any
**                           arbitrary time value instead of just for the current time
**                           since midnight.
** 048 GES 20110303          Moved maximum/minimum to varargs.
** 049 GES 20111013          Resolve deadlock that occurs on the client when context-local
**                           initialization calls to the server to read the directory. The
**                           solution is to separate the server communication from the
**                           context-local get() method. Once the get() is complete, it is
**                           safe to call the server.
** 050 CA  20130123          Added c'tor which builds a new instance from a BaseDataType value,
**                           if possible. This should be used only when constructing an instance
**                           using the value returned by a DYNAMIC-FUNCTION call; 4GL
**                           does some special casting in this case, which is not implemented yet.
** 051 GES 20130115          Added support for datetime(tz) data types.
** 052 GES 20130307          Added leading zero removal from date strings.
** 053 GES 20130322          Fixed the BDT c'tor and modified the assign(BDT,boolean) to be the
**                           common location for the _POLY morphing. This is safe because all
**                           current usage would have generated a ClassCastException if that
**                           method was ever used with an invalid type. Now, instead of the CCE,
**                           the code will morph the value as is possible. The morphing code
**                           still needs to be checked for validity.
** 054 OM  20130329          Added int64 support. Cosmetic changes.
** 055 OM  20130513          Added static function for parsing date literals.
** 056 OM  20130604          Massive update to support datetime/-tz types: added 
**                           SESSION:DISPLAY-TIMEZONE attribute, static from/to SQL/literal
**                           conversion functions and static today function. Default c'tor now
**                           creates unknown values. Removed timezone private field (only used by
**                           TODAY function)
** 057 CA  20130822          set/getYearOffset and set/getDateOrder are made package private, so
**                           that the static proxy for the SESSION resource can be created (the
**                           names collide with their SessionUtils counterpart).
** 058 EVL 20131014          Change the methods access type to package private for the following
**                           methods: getSessionTimeZone(), getTimeSource(), getDisplayTimeZone()
**                           to avoid runtime ambiguous exception when creating proxy for session
**                           resource.
** 059 MAG 20140602          Fixed the BDT c'tor and modified the assign(BDT,boolean) on _POLY
**                           morphing.
** 060 VIG 20140914          Changed ErrorManager.displayError calls to equivalent method, that
**                           also adds error numbers to _MSG stack.
** 061 OM  20141016          Fixed equality of unknown date to typeless unknown literal.
**                           Made setDateOrder() public (accessible to TRPL).
** 062 OM  20141105          Rolled back setDateOrder() to package level because of proxy creation
**                           conflict. Functionality accessed indirectly via SessionUtils.
** 063 OM  20141111          Rolled back H062. Proxy conflict eliminated using refactoring the
**                           name of the method in the SessionUtils class.
** 064 GES 20150115          Refactored to create a separate class ParsedDateFormat, where shared
**                           format string parsing resides.
** 065 OM  20150129          Fixed getDefaultOffset() to add DST only when needed.
**                           Fixed millisToJulianDay to return correct day when the date is before
**                           java epoch.
** 066 OM  20150312          Fixed special conversions to/from empty string.
** 067 GES 20160617          Added unknown literal processing for the polymorphic assign case.
** 068 IAS 20160628          Set to default value of the SESSION:TIME-SOURCE attribute to
**                           'LOCAL' (uppercase).
** 069 GES 20160707          Type conversion from a '?' string should result in unknown value.
** 070 CA  20160627          Reworked undoable support - the undoables register themselves with
**                           all blocks up the stack, until either 1. the tx block which created
**                           it is reached or 2. the last tx block where it was saved is reached.
** 071 CA  20160812          getZoneFromOffset will cache TimeZone instances, keyed by offset.
** 072 OM  20160831          Optimized context-local accesses.
** 073 EVL 20161003          Adding more case to handle unknown values.  4GL developers can use
**                           "" empty string as another form of unknown value.
** 074 GES 20171206          Removed explicit bypass on pending error (new silent error mode).
** 075 CA  20180503          Renamed/moved some SESSION attribute getters/setters to fix issues
**                           with using the SESSION via a handle.
** 076 ECF 20180918          Performance improvement in fromLiteral.
** 077 CA  20181029          Fixed a bug in assign(BDT) where the unknown flag was not being 
**                           cleared.  Fixed export formatting in case where year is null.
** 078 OM  20190206          Added implementation of field size needed by record-length.
**     HC  20190315          When converting dynamic queries the date format must be restored
**                           after the conversion. The conversion date format(p2j.cfg.xml) may
**                           differ from the runtime format (directory.xml).
** 079 CA  20190615          Added parseIsoDate() and fromIso() to parse a ISO-8601 date.
** 080 CA  20190628          Javadoc fix.
** 081 OM  20190626          Granted public access to getIsoDate().
** 082 EVL 20190729          Extended invalid format criteria to empty cleaned string.
** 083 CA  20190927          Added support for direct Java access from 4GL code.
** 084 CA  20191206          Fixed assign/deepAssign when the value is null or unknown.
** 085 CA  20200519          ISO 8601 format allows single-digit fields in 4GL.
** 086 OM  20200616          ParsedDateFormat API changes.
** 087 HC  20200806          Added OCX_EPOCH_OFFSET.
** 088 IAS 20201007          Added Type enum
** 089 ME  20210114          Throw invalid data type conversion when assigned longchar/clob/blob even if unknown.
**     CA  20210304          Fixed date, datetime and datetimetz literal parsing. 
**     VVT 20210317          Updated for #4880, genBadCharInFormatError() removed.
**     VVT 20210322          Fixed after the review. See #4880-107.
**     CA  20211227          ETIME rerepsents the millis since the FWD session started, and not epoch.
**     AL2 20220319          Added value proxy check in assign.
**     CA  20220329          'fromIso' method was changed to public.
**     OM  20220622          Improved date interval operations.
**     CA  20230116          Added more POLY ctor variants.
**     VVT 20230224          The public windowing(year) method added to use in customer projects. See #6953. 
** 090 CA  20230215          'duplicate()' method returns the real type instead of BDT.
** 091 SBI 20230308          Chained decrement() and increment().
** 092 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 093 VVT 20230710          Wrong error fixed when parsing valid string into date. See 7407.
**                           Also minor non-functional fixes.
** 094 AB  20230906          Convert the variable to the required type in assign().
**     RAA 20230906          Added default case to switch case in assign().
**     AB  20230907          Modified access modifier for createDate from private to protected.
**                           Added getDefaultOffset(Date d).
**     AB  20230907          Added getYearMonthDay() method.
** 095 VVT 20230918          Multiple non-functional minot refactorings/optimizations.
** 096 CA  20240214          Added 'date' constructor overload with POLY year argument.
** 097 AL2 20240222          Make new 'date' POLY constructor convert to the wider int64.
** 098 CA  20240320          Cache the NumberFormat instance.
** 099 CA  20240323          Removed the NumberFormat usage (and the cache) from 'getIsoDate', as this is 
**                           expensive.
** 100 OM  20240508          Runtime parsing of date literal takes into account the current date-format.
**                           Avoid wrapping String literals, when possible.
**         20240510          Added auto detection for date format, depending on the context and a quick
**                           heuristic analysis of the text.
** 101 OM  20240524          Refined algorithm from H100. It also caused regressions.
** 102 VVT 20241202          Session startup parameters now also used when getting SESSION:DATA-FORMAT.
**                           See #9398.
** 103 CA  20250128          DATE(m, d, y) must not use the SESSION:YEAR-OFFSET or the '-yy' offset, the year
**                           is interpreted 'as is'.
**                           Fixed the standalone tests to have the same expected results as in OE - tests
**                           70 and 71 fail at this time.
** 104 ICP 20250123          Used integer, int64 and character constants to leverage caches instances.
** 105 AI  20250220          Change param of withDynamicFormat into Supplier.
** 106 AL2 20250530          Added getIndependentFromContext.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.util;

import java.io.*;
import java.text.*;
import java.time.*;
import java.time.chrono.*;
import java.time.format.*;
import java.time.temporal.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.regex.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.ErrorManager.ErrorEntry;

/**
 * A class that represents a Progress 4GL compatible date object whose
 * data is mutable.  All Progress language features related to date data
 * types are supported including all function that can be accessed via the
 * date aware operators and built-in Progress date-related functions.
 * <p>
 * The Progress date is similar to the &quot;Chronological Julian Day
 * Number&quot; (which has a base date of 01/01/4712 BC or 01/01/-4713 which
 * is the 0 day) however it is 1 day off (its base date is 12/31/-4714 for
 * day 0).
 * <p>
 * The following is the mapping of Progress language features to the
 * corresponding feature in this class:
 * <p>
 * <pre>
 * +               operator (date + integer days)     {@link #plusDays(date,long)} 
 * +               operator (date + decimal days)     {@link #plusDays(date,NumberType)}
 * -               operator (date - integer days)     {@link #minusDays(date,long)}
 * -               operator (date - decimal days)     {@link #minusDays(date,NumberType)}
 * -               operator (date - date)             {@link #differenceNum}
 * =  or EQ        operator                           {@link CompareOps#isEqual}
 * &lt;&gt; or NE  operator                           {@link CompareOps#isNotEqual}
 * &gt;  or GT     operator                           {@link CompareOps#isGreaterThan}
 * &gt;= or GE     operator                           {@link CompareOps#isGreaterThanOrEqual}
 * &lt;  or LT     operator                           {@link CompareOps#isLessThan}
 * &lt;= or LE     operator                           {@link CompareOps#isLessThanOrEqual}
 * day             function                           {@link #day}
 * weekday         function                           {@link #weekday}
 * month           function                           {@link #month}
 * year            function                           {@link #year}
 * maximum         function                           {@link CompareOps#maximum(BaseDataType[])}
 * minimum         function                           {@link CompareOps#minimum(BaseDataType[])}
 * decimal         function (to convert dates)        {@link #decimalValue}
 * integer         function (to convert dates)        {@link #integerValue}
 * string          function (default format)          {@link #toString()}
 *                          (user-defined format)     {@link #toString(String)} 
 *                          (export format)           {@link #toStringExport()}
 * time            function                           {@link #secondsSinceMidnight}
 * timezone        function (no parameter)            {@link #getDefaultTimeZoneOffset}
 * timezone        function (character parameter)     {@link #getOffsetForSpec}
 * etime           function                           {@link #elapsed}
 * today           function                           {@link #today()}
 * date            function (integer parameter)       {@link #date(NumberType)}
 * date            function (integer m, d, y)         {@link #date(NumberType,NumberType,NumberType)}
 * date            function (string parameter)        {@link #date(String)}
 * date            function (date subclass parameter) {@link #date(date)}
 * date-format     SESSION attribute                  {@link #getDateOrder} or {@link #setDateOrder(character)}
 * timezone        SESSION attribute                  {@link #getTimeZone} or {@link #setTimeZone(NumberType)}
 * time-source     SESSION attribute                  {@link #getTimeSource} or {@link #setTimeSource(character)}
 * year-offset     SESSION attribute                  {@link #getYearOffset} or {@link #setYearOffset(NumberType)}
 *                         [literal]                  {@link #fromLiteral(String)}
 * </pre>
 * <p>
 * Date processing has been made compatible with Progress including the 
 * implementation of controllable Y2K windowing (the start year defaults to
 * 1950). The replacement for using the -yy startup parameter or the
 * session:year-offset attribute is to use the {@link #setWindowingYear}.
 * 2-digit to 4-digit year conversion can be accessed manually via the
 * {@link #windowingImpl}.
 * <p>
 * The order of the date fields when reading from a string or when writing to
 * a string defaults to 'mdy' format, but this can be changed using a
 * directory setting.  That is equivalent to the -d startup option or the
 * session:date-format attribute in Progress.  To set this value, one must
 * store the order of the 3 date sub-components as a 3 character string with
 * one character each for "D" (day), "M" (month) and "Y" (year).  This value
 * is context-specific to the current user's session.  The default value is
 * "MDY".
 * <p>
 * The value returned may have been found via a search algorithm that
 * is account (user or process) specific or group specific
 * within the current server or a global default for all servers. 
 * <p>
 * The implementation iteratively looks up the directory node under:
 * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/dateFormat
 * <p>
 * If no user/process or group nodes are present, then this is checked:
 * /server/&lt;serverID&gt;/runtime/default/dateFormat
 * <p>
 * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
 * (it is the global default area for all servers):
 * /server/default/runtime/&lt;account_or_group&gt;/dateFormat
 * <p>
 * Finally, if no user/process or group nodes are present in the global
 * default area, then this is checked:
 * /server/default/runtime/default/dateFormat
 * <p>
 * This class is very dependent upon the <code>GregorianCalendar</code> and
 * Progress itself seems to have no concept of different types of calendars
 * (e.g. lunar...) so this is OK. The two <code>GregorianCalendar</code> 
 * implementations (Progress and J2SE) appear to be quite compatible including
 * how they handle the missing days from 10-05-1582 through 10-14-1582. Both
 * implementations allow these dates to be instantiated and both generate
 * a date that is equivalent to 10-15-1582 through 10-24-1582 respectively. 
 * This leads to apparent logical contradictions such as 10-14-1582 comparing
 * as greater than 10-15-1582.
 * <p>
 * The Progress date has no notion of a time of day and the Java
 * <code>java.util.Date</code> is inherently built with this notion (and
 * the notion of a timezone).  When being created from any J2SE generated 
 * date source (e.g. an instance of the Date class or the default constructor
 * which accesses the current system time), this timezone specific data
 * must be taken into account and neutered in the resulting stored date. 
 * Likewise, whenever using this data, no timezone is used unless one is
 * converting this instance into an instance of <code>java.util.Date</code>, 
 * in which case the timezone can be overridden as in
 * {@link #dateValue(TimeZone)} or in the {@link #date(Date,TimeZone)} constructor.
 * <p>
 * This class also features a built-in regression testing harness that is
 * invoked via {@link #main}.
 *
 * @author    GES
 */
public class date
extends BaseDataType
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(date.class);
   
   /**
    * The number of days between the Progress base date (12/31/-4714) and the
    * Java epoch start (01/01/1970).
    */
   public static final long JULIAN_EPOCH_OFFSET = 2440589L;
   
   /**
    * For unknown reasons, raw transfer uses a different epoch than the standard 
    * {@link #JULIAN_EPOCH_OFFSET} = 2440589 (01/01/1970).
    * In reference manual it is called  'origin of the ABL DATE data type.' and experimentally was
    * detected to have a value of a value of 2433405 which correspond to 05/02/1950. The size
    * of a field holding this value (returned by this method) is hence 0.
    * This value will not improve the bandwidth, instead it will make it worse.
    */
   public static final long RAW_EPOCH = 2433405L;

   /**
    * The number of days between Progress base date and the OCX epoch start as used when interacting with OCX
    * components (01/01/1990). Progress silently offsets date values passed to OCX methods by this number
    * of days.
    */
   public static final long OCX_EPOCH_OFFSET = 2415020L;
   
   /** An invalid date (cannot exist in Progress 4GL). */
   public static final long INVALID_DATE =  -10246723L; 
   
   /** Represents the day component of the string formatted date. */
   public static final byte DAY = 0;
   
   /** Represents the month component of the string formatted date. */
   public static final byte MONTH = 1;
   
   /** Represents the year component of the string formatted date. */
   public static final byte YEAR = 2;
   
   /** MDY order of date components. */
   public static final byte[] ORDER_MDY = new byte[] {MONTH, DAY, YEAR};
   
   /** YMD order of date components. */
   public static final byte[] ORDER_YMD = new byte[] {YEAR, MONTH, DAY};
   
   /** The default data format, also used for parsing {@code date} literals in static context. */
   public static final String DEFAULT_DATE_FORMAT = "MDY";
   
   /** The default data format, also used for parsing {@code datetime} and {@code datetime-tz} literals. */
   public static final String DEFAULT_DATETIME_FORMAT = "YMD";
   
   /**
    *  The default windowing year, also used for parsing {@code date}, {@code datetime} and
    *  {@code datetime-tz} literals in static context.
    */
   public static final int DEFAULT_DATE_WINDOWING_YEAR = 1950;
   
   /** Number of milliseconds per day. */
   public static final long MILLIS_PER_DAY = 1000 * 60 * 60 * 24;

   /** The specific ISO-8601 date format used by 4GL. */
   protected static final DateTimeFormatter ISO_LOCAL_DATE = 
      new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
                                    .appendLiteral('-')
                                    .appendValue(ChronoField.MONTH_OF_YEAR, 1, 2, SignStyle.NEVER)
                                    .appendLiteral('-')
                                    .appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NEVER)
                                    .toFormatter()
                                    .withResolverStyle(ResolverStyle.LENIENT)
                                    .withChronology(IsoChronology.INSTANCE);
   
   /** Minimum valid offset for SESSION:TIMEZONE. */ 
   protected static final int MIN_VALID_TZ = -14 * 60;
   
   /** Maximum valid offset for SESSION:TIMEZONE. */
   protected static final int MAX_VALID_TZ = 14 * 60;
   
   /** An invalid offset for SESSION:TIMEZONE. */
   protected static final int INVALID_TZ = MAX_VALID_TZ + 1;
   
   /** Stores context-local state variables. */
   private static final ContextContainer work = new ContextContainer();
   
   /** The default format string. */
   private static final String DEFAULT_FORMAT = "99/99/99";
   
   /** Format specification for first TIMEZONE attribute error. */ 
   private static final String TZ_ATTR_ERR1 = 
      "Invalid value %d for SESSION:TIMEZONE attribute";
   
   /** Format specification for second TIMEZONE attribute error. */
   private static final String TZ_ATTR_ERR2 = 
      "Attribute TIMEZONE for the PSEUDO-WIDGET has an invalid value of %d";
   
   /**
    * The default calendar cache (thread local) used for validation and instantiation of {@code date} from
    * day/month/year triplets.
    */
   private static final NullCalendar nullGC = new NullCalendar();
   
   /** The GMT calendar cache (thread local). */
   private static GMTCalendar gmtGC = new GMTCalendar();
   
   /** Pattern used for literal matches */
   private static Pattern literalPattern = Pattern.compile("^(\\d+)[/|\\.|-](\\d+)[/|\\\\.|-]((\\-)?\\d+)$");
   
   /** Cache of other timezone-specific calendar thread local caches. */
   private static Map<TimeZone, ZoneCalendar> tzcals = null;   
   
   /** Cached mappings of tz offsets to their {@link TimeZone} instance. */
   private static Map<Integer, TimeZone> offset2tz = new ConcurrentHashMap<>();
   
   /** Pattern matching some date unknown values: string consisting of any (including zero)
    *  number of spaces, dots and slashes */
   private final static Pattern unknownDateSpecRegexp = Pattern.compile("[?]|[- /.]*");

   /** Stores the date for this instance in Progress 4GL format. */
   private long julian = INVALID_DATE;
   
   /** Indicates if this instance is set to the unknown value. */
   private boolean unknown = false;
   
   /** The working area where this instance was created. Caches costly access to context local. */
   private WorkArea workArea = null;
   
   /**
    * Creates an instance that represents an unknown date.
    */
   public date()
   {
      unknown = true;
   }
   
   /**
    * This is a special c'tor which should be used only when converting the
    * value returned by a function or method with polymorphic return type into the
    * expected type (i.e. DYNAMIC-FUNCTION()).  In such cases, the 4GL does some
    * automatic type conversion (see {@link #assign(BaseDataType)}).
    * 
    * @param    value
    *           The value to be used for this instance.
    */
   public date(BaseDataType value)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      value = value == null ? null : value.val();
      if (value == null || value.isUnknown())
      {
         setUnknown();
      }
      else
      {
         assign(value);
      }
   }
   
   /**
    * Creates an instance from a <code>java.util.Date</code> instance.
    * <p>
    * <b>WARNING: this will only work if the calendar in which this date
    * was constructed is the <code>GregorianCalendar</code> AND if the
    * timezone in which this object was created is the same as the current
    * default for this JVM instance.</b>  If this is not correct then you
    * must use the {@link #date(Date,TimeZone)} version.
    *
    * @param   d
    *          The <code>Date</code> instance from which to pattern the instance.
    */
   public date(Date d)
   {
      this(d, TimeZone.getDefault());
   }
   
   /**
    * Creates an instance from a <code>java.util.Date</code> instance.
    * <p>
    * If a timezone is specified, it is stored to be used when instantiating
    * <code>java.util.Date</code> instances using this instance's data.
    *
    * @param   d
    *          The <code>Date</code>instance from which to pattern the
    *          instance or <code>null</code> to obtain the current time.
    * @param   zone
    *          The timezone whose offset is to be used to calculate the 
    *          date or <code>null</code> if the default JVM timezone is to be used.
    */
   public date(Date d, TimeZone zone)
   {
      if (d == null)
      {
         setUnknown();
         return;
      }

      julian = millisToJulianDay(dateToLocalMillis(d, zone));
   }
   
   /**
    * Parses a string with a format based on the Progress 4GL rules for converting a string to a date.
    * <p>
    * The special string "TODAY" (case-insensitive) is treated as the same result as the built-in function
    * {@code TODAY}. This means it generates a date that is equal to the current date based on the local
    * date and time of day.
    * <p>
    * Date literals are generally matched in mm/dd/yyyy or mm-dd-yyyy or mm.dd.yyyy formats, although the 3
    * types of separators ('-', '/' and '.') can even be mixed in the same string (e.g. (mm-dd/yyyy or 
    * mm.dd-yyyy). Both months and days can be specified in any number of digits (there must be at least 1
    * digit, but there can 5 digits as long as the resulting month or day is valid).  The month must be an
    * integer between 1 and 12 and the day must be between 1 and the number of days in the given month.  All
    * separators and the year digits are optional. If separators are missing, the first 2 digits are treated
    * as the month, the next 2 digits as the day and the remaining digits as the year. If the first separator
    * is used and there is a year, then the second separator must also be used.  Likewise, there cannot be a
    * day/year separator without first having a month/day separator.
    * <p>
    * If the year digits are missing, the current year will be used.  There can be an optional negative sign
    * preceding any year digits and one can specify a dd and yyyy separator without specifying any year
    * digits at all.  Any number of year digits are valid, though in Progress the year 0 is treated
    * specially, as follows:
    * <ul>
    *    <li> Valid (all resolve to the year 2000)
    *    <ul>
    *       <li> 0
    *       <li> -0
    *       <li> 00
    *    </ul>
    *    <li> Invalid (all complain about specifying the year 0)
    *    <ul>
    *       <li> 000
    *       <li> 0000
    *       <li> 00000 (and so on...)
    *       <li> -00
    *       <li> -000 (and so on...)
    *    </ul>
    * </ul>
    * <p>
    * So y, yy, yyy, yyyy, yyyyy... and -y, -yy, -yyy, -yyyy... all are valid year specifications, except for
    * the specific year zero issue noted above.  The largest positive year value is 32767 and the largest
    * negative year value is -32768.  Note that leading zeros are ignored. Negative years can only be
    * specified in a string that has separators. 
    * <p>
    * Any positive year of 1 or 2 digits is interpreted based on the current Y2K window which can be obtained
    * using {@link #getWindowingYear}.
    * <p>
    * Leading and trailing spaces are allowed around the mm/dd/yyyy data but no spaces may be inserted inside
    * the date value.  Any additional non-whitespace included in the string after the date AND after
    * intervening whitespace, is ignored.
    *
    * @param    d
    *           The formatted string date to convert.
    *           
    * @throws   ErrorConditionException 
    */
   public date(String d)
   throws ErrorConditionException
   {
      instantiateFromStringWorker(d, null/*auto*/, -1);
   }
   
   /**
    * Converts a string to a date using a specified format and an optional windowing year.
    * The purpose of this constructor is to be used with PL/Java as there is no way to access 
    * P2J SESSION's attributes on SQL server side. 
    *  
    * @see     date#date(String)
    * 
    * @param   d
    *          The formatted string date to convert.
    * @param   format
    *          The format string used for date conversion. May be {@code null}. In this case, the dynamic
    *          format is used if it was activated or {@code DEFAULT_DATE_FORMAT} otherwise.
    * @param   windowingYear
    *          The start of the 100 year window used for conversion when year is in 2-digits.
    *
    * @throws  ErrorConditionException 
    */
   public date(String d, String format, int windowingYear)
   throws ErrorConditionException
   {
      if (format == null)
      {
         format = getDynamicFormat();
         if (format == null)
         {
            format = DEFAULT_DATE_FORMAT;
         }
      }
      instantiateFromStringWorker(d, format, windowingYear);
   }
   
   /**
    * Get the dynamic format.
    *
    * @return   the dynamic format, if one was configured. Otherwise, return {@code null}.
    */
   public static String getDynamicFormat()
   {
      return work.get().dynamicFormat;
   }
   
   /**
    * Constructs an instance with the exact same state as the passed
    * instance.  This can be used both as a copy constructor as well as an
    * implementation of the DATE() built-in function when called with a
    * <code>date</code> subclass.
    *
    * @param    d
    *           The instance from which to copy state.
    */
   public date(date d)
   {
      deepAssign(d);
   }
   
   /**
    * Parses a string with a format based on the Progress 4GL rules for
    * converting a string to a date.
    * <p>
    * Date literals are generally matched in mm/dd/yyyy or mm-dd-yyyy or
    * mm.dd.yyyy formats, although the 3 types of separators ('-', '/' and
    * '.') can even be mixed in the same string (e.g. (mm-dd/yyyy or 
    * mm.dd-yyyy). Both months and days can be specified in any number of 
    * digits (there must be at least 1 digit, but there can 5 digits as long
    * as the resulting month or day is valid).  The month must be an integer
    * between 1 and 12 and the day must be between 1 and the number of days
    * in the given month.  All separators and the year digits are optional.
    * If separators are missing, the first 2 digits are treated as the month,
    * the next 2 digits as the day and the remaining digits as the year. If 
    * the first separator is used and there is a year, then the second
    * separator must also be used.  Likewise, there cannot be a day/year
    * separator without first having a month/day separator.
    * <p>
    * If the year digits are missing, the current year will be used.  There 
    * can be an optional negative sign preceding any year digits and one can
    * specify a dd and yyyy separator without specifying any year digits at
    * all.  Any number of year digits are valid, though in Progress the year
    * 0 is treated specially, as follows:
    * <ul>
    *    <li> Valid (all resolve to the year 2000)
    *    <ul>
    *       <li> 0
    *       <li> -0
    *       <li> 00
    *    </ul>
    *    <li> Invalid (all complain about specifying the year 0)
    *    <ul>
    *       <li> 000
    *       <li> 0000
    *       <li> 00000 (and so on...)
    *       <li> -00
    *       <li> -000 (and so on...)
    *    </ul>
    * </ul>
    * <p>
    * So y, yy, yyy, yyyy, yyyyy... and -y, -yy, -yyy, -yyyy... all are valid
    * year specifications, except for the specific year zero issue noted
    * above.  The largest positive year value is 32767 and the largest
    * negative year value is -32768.  Note that leading zeros are ignored.
    * Negative years can only be specified in a string that has separators. 
    * <p>
    * Any positive year of 1 or 2 digits is interpreted based on the current
    * Y2K window which can be obtained using {@link #getWindowingYear}.
    * <p>
    * Leading and trailing spaces are allowed around the mm/dd/yyyy data but
    * no spaces may be inserted inside the date value.  Any additional
    * non-whitespace included in the string after the date AND after
    * intervening whitespace, is ignored.
    * <p>
    * If the parameter is <code>unknown value</code> the resulting date is 
    * the <code>unknown value</code>.
    *
    * @param    d
    *           The formatted string date to convert.
    *           
    * @throws   ErrorConditionException 
    */
   public date(character d)
   throws ErrorConditionException
   {
      if (BaseDataType.isProxy(d))
      {
         if (d.val() instanceof character) 
         {
            d = (character) d.val();
         }
         else
         {
            initialize(d.val());
            return;
         }
      }
      if (d.isUnknown())
      {
         unknown = true;
      }
      else
      {
         instantiateFromStringWorker(d.toStringMessage(), null/*auto*/, -1);
      }
   }
   
   /**
    * Creates an instance based on specific numeric input.
    *
    * @param    month
    *           The month from 1 to 12.
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less).
    * @param    year
    *           The Progress 4GL year which may be a negative number which represents the BC era or a 
    *           positive AD era year. The value must be between -32768 and 32767 and it cannot be 0 since the 
    *           year 0 is invalid in Progress. The value will be used unchanged, and will not be windowed.
    *           
    * @throws   ErrorConditionException 
    */
   public date(double month, double day, double year)
   throws ErrorConditionException
   {
      yymmddWorker((int) year, (int) month, (int) day);
   }
   
   /**
    * Creates an instance based on specific numeric input.
    *
    * @param    month
    *           The month from 1 to 12.
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less).
    * @param    year
    *           The Progress 4GL year which may be a negative number which represents the BC era or a 
    *           positive AD era year. The value must be between -32768 and 32767 and it cannot be 0 since the 
    *           year 0 is invalid in Progress. The value will be used unchanged, and will not be windowed.
    * @param    forceFourDigits
    *           If true then 2-digit years will be windowed into the right
    *           century.
    *           
    * @throws   ErrorConditionException 
    */
   public date(double month, double day, double year, boolean forceFourDigits)
   throws ErrorConditionException
   {
      yymmddWorker((int) year, (int) month, (int) day, forceFourDigits);
   }
   
   /**
    * Creates an instance based on specific numeric input.
    * <p>
    * If the parameters are <code>unknown value</code> the resulting date is 
    * the <code>unknown value</code>.
    *
    * @param    month
    *           The month from 1 to 12.
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less).
    * @param    year
    *           The Progress 4GL year which may be a negative number which represents the BC era or a 
    *           positive AD era year. The value must be between -32768 and 32767 and it cannot be 0 since the 
    *           year 0 is invalid in Progress. The value will be used unchanged, and will not be windowed.
    *           
    * @throws   ErrorConditionException 
    */
   public date(NumberType month, NumberType day, double year)
   throws ErrorConditionException
   {
      if (month.isUnknown() || day.isUnknown())
      {
         unknown = true;
      }
      else
      {
         yymmddWorker((int) year, month.intValue(), day.intValue());
      }
   }
   
   /**
    * Creates an instance based on specific numeric input.
    * <p>
    * If the parameters are <code>unknown value</code> the resulting date is 
    * the <code>unknown value</code>.
    *
    * @param    month
    *           The month from 1 to 12.
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less).
    * @param    year
    *           The Progress 4GL year which may be a negative number which represents the BC era or a 
    *           positive AD era year. The value must be between -32768 and 32767 and it cannot be 0 since the 
    *           year 0 is invalid in Progress. The value will be used unchanged, and will not be windowed.
    *           
    * @throws   ErrorConditionException 
    */
   public date(BaseDataType month, double day, BaseDataType year)
   throws ErrorConditionException
   {
      if (month.isUnknown() || year.isUnknown())
      {
         unknown = true;
      }
      else
      {
         yymmddWorker(new integer(month).intValue(), (int) day, new integer(year).intValue());
      }
   }
   
   /**
    * Creates an instance based on specific numeric input.
    * <p>
    * If the parameters are <code>unknown value</code> the resulting date is 
    * the <code>unknown value</code>.
    *
    * @param    month
    *           The month from 1 to 12.
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less).
    * @param    year
    *           The Progress 4GL year which may be a negative number which represents the BC era or a 
    *           positive AD era year. The value must be between -32768 and 32767 and it cannot be 0 since the 
    *           year 0 is invalid in Progress. The value will be used unchanged, and will not be windowed.
    *           
    * @throws   ErrorConditionException 
    */
   public date(double month, double day, BaseDataType year)
   throws ErrorConditionException
   {
      if (year.isUnknown())
      {
         unknown = true;
      }
      else
      {
         yymmddWorker((int) month, (int) day, new integer(year).intValue());
      }
   }
   
   /**
    * Creates an instance based on specific numeric input.
    * <p>
    * If the parameters are <code>unknown value</code> the resulting date is 
    * the <code>unknown value</code>.
    *
    * @param    month
    *           The month from 1 to 12.
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less).
    * @param    year
    *           The Progress 4GL year which may be a negative number which represents the BC era or a 
    *           positive AD era year. The value must be between -32768 and 32767 and it cannot be 0 since the 
    *           year 0 is invalid in Progress. The value will be used unchanged, and will not be windowed.
    *           
    * @throws   ErrorConditionException 
    */
   public date(double month, NumberType day, NumberType year)
   throws ErrorConditionException
   {
      if (year.isUnknown()|| day.isUnknown())
      {
         unknown = true;
      }
      else
      {
         yymmddWorker(year.intValue(), (int) month, day.intValue());
      }
   }
   
   /**
    * Creates an instance based on specific numeric input.
    * <p>
    * If the parameters are <code>unknown value</code> the resulting date is 
    * the <code>unknown value</code>.
    *
    * @param    month
    *           The month from 1 to 12.
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less).
    * @param    year
    *           The Progress 4GL year which may be a negative number which represents the BC era or a 
    *           positive AD era year. The value must be between -32768 and 32767 and it cannot be 0 since the 
    *           year 0 is invalid in Progress. The value will be used unchanged, and will not be windowed.
    *           
    * @throws   ErrorConditionException 
    */
   public date(NumberType month, double day, NumberType year)
   throws ErrorConditionException
   {
      if (year.isUnknown() || month.isUnknown())
      {
         unknown = true;
      }
      else
      {
         yymmddWorker(year.intValue(), month.intValue(), (int) day);
      }
   }
   
   /**
    * Creates an instance based on specific numeric input.
    * <p>
    * If the parameters are <code>unknown value</code> the resulting date is 
    * the <code>unknown value</code>.
    *
    * @param    month
    *           The month from 1 to 12.
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less).
    * @param    year
    *           The Progress 4GL year which may be a negative number which represents the BC era or a 
    *           positive AD era year. The value must be between -32768 and 32767 and it cannot be 0 since the 
    *           year 0 is invalid in Progress. The value will be used unchanged, and will not be windowed.
    *           
    * @throws   ErrorConditionException 
    */
   public date(double month, NumberType day, double year)
   throws ErrorConditionException
   {
      if (day.isUnknown())
      {
         unknown = true;
      }
      else
      {
         yymmddWorker((int) year, (int) month, day.intValue());
      }
   }
   
   /**
    * Creates an instance based on specific numeric input.
    * <p>
    * If the parameters are <code>unknown value</code> the resulting date is 
    * the <code>unknown value</code>.
    *
    * @param    month
    *           The month from 1 to 12.
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less).
    * @param    year
    *           The Progress 4GL year which may be a negative number which represents the BC era or a 
    *           positive AD era year. The value must be between -32768 and 32767 and it cannot be 0 since the 
    *           year 0 is invalid in Progress. The value will be used unchanged, and will not be windowed.
    *           
    * @throws   ErrorConditionException 
    */
   public date(NumberType month, double day, double year)
   throws ErrorConditionException
   {
      if (month.isUnknown())
      {
         unknown = true;
      }
      else
      {
         yymmddWorker((int) year, month.intValue(), (int) day);
      }
   }
   
   /**
    * Creates an instance based on specific numeric input.
    * <p>
    * If the parameters are <code>unknown value</code> the resulting date is 
    * the <code>unknown value</code>.
    *
    * @param    month
    *           The month from 1 to 12.
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less).
    * @param    year
    *           The Progress 4GL year which may be a negative number which represents the BC era or a 
    *           positive AD era year. The value must be between -32768 and 32767 and it cannot be 0 since the 
    *           year 0 is invalid in Progress. The value will be used unchanged, and will not be windowed.
    *           
    * @throws   ErrorConditionException 
    */
   public date(double month, double day, NumberType year)
   throws ErrorConditionException
   {
      if (year.isUnknown())
      {
         unknown = true;
      }
      else
      {
         yymmddWorker(year.intValue(), (int) month, (int) day);
      }
   }
   
   /**
    * Creates an instance based on specific numeric input.
    * <p>
    * If the parameters are <code>unknown value</code> the resulting date is 
    * the <code>unknown value</code>.
    *
    * @param    month
    *           The month from 1 to 12.
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less).
    * @param    year
    *           The Progress 4GL year which may be a negative number which represents the BC era or a 
    *           positive AD era year. The value must be between -32768 and 32767 and it cannot be 0 since the 
    *           year 0 is invalid in Progress. The value will be used unchanged, and will not be windowed.
    *           
    * @throws   ErrorConditionException 
    */
   public date(NumberType month, NumberType day, BaseDataType year)
   throws ErrorConditionException
   {
      this(month, day, new int64(year));
   }
   
   /**
    * Creates an instance based on specific numeric input.
    * <p>
    * If the parameters are <code>unknown value</code> the resulting date is 
    * the <code>unknown value</code>.
    *
    * @param    month
    *           The month from 1 to 12.
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less).
    * @param    year
    *           The Progress 4GL year which may be a negative number which represents the BC era or a 
    *           positive AD era year. The value must be between -32768 and 32767 and it cannot be 0 since the 
    *           year 0 is invalid in Progress. The value will be used unchanged, and will not be windowed.
    *           
    * @throws   ErrorConditionException 
    */
   public date(NumberType month, NumberType day, NumberType year)
   throws ErrorConditionException
   {
      if (year.isUnknown() || month.isUnknown() || day.isUnknown())
      {
         unknown = true;
      }
      else
      {
         yymmddWorker(year.intValue(), month.intValue(), day.intValue());         
      }
   }
      
   /**
    * Creates an instance that represents the number of days (positive or
    * negative) from the Progress 4GL base date of 12/31/-4714.  Note the
    * following examples (formatted in 'export' format):
    * <p>
    * <pre>
    * date   -1000 is 04/05/-4716
    * date      -1 is 12/30/-4714
    * date       0 is 12/31/-4714
    * date       1 is 01/01/-4713
    * date 1685264 is 12/31/-100
    * date 1685265 is 01/01/-099
    * date 1721424 is 12/31/-001
    * date 1721425 is 01/01/001
    * date 2299161 is 10/04/1582
    * date 2299162 is 10/15/1582 (note the gap in the Gregorian Calendar)
    * date 2421684 is 03/30/1918
    * date 2421685 is 03/31/1918 (first DST in U.S.A.)
    * date 2421686 is 04/01/1918
    * date 2433283 is 12/31/1949
    * date 2433284 is 01/01/50 (first date in default Y2K window - 1950)
    * date 2440589 is 01/01/70 (beginning of Java epoch)
    * date 2451545 is 12/31/99
    * date 2451546 is 01/01/00
    * date 2453475 is 04/13/05
    * date 2469808 is 12/31/49 (last date in default Y2K window - 2049)
    * date 2469809 is 01/01/2050
    * </pre>
    *
    * @param    dayNumber
    *           The number of days since 12/31/-4714.
    */
   public date(long dayNumber)
   {
      julian = dayNumber;
   }
   
   /**
    * Creates an instance that represents the number of days (positive or
    * negative) from the Progress 4GL base date of 12/31/-4714.  See
    * {@link #date(long)}.
    *
    * @param    dayNumber
    *           The number of days since 12/31/-4714.
    */
   public date(int dayNumber)
   {
      julian = dayNumber;
   }
   
   /**
    * Creates an instance that represents the number of days (positive or
    * negative) from the Progress 4GL base date of 12/31/-4714.  See
    * {@link #date(long)}.
    * <p>
    * If the parameter is <code>unknown value</code> the resulting date is 
    * the <code>unknown value</code>.
    *
    * @param    dayNumber
    *           The number of days since 12/31/-4714.
    */
   public date(NumberType dayNumber)
   {
      if (dayNumber.isUnknown())
      {
         unknown = true;
      }
      else
      {
         julian = dayNumber.intValue();
      }
   }   
   
   /**
    * Creates an instance that represents today's date using the local system clock time.
    * @return today's date
    */
   public static date today()
   {
      date ret = new date();
      ret.setDayNumber(_today());
      return ret;
   }
   
   /**
    * Parse a Progress date literal (format mm/dd/yyyy). The year may be in 2 or 4 digits format.
    * If in 2 digits the current year windowing is used.
    *  
    * @param   str
    *          The string representation of the date literal.
    *          
    * @return  The date represented by the string str.
    */
   public static date fromLiteral(String str)
   {
      Matcher matcher = literalPattern.matcher(str);
      if (!matcher.matches())
      {
         return new date(); // format not recognized
      }
      
      try
      {
         int month = Integer.parseInt(matcher.group(1));
         int day = Integer.parseInt(matcher.group(2));
         String strYear = matcher.group(3);
         int year = Integer.parseInt(strYear);
         
         return new date(month, day, year, strYear.length() < 3);
      }
      catch (NumberFormatException e)
      {
         return new date(); // format not recognized
      }
   }
   
   /**
    * Get the type
    * @return type
    */
   @Override
   public Type getType()
   {
      return Type.DATE;
   }

   /**
    * Convert a UTC time into a timezone specific number of milliseconds
    * since the epoch (January 1, 1970).  If no timezone is passed in,
    * the default JVM timezone will be used.  If no date is passed in, the
    * current time will be used.
    *
    * @param    d
    *           The date object representing the UTC value to convert or
    *           <code>null</code> if the current UTC time should be used.
    * @param    zone
    *           The timezone object whose specific attributes should be used
    *           in the conversion of the UTC value or <code>null</code> if
    *           JVM default timezone should be used.
    *
    * @return   The localized number of milliseconds from the Java epoch.
    */
   public static long dateToLocalMillis(Date d, TimeZone zone)
   {
      if (zone == null)
      {
         zone = TimeZone.getDefault();
      }

      // use the passed/session/local timezone to create the calendar
      GregorianCalendar gc = getZoneCalendar(zone);
         
      if (d != null)
      {
         // take the current time
         gc.setTime(d);
      }
      else
      {
         // reset the state to the current time
         gc.setTimeInMillis(System.currentTimeMillis());
      }
      
      return calendarToLocalMillis(gc);
   }
   
   /**
    * Convert a UTC time into a timezone specific number of milliseconds
    * since the epoch (January 1, 1970).
    *
    * @param    gc
    *           The <code>GregorianCalendar</code> representing the UTC value to convert.
    *
    * @return   The localized number of milliseconds from Java epoch.
    */
   public static long calendarToLocalMillis(GregorianCalendar gc)
   {
      // determine the timezone-specific offset
      long offset = gc.get(Calendar.ZONE_OFFSET) + gc.get(Calendar.DST_OFFSET); 
      
      // convert the UTC to a local time and return it
      return gc.getTimeInMillis() + offset;
   }
   
   /**
    * Convert a time-neutral number of milliseconds since the epoch into a
    * UTC time as represented by the <code>Date</code> class. If no timezone
    * is passed in, the default JVM timezone will be used.
    *
    * @param    millis
    *           The time-neutral number of milliseconds since the epoch.
    * @param    zone
    *           The timezone object whose specific attributes should be used
    *           in the conversion to the UTC value or <code>null</code> if
    *           JVM default timezone should be used.
    *
    * @return   A date object based on the UTC value such that the same
    *           time-neutral result will be obtained when using the timezone
    *           aware resulting class.
    */
   public static Date neutralMillisToDate(long millis, TimeZone zone)
   {
      return neutralMillisToCalendar(millis, zone).getTime();
   }
   
   /**
    * Convert a time-neutral number of milliseconds since the epoch into a
    * UTC time as represented by the <code>GregorianCalendar</code> class.
    * If no timezone is passed in, the GMT timezone will be used which means
    * that all timezone processing is effectively disabled.
    *
    * @param    millis
    *           The time-neutral number of milliseconds since the epoch.
    * @param    zone
    *           The timezone object whose specific attributes should be used
    *           in the conversion to the UTC value or <code>null</code> if
    *           timezone processing should be disabled.
    *
    * @return   A calendar object based on the UTC value such that the same
    *           time-neutral result will be obtained when using the timezone
    *           aware resulting class.
    */
   public static GregorianCalendar neutralMillisToCalendar(long millis, TimeZone zone)
   {
      GregorianCalendar gc = null;
      
      if (zone == null)
      {
         // no timezone correction is needed
         gc = gmtGC.get();
         
         // set the time into the calendar
         gc.setTimeInMillis(millis);
      }
      else
      {
         // use the passed timezone
         gc = getZoneCalendar(zone);
         
         // determine the timezone-specific offset (raw offset from GMT in milliseconds)
         int tzOffset = gc.get(Calendar.ZONE_OFFSET);
         
         // convert the neutral time to a UTC
         millis -= tzOffset;
         
         // set the time into the calendar
         gc.setTimeInMillis(millis);
         
         // now that the date is set, the calendar can accurately compute an
         // offset for Daylight Savings;  if positive, adjust for it
         int dstOffset = gc.get(Calendar.DST_OFFSET);
         if (dstOffset > 0L)
         {
            gc.add(Calendar.MILLISECOND, -dstOffset);
         }
      }
      
      // return
      return gc;
   }
   
   /**
    * Converts a Progress 4GL style Julian day into the number of
    * milliseconds since the Java epoch. Note that the resulting milliseconds
    * value is to be considered as specific to whatever timezone is in
    * use as the time index itself is invalid and only the date is valid.
    *
    * @param    jd
    *           The Julian day (days since 12/31/-4714).
    *
    * @return   The number of milliseconds since the Java epoch.
    */
   public static long julianDayToMillis(long jd)
   {
      return (jd - JULIAN_EPOCH_OFFSET) * MILLIS_PER_DAY;
   }
   
   /**
    * Converts a number of milliseconds from the Java epoch into a Progress 4GL style Julian day.
    * The milliseconds value is used 'raw' so any local timezone offset processing that is needed
    * must have already been applied.
    *
    * @param    millis
    *           The number of milliseconds from the Java epoch which specifies a specific point 
    *           in time.
    *
    * @return   The Julian day (days since 12/31/-4714). If <code>millis</code> does not 
    *           represent a full day, the result is adjusted to correct result using Euclidian
    *           remainder arithmetic.
    */
   public static long millisToJulianDay(long millis)
   {
      long julianDay = (millis / MILLIS_PER_DAY) + JULIAN_EPOCH_OFFSET;
      if (millis % MILLIS_PER_DAY < 0)
      {
         // count full days, otherwise we will get negative time
         // in the case millis < 0 (ie. before java epoch):
         //    - if the remainder is 0 then a full day is counted
         //    - otherwise the negative remainder represents the time from previous day, not yet
         //          counted, so we decrement the result
         -- julianDay;
      }
      
      return julianDay;
   }
   
   /**
    * Returns the instance from the list which has the latest date.  This
    * list may not include <code>null</code> values.
    *
    * @param    list
    *           The list of dates to check.
    *
    * @return   The latest date or <code>unknown value</code> if any
    *           entries in the list are unknown or <code>null</code> if
    *           an empty list is passed.
    */
   public static date maximum(date ... list)
   {
      return (date) CompareOps.maximum(list);
   }
   
   /**
    * Returns the instance from the list which has the earliest date.  This
    * list may not include <code>null</code> values.
    *
    * @param    list
    *           The list of dates to check.
    *
    * @return   The earliest date or <code>unknown value</code> if any
    *           entries in the list are unknown or <code>null</code> if
    *           an empty list is passed.
    */
   public static date minimum(date ... list)
   {
      return (date) CompareOps.minimum(list);
   }
   
   /**
    * Executes a piece of code, making sure the value of {@code dynamicFormat} reflects
    * the current dynamic date-format. When the method ends, the flag is restored.
    *
    * @param   r
    *          The code to be executed.
    */
   public static <T> T withDynamicFormat(Supplier<T> r)
   {
      WorkArea wa = work.get();
      if (wa.dynamicFormat != null)
      {
         // Already in a bracket.
         // This can happen when the before table is constructed during the bracket of its after table.
         return r.get();
      }
      
      try
      {
         wa.dynamicFormat = date.getDateOrderNative();
         return r.get();
      }
      finally
      {
         wa.dynamicFormat = null;
      }
   }
   
   /** 
    * Accesses the beginning year of the 100 year period where 2 digit dates
    * implicitly correspond to 4 year dates.  This value is context-specific
    * to the current user's session.
    * <p>
    * The value returned may have been found via a search algorithm that
    * is account (user or process) specific or group specific
    * within the current server or a global default for all servers. 
    * <p>
    * The implementation iteratively looks up the directory node under:
    * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/windowingYear
    * <p>
    * If no user/process or group nodes are present, then this is checked:
    * /server/&lt;serverID&gt;/runtime/default/windowingYear
    * <p>
    * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
    * (it is the global default area for all servers):
    * /server/default/runtime/&lt;account_or_group&gt;/windowingYear
    * <p>
    * Finally, if no user/process or group nodes are present in the global
    * default area, then this is checked:
    * /server/default/runtime/default/windowingYear
    * <p>
    * If no value is found via this lookup, then the Progress 4GL default of  
    * 1950 will be returned.
    *
    * @return   The first year of the 2-digit to 4-digit mapping window for
    *           Y2K purposes.
    */
   public static int getWindowingYear()
   {
      return work.obtain().windowingYear;
   }
   
   /** 
    * Sets the beginning year of the 100 year period where 2 digit dates
    * implicitly correspond to 4 year dates.  The Progress 4GL default is 1950.
    * <p>
    * The value set is specific to the current user's context.
    *
    * @param    year
    *           The first year of the 2-digit to 4-digit mapping window for Y2K purposes.
    */
   public static void setWindowingYear(long year)
   {
      // P4GL silently truncates any large value to fit the 32 bit representation
      work.obtain().windowingYear = (int)year;
   }
   
   /**
    * Implementation of SESSION:YEAR-OFFSET attribute setter. Sets the 
    * configured start year for the two-digit Y2K windowing range. This is
    * the equivalent of the Progress -yy command line parameter. The 
    * default value is 1950 unless otherwise specified in the directory (see
    * {@link #getWindowingYear}).
    * 
    * @param   year
    *          The new start year for the two-digit windowing range.
    */
   static void setYearOffset(NumberType year)
   {
      if (year.isUnknown())
      {
         // P4GL prints no error, silently ignoring this.
         return;
      }
      
      date.setWindowingYear(year.longValue());
   }
   
   /**
    * Implementation of SESSION:YEAR-OFFSET attribute getter. Returns the 
    * currently configured start year for the two-digit Y2K windowing range.
    * This is the equivalent to the Progress -yy command line parameter. The 
    * default value is 1950 unless otherwise specified in the directory (see
    * {@link #getWindowingYear}).
    * 
    * @return  The start year for the two-digit windowing range.
    */
   static integer getYearOffset()
   {
      return new integer(date.getWindowingYear());
   }
   
   /** 
    * Gets the order of date components for conversion to/from strings.
    * <p>
    * The order of the 3 date sub-components as a 3 byte array using the
    * constants <code>DAY</code>, <code>MONTH</code> and <code>YEAR</code>
    * once each.  The byte order from index 0 to index 2 represents the
    * left to right ordering of the components. So the leftmost date component
    * will be defined by the byte at index position 0.
    * <p>
    * The value accessed is specific to the current user's context.
    *
    * @return   Defines the order of date components for conversion to/from
    *           strings.
    */
   public static byte[] getDateComponentOrder()
   {
      return work.obtain().dateFormat;
   }
   
   /** 
    * Sets the order of date components for conversion to/from strings.
    * <p>
    * The order of the 3 date sub-components as a 3 byte array using the
    * constants <code>DAY</code>, <code>MONTH</code> and <code>YEAR</code>
    * once each.  The byte order from index 0 to index 2 represents the
    * left to right ordering of the components. So the leftmost date component
    * will be defined by the byte at index position 0.
    * <p>
    * The value set is specific to the current user's context.
    *
    * @param    dateFormat
    *           Defines the order of date components for conversion to/from
    *           strings.
    */
   public static void setDateComponentOrder(byte[] dateFormat)
   {
      work.obtain().dateFormat = dateFormat;
   }
   
   /** 
    * Gets the order of date components for conversion to/from strings.
    * <p>
    * The order of the 3 date sub-components is a 3 character string using
    * "M", "D" and "Y" once each.  The order from index 0 to index 2
    * represents the left to right ordering of the components. So the leftmost
    * date component will be defined by the character at index position 0.
    * <p>
    * The value accessed is specific to the current user's context.
    *
    * @return   Defines the order of date components for conversion to/from strings.
    */
   public static String getDateOrderNative()
   {
      WorkArea      wa = work.obtain();
      StringBuilder sb = new StringBuilder();
      
      for (int i = 0; i < 3; i++)
      {
         if (wa.dateFormat[i] == DAY)
         {
            sb.append('d');
         }
         else if (wa.dateFormat[i] == MONTH)
         {
            sb.append('m');
         }
         else
         {
            sb.append('y');
         }
      }
      
      return sb.toString();
   }
   
   /** 
    * Gets the order of date components for conversion to/from strings.
    * <p>
    * The order of the 3 date sub-components is a 3 character string using "M", "D" and "Y" once each. The
    * order from index 0 to index 2 represents the left to right ordering of the components. So the leftmost
    * date component will be defined by the character at index position 0.
    * <p>
    * The value accessed is specific to the current user's context.
    *
    * @return   Defines the order of date components for conversion to/from strings.
    */
   public static character getDateOrder()
   {
      return character.of(getDateOrderNative());
   }
   
   /** 
    * Sets the order of date components for conversion to/from strings.
    * <p>
    * The order of the 3 date sub-components as a 3 character string using
    * "M", "D" and "Y" once each.  The order from index 0 to index 2
    * represents the left to right ordering of the components. So the leftmost
    * date component will be defined by the character at index position 0.
    * <p>
    * The value set is specific to the current user's context.
    *
    * @param    dateFormat
    *           Defines the order of date components for conversion to/from strings.
    */
   public static void setDateOrder(String dateFormat)
   {
      setDateComponentOrder(initFormat(dateFormat));
   }
   
   /** 
    * Sets the order of date components for conversion to/from strings.
    * <p>
    * The order of the 3 date sub-components as a 3 character string using
    * "M", "D" and "Y" once each.  The order from index 0 to index 2
    * represents the left to right ordering of the components. So the leftmost
    * date component will be defined by the character at index position 0.
    * <p>
    * The value set is specific to the current user's context.
    *
    * @param    dateFormat
    *           Defines the order of date components for conversion to/from strings.
    */
   public static void setDateOrder(character dateFormat)
   {
      if (dateFormat != null && dateFormat.isUnknown())
      {
         genInvalidDateOrder();
         return;
      }
      
      setDateOrder(dateFormat == null ? null : dateFormat.toStringMessage());
   }
   
   /**
    * Sets the SESSION:TIMEZONE used as the default offset in minutes from
    * UTC for this user's context.
    *
    * @param    offset
    *           The new timezone offset which must be between -840 and 840
    *           inclusive.  The value may also be <code>unknown</code> which
    *           is the equivalent of resetting this attribute to the default.
    */
   public static void setTimeZone(NumberType offset)
   {
      final WorkArea wa = work.obtain();
      if (offset.isUnknown())
      {
         // handle particular case first
         wa.timezoneOffset = new integer();
         return;
      }
      
      long tz = offset.longValue();
      if (tz < MIN_VALID_TZ || tz > MAX_VALID_TZ)
      {
         // DO NOT RAISE AN ERROR CONDITION!  Simply display these on the
         // terminal's message line.  Yes, the cast to a short is deliberate
         // since the first error message is limited to that size number.
         // The result is that the over/underflow will be displayed if the
         // number can't fit.  Progress is insane that way.  The 2nd message
         // doesn't act the same way.  Go figure.
         String err1 = String.format(TZ_ATTR_ERR1, (short) tz);
         String err2 = String.format(TZ_ATTR_ERR2, tz);
         ErrorManager.displayError(14800, err1, false);
         ErrorManager.displayError(4057, err2, true);
         
         // no change will be made
         return;
      }
     
      wa.timezoneOffset = new integer(offset);
   }
   
   /**
    * Sets the SESSION:TIMEZONE used as the default offset in minutes from
    * UTC for this user's context.
    *
    * @param    offset
    *           The new timezone offset which must be between -840 and 840 inclusive.
    */
   public static void setSessionTimeZone(long offset)
   {
      setTimeZone(new int64(offset));
   }
   
   /**
    * Sets the <code>TIME-SOURCE</code> attribute for the user's context.
    * This attribute is can be read and written but it is not honored for 
    * date/time processing in P2J.  The intention of the attribute is to define
    * the system from which time values will be obtained. In P2J, time values
    * are always obtained from the application server.
    *
    * @param    timeSource
    *           The new value to set. If <code>null</code>, then the empty
    *           string will be used, which in the 4GL has the same affect as "local".
    */
   public static void setTimeSource(String timeSource)
   {
      work.obtain().timeSource = (timeSource == null) ? "LOCAL" : timeSource;
   }
   
   /**
    * Sets the <code>TIME-SOURCE</code> attribute for the user's context.
    * This attribute is can be read and written but it is not honored for 
    * date/time processing in P2J.  The intention of the attribute is to define
    * the system from which time values will be obtained. In P2J, time values
    * are always obtained from the application server.
    *
    * @param    timeSource
    *           The new value to set. If <code>null</code>, then the empty
    *           string will be used, which in the 4GL has the same affect as
    *           "local".
    */
   public static void setTimeSource(character timeSource)
   {
      if (timeSource == null || timeSource.isUnknown())
      {
         setTimeSource("LOCAL");
      }
      else 
      {
         setTimeSource(timeSource.toStringMessage());
      }
   }
   
   /**
    * Sets the <code>DISPLAY-TIMEZONE</code> attribute for the user's context.
    * This attribute is used for displaying DATETIME-TZ values that do not have 
    * the timezone specified in the display format. 
    *
    * @param    displayTimezone
    *           The new value to set.
    */
   public static void setDisplayTimeZone(long displayTimezone)
   {
      setDisplayTimeZone(new integer(displayTimezone));
   }
   
   /**
    * Sets the <code>DISPLAY-TIMEZONE</code> attribute for the user's context.
    * This attribute is used for displaying DATETIME-TZ values that do not have 
    * the timezone specified in the display format. 
    *
    * @param    displayTimezone
    *           The new value to set.
    */
   public static void setDisplayTimeZone(NumberType displayTimezone)
   {
      final WorkArea wa = work.obtain();
      if (displayTimezone == null || displayTimezone.isUnknown())
      {
         wa.displayTimezone = new integer();
         return;
      }
      
      // 4GL will allow any value here wrapping at 32768
      wa.displayTimezone = new integer((short) displayTimezone.longValue());
   }
   
   /**
    * Converts a 2 digit year to a 4 digit year, if needed.  Negative numbers
    * are not converted as these are considered to be in the BC era. The
    * conversion is based on the windowing start year which can be specified 
    * using {@link #setWindowingYear}.
    *
    * @param    year
    *           The year which will be converted to a 4 digit year based on
    *           the starting window year if and only if it is between 0 and
    *           99 inclusive.
    *
    * @return   The converted 4 digit year or the passed-in year if the
    *           parameter does not need conversion.
    */
   public static int windowingImpl(int year)
   {
      return date.windowingImpl(year, work.obtain().windowingYear);
   }
   
   /**
    * Calculates the current date using the local system clock time.
    *
    * @return   The current date as a julian day number.
    */
   private static long _today()
   {
      return millisToJulianDay(dateToLocalMillis(null, date.getDefaultTimeZone()));
   }
   
   /**
    * Converts a 2 digit year to a 4 digit year, if needed.  Negative numbers
    * are not converted as these are considered to be in the BC era. The
    * conversion is based on the windowing start year which can be specified 
    * using the second argument.
    *
    * @param    year
    *           The year which will be converted to a 4 digit year based on
    *           the starting window year if and only if it is between 0 and
    *           99 inclusive.
    * @param    windowingYear
    *           The window start year used for conversion.
    *
    * @return   The converted 4 digit year or the passed-in year if the
    *           parameter does not need conversion.
    */
   private static int windowingImpl(int year, int windowingYear)
   {
      int converted = year;
      
      // is this year a 2 digit year?
      if (year >= 0 && year <= 99)
      {
         // use integer arithmetic to "drop" everything but the century
         int century = windowingYear / 100;
         century *= 100;
         
         // calculate the difference as the "break" year
         int breakyr = windowingYear - century; 
         
         // everything at or later than the break year is in the earlier
         // century
         if (year >= breakyr)
         {
            converted = year + century;
         }
         else
         {
            converted = year + century + 100;
         }
      }
      
      return converted;
   }
   
   /**
    * Converts a 2 digit year to a 4 digit year, if needed.  Negative numbers
    * are not converted as these are considered to be in the BC era. The
    * conversion is based on the windowing start year which can be specified 
    * using the second argument.
    *
    * @param    year
    *           The year which will be converted to a 4 digit year based on
    *           the starting window year if and only if it is between 0 and
    *           99 inclusive.
    *
    * @return   The converted 4 digit year or the passed-in year if the
    *           parameter does not need conversion.
    */
   public static int windowing(final int year)
   {
      return windowingImpl(year, getWindowingYear());
   }
   
   /**
    * Returns the number of seconds since midnight in the current JVM timezone.
    *
    * @return   The number of seconds since midnight.
    */
   public static int64 secondsSinceMidnight()
   {
      // this represents the current time in the default JVM timezone
      return secondsSinceMidnight(new GregorianCalendar());
   }
   
   /**
    * Returns the number of seconds since midnight in the given calendar.
    *
    * @param    gc
    *           The calendar to use for the calculation which includes the time
    *           (in millis since epoch) and timezone. This may be set to any
    *           time value (in the future or present) including the current time.
    *
    * @return   The number of seconds since midnight.
    */
   public static int64 secondsSinceMidnight(GregorianCalendar gc)
   {
      // query the time value
      int hrs  = gc.get(Calendar.HOUR_OF_DAY);
      int min  = gc.get(Calendar.MINUTE);
      int secs = gc.get(Calendar.SECOND);
      long ssm  = (hrs * 3600) + (min * 60) + secs;
      
      return int64.of(ssm);
   }
   
   /**
    * Returns the number of milliseconds since the base milliseconds counter
    * was reset or since the process started (this is an approximation).
    *
    * @return   The number of elapsed milliseconds.
    */
   public static int64 elapsed()
   {
      return elapsed(false); 
   }
   
   /**
    * Returns the number of milliseconds since the base milliseconds counter was reset or since
    * the process started (this is an approximation).
    *
    * @param   reset
    *          {@code true} to reset the base milliseconds counter to the current time.
    *
    * @return   The number of elapsed milliseconds.
    */
   public static int64 elapsed(boolean reset)
   {
      WorkArea wa = work.obtain();
      int64 result = int64.of(System.currentTimeMillis() - wa.baseMillis);
      
      if (reset)
      {
         wa.baseMillis = System.currentTimeMillis();
      }
      
      return result;
   }
   
   /**
    * Returns the number of milliseconds since the base milliseconds counter was reset or since
    * the process started (this is an approximation).
    *
    * @param   reset
    *          {@code true} to reset the base milliseconds counter to the current time.
    *
    * @return   The number of elapsed milliseconds.
    */
   public static int64 elapsed(logical reset)
   {
      return elapsed(reset.booleanValue());
   }
   
   /**
    * Adds <code>days</code> to the date of the left operand and returns a 
    * new instance representing that date.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand (the number of days to add to the day
    *           number). Note that this can be a negative number to
    *           effectively subtract instead of add.
    *
    * @return   The date which is <code>days</code> different from the
    *           left operand or <code>unknown value</code> if any input is
    *           <code>unknown value</code>.
    */
   public static date plusDays(date op1, long op2)
   {
      if (op1.isUnknown())
      {
         return new date();
      }
      
      return new date(op1.julian + op2);
   }
   
   /**
    * Adds <code>days</code> to the date of the left operand and returns a 
    * new instance representing that date.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand (the number of days to add to the day
    *           number). Note that this can be a negative number to
    *           effectively subtract instead of add.  As in Progress, the
    *           value is rounded to the nearest integral value before usage.
    *
    * @return   The date which is <code>days</code> different from the
    *           left operand or <code>unknown value</code> if any input is
    *           <code>unknown value</code>.
    */
   public static date plusDays(date op1, double op2)
   {
      if (op1.isUnknown())
      {
         return new date();
      }
      
      return new date(op1.julian + Math.round(op2));
   }
   
   /**
    * Adds <code>days</code> to the date of the left operand and returns a 
    * new instance representing that date.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand (the number of days to add to the day
    *           number). Note that this can be a negative number to
    *           effectively subtract instead of add.
    *
    * @return   The date which is <code>days</code> different from the
    *           left operand or <code>unknown value</code> if any input is
    *           <code>unknown value</code>.
    */
   public static date plusDays(date op1, NumberType op2)
   {
      if (op1.isUnknown() || op2.isUnknown())
      {
         return new date();
      }
      
      return plusDays(op1, op2.longValue());
   }
   
   /**
    * Convenience method to subtract <code>days</code> from the date which
    * is the left operand and return a new instance representing that date. 
    * This just uses {@link #plusDays} after negating the parameter's sign.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand (the number of days to subtract from the 
    *           day number.
    *
    * @return   The date which is <code>days</code> different from the
    *           left operand or <code>unknown value</code> if any input is
    *           <code>unknown value</code>.
    */
   public static date minusDays(date op1, long op2)
   {
      return plusDays(op1, -op2);
   }
   
   /**
    * Convenience method to subtract <code>days</code> from the date which
    * is the left operand and return a new instance representing that date. 
    * This just uses {@link #plusDays} after negating the parameter's sign.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand (the number of days to subtract from the 
    *           day number. As in Progress, the value is rounded to the
    *           nearest integral value before usage.
    *
    * @return   The date which is <code>days</code> different from the
    *           left operand or <code>unknown value</code> if any input is
    *           <code>unknown value</code>.
    */
   public static date minusDays(date op1, double op2)
   {
      return plusDays(op1, -op2);
   }
   
   /**
    * Convenience method to subtract <code>days</code> from the date which
    * is the left operand and return a new instance representing that date. 
    * This just uses {@link #minusDays}.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand (the number of days to subtract from the 
    *           day number.
    *
    * @return   The date which is <code>days</code> different from the
    *           left operand or <code>unknown value</code> if any input is
    *           <code>unknown value</code>.
    */
   public static date minusDays(date op1, NumberType op2)
   {
      if (op1.isUnknown() || op2.isUnknown())
      {
         return new date();
      }
      
      return minusDays(op1, op2.longValue());
   }
   
   /**
    * Returns the number of days difference between the the dates of two
    * instances.
    *
    * @param    op1
    *           The left operand.  Must NOT be <code>unknown</code>.
    * @param    op2
    *           The right operand (instance to subtract from the left
    *           operand).  Must NOT be <code>unknown</code>.
    *
    * @return   The difference in days between the two dates.
    */
   public static int difference(date op1, date op2)
   {
      // this is safe since the difference will never overflow an int,
      // because internally Progress limits years to a short (-32768 to 32767)
      return (int) (op1.julian - op2.julian);
   }

   /**
    * Returns the number of days difference between the dates of two instances.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand (instance to subtract from the left
    *           operand).
    *
    * @return   The difference in days between the two dates or <code>unknown value</code> if 
    *           any input is <code>unknown value</code>.
    */
   public static integer differenceNum(date op1, date op2)
   {
      if (op1.isUnknown() || op2.isUnknown())
      {
         return new integer();
      }
      
      return new integer(difference(op1, op2));
   }

   /**
    * Returns the current day of the week for the given <code>date</code>. 
    *
    * @param    dd
    *           The date to query.
    *
    * @return   The day of the month.
    */
   public static integer weekday(date dd)
   {
      return dd.getWeekdayNum();
   }
   
   /**
    * Returns the current day of the month for the given <code>date</code>. 
    *
    * @param    dd
    *           The date to query.
    *
    * @return   The day of the month.
    */
   public static integer day(date dd)
   {
      return dd.getDayNum();
   }
   
   /**
    * Returns the month of the year for the given <code>date</code>.
    *
    * @param    dd
    *           The date to query.
    *
    * @return   The month (1-based).
    */
   public static integer month(date dd)
   {
      return dd.getMonthNum();
   }
   
   /**
    * Returns the year for the given <code>date</code>. 
    *
    * @param    dd
    *           The date to query.
    *
    * @return   The year.
    */
   public static integer year(date dd)
   {
      return dd.getYearNum();
   }

   /**
    * Creates a new <code>date</code> instance that represents the 
    * <code>unknown value</code>.
    * <p>
    * This method is deprecated. Use the default constructor instead.
    * 
    * @return   An instance that represents the <code>unknown value</code>.
    */
   @Deprecated
   public static date instantiateUnknownDate()
   {
      return new date();
   }
   
   /**
    * Return the default display format string for this type.
    *
    * @return   The default format string.
    */
   @Override
   public String defaultFormatString()
   {
      return DEFAULT_FORMAT;
   }
   
   /**
    * Obtain the length (in bytes) of this BDT will use when serialized. 
    *
    * @return  the length of this BDT will use when serialized.
    */
   @Override
   public int getSize()
   {
      if (isUnknown())
      {
         return 1;
      }
      
      long raw = julian - RAW_EPOCH; 
      
      if (raw == 0)
      {
         return 1;
      }
      else if (-127L <= raw && raw <= 127L) // 0x7F note: 7F, the other are 7E...
      {
         return 2;
      }
      else if (-32_512L <= raw && raw <= 32_511L) // 0x7EFFL
      {
         return 3;
      }
      else if (-8_323_072L <= raw && raw <= 8_323_071L) //  7E_FFFF
      {
         return 4;
      }
      else
      {
         return 5;
      }
   }
   
   /**
    * An equality test which handles the most common cases with the least
    * amount of overhead.  The superclass implementation is invoked otherwise.
    * <p>
    * This implementation is consistent with {@link #hashCode()}.
    * 
    * @see com.goldencode.p2j.util.BaseDataType#equals(java.lang.Object)
    */
   @Override
   public boolean equals(Object o)
   {
      if (this == o)
      {
         // compare to itself ?
         return true;
      }
      if (o instanceof unknown && this.isUnknown())
      {
         // if this is unknown then it is equal to typeless unknown 
         return true;
      }
      if (!(o instanceof date) || o instanceof datetime)
      {
         // a date can be compared ONLY WITH other instances of date
         return false;
      }
      
      date that = (date) o; 
      boolean u1 = this.isUnknown();
      boolean u2 = that.isUnknown();
      
      if (u1 && u2)
      {
         // two unknown are equals
         return true;
      }
      if (u1 || u2)
      {
         // only of is unknown, the other is not
         return false;
      }
      
      return this.julian == that.julian;
   }
   
   /**
    * Hash code implementation which is consistent with {@link
    * BaseDataType#equals}.
    *
    * @return  Hash code value for this object instance.
    */
   @Override
   public int hashCode()
   {
      int result = 17;
      if (isUnknown())
      {
         return result;
      }
      
      result = 37 * result + (int) (julian ^ (julian >>> 32));
      
      return result;
   }
   
   /**
    * Does the same as standard <code>clone()</code> method but returns an
    * instance of <code>BaseDataType</code> and doesn't throw the
    * <code>CloneNotSupportedException</code>.
    *
    * @return   A clone of this instance.
    */
   @Override
   public date duplicate()
   {
      return new date(this);
   }
   
   /**
    * Creates a new instance of the same type that represents the <code>unknown value</code>.
    *
    * @return   An instance that represents the <code>unknown value</code>.
    */
   @Override
   public BaseDataType instantiateUnknown()
   {
      return new date();
   }

   /**
    * Creates a new instance of the same type that represents the default initialized value.
    *
    * @return   An instance that represents the default value.
    */
   @Override
   public BaseDataType instantiateDefault()
   {
      return new date();
   }
   
   /**
    * Reports if this instance represents the <code>unknown value</code>.
    *
    * @return   <code>true</code> if this instance is set to the <code>unknown value</code>.
    */
   @Override
   public boolean isUnknown()
   {
      return unknown;
   }
   
   /**
    * Sets the state of this instance's <code>unknown value</code> flag to
    * <code>true</code>.
    * <p>
    * <b>Warning: a separate call is needed to ensure that the data of this
    * instance is set to the correct value.</b>
    */
   @Override
   public void setUnknown()
   {
      if (!unknown)
      {
         checkUndoable(true);
      }
      
      unknown = true;
   }
   
   /**
    * Sets the state (data and unknown value) of this instance based on the
    * state of the passed instance.
    * <p>
    * If the value is not of type <code>date</code>, the following automatic type
    * conversion will occur:
    * <p>
    * <ol>
    *    <li> if the value is a date/datetime/datetimetz value, the current value is used
    *    <li> if the value is unknown (including the unknown literal) or is a string with the
    *         value '?', sets the instance to unknown
    *    <li> if the value is of type logical, the value is converted to integer (1 / 0) and then 
    *         evaluate for the new value
    *    <li> if the value is a non-unknown numeric value, including recid, this uses the number
    *         as the julian day
    *    <li> if the value is a non-unknown character value, it attempts to parse this as a
    *         properly formatted date
    *    <li> otherwise it will recordOrThrowError number 5729.
    * </ol>
    * <p>
    * This variant is meant to handle the cases of built-in functions and methods in the 4GL
    * which have polymorphic return types (e.g. DYNAMIC-FUNCTION()). This should NOT be used for
    * non-polymorphic assignments.
    *
    * @param    value
    *           The instance from which to copy state.
    */
   @Override
   public void assign(BaseDataType value)
   {
      long oldJulian = julian;

      if (value == null)
      {
         setUnknown();
         return;
      }
      
      // the value may be a proxy; make sure to unwrap and work with the real value
      value = value.val();
      if (value.isUnknown())
      {
         if (value instanceof longchar || value instanceof clob || value instanceof blob)
         {
            incompatibleTypesOnConversion();
         }
         else 
         {
            setUnknown();
         }
      }
      else if (value instanceof date)
      {
         // this is the common path, also handles datetime and datetimetz 
         assign((date) value);
      }
      else
      {
         // this path is only used when type morphing is needed
         if (value == null || value.isUnknown())
         {
            setUnknown();
         }
         else if (value instanceof character)
         {
            // covers both character and longchar
            
            String txt = value.toStringMessage();
            
            if ("?".equals(txt))
            {
               // quick out
               setUnknown();
            }
            else
            {
               // try the more extensive datetime-tz parsing first
               if (ErrorManager.nestedSilent(() -> 
               {
                  assign(new datetimetz(txt));
               }))
               {
                  // try the less-specific parsing approach
                  WorkArea wa = getWorkArea();
                  
                  julian = parseWorker(value.toStringMessage(), wa.dateFormat, wa.windowingYear);
                  
                  if (julian == INVALID_DATE)
                  {
                     setUnknown();
                  }
                  else
                  {
                     unknown = false;
                  }
               }
            }
         }
         else if (value instanceof logical)
         {
            /* true = 1 => [01.01.-4713], false = 0 => [31.12.-4714] */
            julian = ((logical) value).booleanValue() ? 1 : 0;
            unknown = false;
         }
         else if (value instanceof handle)
         {
            Long id = ((handle) value).getResourceId();
            
            if (id != null)
            {
               julian = id;
               unknown = false;
            }
            else
            {
               setUnknown();
            }
         }
         else if (value instanceof NumberType)
         {
            julian = ((NumberType) value).longValue();
            unknown = false;
         }
         else if (value instanceof jobject)
         {
            assign((jobject<?>) value);
         }
         else
         {
            setUnknown();

            // conversion not possible
            incompatibleTypesOnConversion();
         }
      }
      
      if (oldJulian != julian)
      {
         checkUndoable(true);
      }
   }
   
   /**
    * Sets the state (data and unknown value) of this instance based on the state of the passed 
    * instance. If a derived class object is passed (datetime or datetimetz), then the extra 
    * informations (time, time-zone offset) are ignored.  Overriding methods from derived classes
    * must call supper to assign these bits of information.
    *
    * @param    value
    *           The instance from which to copy state.
    */
   public void assign(date value)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      if (BaseDataType.isProxy(value))
      {
         assign(value.val());
         return;
      }
      
      if (value == null || value.isUnknown())
      {
         setUnknown();
         return;
      }
      
      deepAssign(value.toLocalDatetime());
   }
   
   /**
    * Sets the state (data and unknown value) of this instance based on the
    * state of the passed instance.
    *
    * @param    value
    *           The instance from which to copy state.
    */
   @Override
   public void assign(Undoable value)
   {
      if (value instanceof BaseDataType)
      {
         assign((BaseDataType) value);
      }
      else
      {
         throw new RuntimeException("Assignment of " + value.getClass() + 
               " to " + this.getClass() + " not implemented.");
      }
   }
   
   /**
    * Sets the state (data and unknown value) and configuration (time-zone)
    * of this instance based on the state of the passed instance.
    *
    * @param    value
    *           The instance from which to copy state.
    */
   public void deepAssign(date value)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      if (BaseDataType.isProxy(value))
      {
         if (value.val() instanceof date)
         {
            value = (date) value.val();
         }
         else
         {
            assign(value.val());
            return;
         }
      }
      if (value == null || value.isUnknown())
      {
         setUnknown();
         return;
      }
      
      checkUndoable(value);
      
      julian  = value.julian;
      unknown = value.unknown;
   }
   
   /**
    * Increments the Julian day number by 1.
    * 
    * @return    The instance of this date object
    */
   public date increment()
   {
      checkUndoable(true);

      julian++;
      return this;
   }
   
   /**
    * Decrements the Julian day number by 1.
    * 
    * @return    The instance of this date object
    */
   public date decrement()
   {
      checkUndoable(true);

      julian--;
      return this;
   }
   
   /**
    * Sets the Julian day number (independently of the state of the unknown
    * value) as an integral number of days since the Progress 4GL base date
    * of 12/31/-4714.  This also sets the <code>unknown value</code> state
    * for this instance to <code>false</code>.
    *
    * @param    day
    *           The number of days since 12/31/-4714.
    */
   public void setDayNumber(long day)
   {
      if (unknown || day != julian)
      {
         checkUndoable(true);
      }
      
      julian = day;
      unknown = false;
   }
   
   /**
    * Sets the Julian day number (independently of the state of the unknown
    * value) as an integral number of days since the Progress 4GL base date
    * of 12/31/-4714.  This also sets the <code>unknown value</code> state
    * for this instance to <code>false</code>.
    *
    * @param    day
    *           The number of days since 12/31/-4714.
    */
   public void setDayNumber(double day)
   {
      setDayNumber(new decimal(day));
   }

   /**
    * Sets the Julian day number (independently of the state of the unknown
    * value) as an integral number of days since the Progress 4GL base date
    * of 12/31/-4714.  This also sets the <code>unknown value</code> state
    * for this instance to <code>false</code>.
    *
    * @param    day
    *           The number of days since 12/31/-4714.
    */
   public void setDayNumber(NumberType day)
   {
      if (day.isUnknown())
      {
         setUnknown();
      }
      else 
      {
         setDayNumber(day.longValue());
      }
   }
   
   /**
    * Returns the instance which has a later date.
    *
    * @param    d
    *           The instance to compare against.
    *
    * @return   The instance with the latest date.
    */
   public date maximum(date d)
   {
      return (date) super.maximum(d);
   }
   
   /**
    * Returns the instance which has an earlier date.
    *
    * @param    d
    *           The instance to compare against.
    *
    * @return   The instance with the earliest date.
    */
   public date minimum(date d)
   {
      return (date) super.minimum(d);
   }
   
   /**
    * Compares this instance with the specified instance and returns a -1
    * if this instance is less than the specified, 0 if the two instances
    * are equal and 1 if this instance is greater than the specified
    * instance.  This is the implementation of the <code>Comparable</code>
    * interface.
    * <p>
    * The algorithm will fail to give meaningful results in the case where
    * one tries to sort against other objects that do not represent compatible
    * values.
    *
    * @param    obj
    *           The date instance to compare against.
    *
    * @return   -1, 0 or 1 depending on if this instance is less than,
    *           equal to or greater (respectively) than the specified
    *           instance <code>obj</code>.
    */
   @Override
   public int compareTo(Object obj)
   {
      Object tmp = obj;
      
      if (obj instanceof BaseDataType)
      {
         BaseDataType bdt = (BaseDataType) obj;
         
         tmp = new date(bdt);
         
         /**
          * An unknown value should be always converted to a unknown date value.
          * A non-unknown value should be always converted to a non-unknown date value.
          * If an unknown value is converted to a non-unknown value the conversion fails.
          * If a non-unknown value is converted to an unknown value the conversion fails.
          */
         if (((date) tmp).isUnknown() != bdt.isUnknown())
         {
            incompatibleTypesOnConversion();
         }
      }
      else
      {
         /* ErrorManager.recordOrThrowError(
               223, "Incompatible data types in expression or assignment"); */
         return -1;
      }

      // The documentation say a date can be compared ONLY WITH other instances of date.
      // This is true for compile-time values, the static checking will print error 223
      // "Incompatible data types in expression or assignment");
      // However, if the compare operator receives dynamically different kind of date-related 
      // only the "common parts" are compared - ie. ignore additional fields from derived classes.
      date that = (date) tmp;
      
      boolean u1 = this.isUnknown();
      boolean u2 = that.isUnknown();
      
      if (u1 && u2)
      {
         // two unknown are equals
         return 0;
      }
      if (u1 || u2)
      {
         // only of is unknown, the other is not
         return -1;
      }
      
      if (this.julian < that.julian)
      {
         return -1;
      }
      else if (this.julian > that.julian)
      {
         return 1;
      }
      
      // otherwise they are equals
      return 0;
   }
   
   /**
    * Returns the month of the current instance from 1 to 12 independent of
    * any timezone considerations.
    *
    * @return   The month.
    */
   public int getMonth()
   {
      long              millis = julianDayToMillis(julian);
      GregorianCalendar gc     = neutralMillisToCalendar(millis, null);
      
      // Java months are zero-indexed
      return gc.get(Calendar.MONTH) + 1;
   }
   
   /**
    * Returns the month of the current instance from 1 to 12 independent of
    * any timezone considerations.
    *
    * @return   The month or <code>unknown</code> if this instance is
    *           <code>unknown</code>.
    */
   public integer getMonthNum()
   {
      return isUnknown() ? new integer() : new integer(getMonth());
   }
   
   /**
    * Returns the day of the month of the current instance from 1 to the
    * highest possible day of the corresponding month (maximum of 31)
    * independent of any timezone considerations.
    * 
    * @return  The day of the month or <code>unknown</code> if this instance is 
    *          <code>unknown</code>.
    */
   public int getDay()
   {
      long              millis = julianDayToMillis(julian);
      GregorianCalendar gc     = neutralMillisToCalendar(millis, null);
      
      return gc.get(Calendar.DAY_OF_MONTH);
   }
   
   /**
    * Returns the day of the month of the current instance from 1 to the
    * highest possible day of the corresponding month (maximum of 31)
    * independent of any timezone considerations.
    * 
    * @return   The day of the month or <code>unknown</code> if this instance
    *           is <code>unknown</code>.
    */
   public integer getDayNum()
   {
      return isUnknown() ? new integer() : new integer(getDay());
   }
   
   /**
    * Returns the day of the week of the current instance from 1 (Sunday) to
    * 7 (Saturday) independent of any timezone considerations.
    *
    * @return  The day of the week or <code>unknown</code> if this instance is 
    *          <code>unknown</code>.
    */
   public int getWeekday()
   {
      long              millis = julianDayToMillis(julian);
      GregorianCalendar gc     = neutralMillisToCalendar(millis, null);
      
      return gc.get(Calendar.DAY_OF_WEEK);
   }
   
   /**
    * Returns the day of the week of the current instance from 1 (Sunday) to
    * 7 (Saturday) independent of any timezone considerations.
    *
    * @return   The day of the week or <code>unknown</code> if this instance is
    *           <code>unknown</code>.
    */
   public integer getWeekdayNum()
   {
      return isUnknown() ? new integer() : new integer(getWeekday());
   }
   
   /**
    * Returns the year of the current instance with a negative value used
    * to represent the years in the BC era, independent of any timezone
    * considerations.
    *
    * @return   The year.
    */
   public int getYear()
   {
      long              millis = julianDayToMillis(julian);
      GregorianCalendar gc     = neutralMillisToCalendar(millis, null);
            
      int year = gc.get(Calendar.YEAR);
      
      if (gc.get(Calendar.ERA) == GregorianCalendar.BC)
         year = -year;
      
      return year;
   }
   
   /**
    * Returns the year of the current instance with a negative value used
    * to represent the years in the BC era, independent of any timezone
    * considerations.
    *
    * @return   The year or <code>unknown</code> if this instance is <code>unknown</code>.
    */
   public integer getYearNum()
   {
      return isUnknown() ? new integer() : new integer(getYear());
   }
   
   /**
    * Returns the current instance as an instance of the <code>Date</code>
    * class which will accurately represent the UTC time that corresponds
    * to the current date in the timezone stored in this instance or
    * in the default timezone if the stored timezone is <code>null</code>.
    * <p>
    * <strong>WARNING:</strong> since the J2SE <code>Date</code> class
    * implicitly encodes locale-specific time information along with date
    * information, the <code>Date</code> object returned by this method is
    * sensitive to the environment in which it is <em>interpreted</em>
    * downstream.  It is imperative therefore, when using the returned
    * <code>Date</code> in downstream code, that the timezone of such code
    * (e.g., a date formatter or <code>Calendar</code> instance) matches the
    * timezone stored in this <code>date</code> instance (if the stored
    * timezone is <code>null</code>, the JVM's default timezone should be used
    * in downstream code).  Otherwise, the discrepancy in timezones (including
    * any Daylight Savings offset) may cause downstream code to interpret the
    * date information in the returned object incorrectly, typically as one
    * day earlier than expected.
    *
    * @return   The date converted to be specific to the stored timezone or
    *           to the default timezone if the stored timezone is <code>null</code>.
    */
   public Date dateValue()
   {
      return dateValue(TimeZone.getDefault());
   }
   
   /**
    * Returns the current instance as an instance of the <code>Date</code>
    * class which will accurately represent the UTC time that corresponds
    * to the current date in the timezone passed as a parameter or in the
    * default timezone if the passed timezone is <code>null</code>.
    * <p>
    * <strong>WARNING:</strong> since the J2SE <code>Date</code> class
    * implicitly encodes locale-specific time information along with date
    * information, the <code>Date</code> object returned by this method is
    * sensitive to the environment in which it is <em>interpreted</em>
    * downstream.  It is imperative therefore, when using the returned
    * <code>Date</code> in downstream code, that the timezone of such code
    * (e.g., a date formatter or <code>Calendar</code> instance) matches the
    * the <code>override</code> parameter to this method (if
    * <code>override</code> is <code>null</code>, the JVM's default timezone
    * should be used in downstream code).  Otherwise, the discrepancy in
    * timezones (including any Daylight Savings offset) may cause downstream
    * code to interpret the date information in the returned object
    * incorrectly, typically as one day earlier than expected.
    *
    * @param    override
    *           The timezone to use for interpreting this date or 
    *           <code>null</code> if the default JVM timezone should be used.
    *
    * @return   The date converted to be specific to the passed timezone.
    */
   public Date dateValue(TimeZone override)
   {
      if (isUnknown())
      {
         return null;
      }
      
      if (override == null)
      {
         override = TimeZone.getDefault();
      }
      
      return neutralMillisToDate(julianDayToMillis(julian), override);
   }
   
   /**
    * Returns the day number as a <code>double</code>.
    *
    * @return   The date as the number of days since 12/31/-4714.
    */
   public double doubleValue()
   {
      return julian;
   }
   
   /**
    * Returns the day number as an {@link integer}.
    *
    * @return   The date as the number of days since 12/31/-4714.
    */
   public decimal decimalValue()
   {
      return new decimal(julian);
   }
   
   /**
    * Returns the day number as an <code>int</code>.
    *
    * @return   The date as the number of days since 12/31/-4714.
    */
   public int intValue()
   {
      return (int) julian;
   }
   
   /**
    * Returns the day number as an {@link integer}.
    *
    * @return   The date as the number of days since 12/31/-4714.
    */
   public integer integerValue()
   {
      return new integer(julian);
   }
   
   /**
    * Returns the day number as a <code>long</code>.
    *
    * @return   The date as the number of days since 12/31/-4714.
    */
   public long longValue()
   {
      return julian;
   }

   /** 
    * Determines if the current date lies inside the 100 year window where
    * a 2-digit date implicitly corresponds to a 4-digit date.
    *
    * @return   <code>true</code> if the year is within the range specified
    *           beginning at the windowing year and ending 99 years later.
    *           By default, the windowing year is 1950 so the ending year
    *           is 2049.  Any dates in this range can be specified as
    *           2-digits.
    */
   public boolean inY2KWindow()
   {
      WorkArea wa   = getWorkArea();
      int      year = getYear();
      
      return (year >= wa.windowingYear && year < (wa.windowingYear + 100));
   }
   
   /**
    * Creates a SQL compatible string representation of the instance data
    * (the format is YYYY-MM-DD).
    * <p>
    * The year component is treated differently since it can have a sign (negative indicates 
    * BC era) and if the date is within the Y2K window it is truncated to fit.
    *
    * @return   The formatted string or the 'failure' string if the date is
    *           too large to fit in the specified format.
    */
   public String toStringSQL()
   {
      NumberFormat nf = NumberFormat.getInstance();
      nf.setMinimumIntegerDigits(4);
      nf.setGroupingUsed(false);
      
      StringBuilder sb = new StringBuilder();
      
      sb.append(nf.format(getYear()));
      sb.append('-');
      
      nf.setMinimumIntegerDigits(2);
      sb.append(nf.format(getMonth()));
      sb.append('-');
      sb.append(nf.format(getDay()));
      
      return sb.toString();
   }
      
   /**
    * Creates a string representation of the instance data using the default
    * Progress 4GL format of '99/99/99' which is only useful for displaying
    * dates in the Y2K window since any other date will generally not be
    * displayable in such a format.  Some dates are too large to fit in the
    * default format.  In such cases the resulting string has all '9'
    * characters of the format string replaced with the '?'.  For example, a
    * failure with a format string of '99/99/99' will result in a '??/??/??'. 
    * <p>
    * The year component is treated differently since it can have a sign
    * (negative indicates BC era) and if the date is within the Y2K window it
    * is truncated to fit.
    *
    * @return   The formatted string or the 'failure' string if the date is
    *           too large to fit in the specified format.
    *
    * @throws   IllegalArgumentException
    *           If the format string passed is invalid.
    */
   @Override
   public String toString()
   {
      return toString(null);
   }
   
   /**
    * Creates a string representation of the instance data in a form that is
    * compatible with the <code>MESSAGE</code> language statement.  If the
    * instance represents the <code>unknown value</code>, a '?' will be
    * returned.
    *
    * @return   The 'message' formatted string.
    */
   @Override
   public String toStringMessage()
   {
      return toStringExport();
   }
   
   /**
    * Creates a string representation of the instance data using the Progress
    * 4GL 'export' format.  This format is unusual in that it does not have
    * a fixed width for the year but in fact it ensures that all years are
    * formatted to at least 3 digits (with leading zeros) up to a maximum of
    * 6 digits (for the year -32768 which is the widest possible Progress
    * year) as necessary.  The months and days are always formatted as
    * '99'. Another 'quirk' of this format is that if the year is within
    * the Y2K window, it is formatted as a 2 digit year.  The separators are
    * always '/' characters.
    * <p>
    * This implementation creates the given string based on the order of
    * date components (e.g. MDY, YMD...) defined for this context.
    *
    * @return   The 'export' formatted string.
    */
   @Override
   public String toStringExport()
   {
      if (unknown)
      {
         return "?";
      }
      
      NumberFormat nf = NumberFormat.getInstance();
      nf.setMinimumIntegerDigits(2);
      nf.setGroupingUsed(false);
      
      WorkArea      wa = getWorkArea();
      StringBuilder sb = new StringBuilder();
      
      for (int i = 0; i < 3; i++)
      {
         if (wa.dateFormat[i] == MONTH)
         {
            sb.append(nf.format(getMonth()));
         }
         else if (wa.dateFormat[i] == DAY)
         {
            sb.append(nf.format(getDay()));
         }
         else
         {
            String yr = toYearString(3, 6, true);
            
            if (yr == null)
            {
               return StringHelper.repeatChar('?', DEFAULT_FORMAT.length() - 2);
            }
            
            sb.append(yr);
         }
         
         // add separators between the 1st/2nd and 2nd/3rd components
         if (i < 2)
         {
            sb.append('/');
         }
      }
      
      return sb.toString();
   }
   
   /**
    * Create a string representation of this date using a Progress 4GL style
    * date format string such as 99/99/99 or 99-99-9999. The '/', '-' and
    * '.' are valid separators.  All 3 components of the date must be
    * represented in the format string and the number of digits in each
    * component is used to pad on the left with zeros as well as to detect 
    * when values are too large to fit in the requested format.  In such
    * cases the resulting string has all '9' characters of the format string
    * replaced with the '?'.  For example, a failure with a format string of
    * '99/99/99' will result in a '??/??/??'. 
    * <p>
    * There are 2 (and only 2) cases where separator characters are not
    * needed: '999999' and '99999999'.  Any other format string without
    * separator characters will cause an error to occur. 
    * <p>
    * The year component is treated differently since it can have a sign
    * (negative indicates BC era) and if the date is within the Y2K window it
    * is truncated to fit inside a 9 (if possible without losing data) or 99
    * format (but not a 999 format).
    * <p>
    * This implementation creates the given string based on the order of
    * date components (e.g. MDY, YMD...) defined for this context.
    *
    * @param    fmt
    *           The Progress style format string to use or <code>null</code>
    *           to use the default '99/99/99'.
    *
    * @return   The formatted string or the 'failure' string if the date is too large to fit in
    *           the specified format.
    *           If the date is {@code unknown} an empty string is returned.
    *
    * @throws   IllegalArgumentException
    *           If the format string passed is invalid.
    */
   @Override
   public String toString(String fmt)
   throws IllegalArgumentException
   {
      // set our default format as needed
      if (fmt == null || fmt.trim().isEmpty())
      {
         fmt = DEFAULT_FORMAT;
      }
      
      if (isUnknown())
      {
         char[] blanks = new char[fmt.length()];
         Arrays.fill(blanks, ' ');
         return new String(blanks);
      }
      
      final ParsedDateFormat parsed = ParsedDateFormat.getInstance(fmt);
      
      final NumberFormat nf = NumberFormat.getInstance(); 
      nf.setGroupingUsed(false);
      
      final byte[] dateFormat = getWorkArea().dateFormat;
      final StringBuilder sb = new StringBuilder();
      
      // create our output string in proper component order
      for (int n = 0; n < 3; n++)
      {         
         final byte comp = dateFormat[n];
         if(comp == YEAR)
         {
            // string output for the YEAR component
            
            final int size = parsed.size[n];
            final String yr = toYearString(size, size, false);

            if (yr == null)
            {
               return genFailedString(fmt);
            }
            
            sb.append(yr);
         }
         else
         {
            final int val = comp == MONTH ? getMonth() : getDay();
            // string output for MONTH or DAY components
            
            // will it fit?
            final int compSize = parsed.size[n];
            if (compSize == 1 && val > 9)
            {
               return genFailedString(fmt);
            }
            
            nf.setMinimumIntegerDigits(compSize);
            sb.append(nf.format(val));
         }
         
         if (parsed.hasSep())
         {
            if (n == 0)
            {
               // output the first separator
               sb.append(parsed.sep1);
            }
            else if (n == 1)
            {
               // output the second separator
               sb.append(parsed.sep2);
            }
         }
      }
      
      return sb.toString();
   }
   
   /**
    * Replacement for the default object reading method. The latest
    * state is read from the input source.
    * 
    * @param    in
    *           The input source from which fields will be restored.
    *
    * @throws   IOException
    *           In case of I/O errors.
    * @throws   ClassNotFoundException
    *           If payload can't be instantiated.
    */
   @Override
   public void readExternal(ObjectInput in)
   throws IOException,
          ClassNotFoundException
   {
      unknown = in.readBoolean();
      julian  = in.readLong();
   }

   /**
    * Replacement for the default object writing method. The latest
    * state is written to the output destination.
    * 
    * @param    out
    *           The output destination to which fields will be saved.
    *
    * @throws   IOException
    *           In case of I/O errors.
    */
   @Override
   public void writeExternal(ObjectOutput out)
   throws IOException
   {
      out.writeBoolean(unknown);
      out.writeLong(julian);
   }
   
   /**
    * Parses a string with a format based on the Progress 4GL rules for converting a string to a
    * {@code date}.
    * <p>
    * If the {@code spec} string is empty or equals '?', an unknown date object is instantiated.
    *
    * @param   spec
    *          The date to parse.
    * @param   format
    *          The date format used for parsing.  If it is null the default is used.
    * @param   windowingYear
    *          The windowing year used for 2 digits years. If it is negative the default is used.
    */
   protected void instantiateFromStringWorker(final String spec, final String format, final int windowingYear)
   {
      if (spec == null || unknownDateSpecRegexp.matcher(spec).matches())
      {
         setUnknown();
         return;
      }
      
      if ("today".equalsIgnoreCase(spec))
      {
         assign(date.today());
         return;
      }
      
      int effWindowingYear = windowingYear < 0 ? getWindowingYear() : windowingYear;
      
      checkUndoable(() -> new date(spec, format, effWindowingYear));
      
      byte[] binaryFormat = format == null/*auto*/ ? getWorkArea().dateFormat : initFormat(format);
      julian = parseWorker(spec, binaryFormat, effWindowingYear);
      
      unknown = (julian == INVALID_DATE);
   }

   /**
    * Returns the absolute time in milliseconds since Java 'Epoch' or 0
    * if this instance is <code>unknown</code>.
    *
    * @return   The absolute time in milliseconds.
    */
   protected long getAbsoluteTimeOffset()
   {
      return isUnknown() ? 0 : julian * date.MILLIS_PER_DAY;
   }
   
   /**
    * Return an object equal to this expressed in local time (current timezone offset).
    *
    * @return  this object (it's already in local time)
    */
   public datetime toLocalDatetime()
   {
      datetime ret = new datetime();
      ret.deepAssign(this);
      return ret;
   }
   
   /**
    * All {@code date} and {@code datetime} used in 4GL are assumed at local time-zone. This method will
    * convert them to the appropriate UTC instant.
    * 
    * @return  the UTC instant of this {@code date} instance.
    */
   public datetime utc()
   {
      int tz = datetimetz.getDefaultTimezoneOffset(this); // minutes
      return datetime.plusMillis(new datetime(this), -tz * 60 * 1000L);
   }
   
   /**
    * Obtains the text form of this date instance in ISO 8601 standard format.
    * 
    * @return   A string that is the ISO representation of this date
    *           ("YYYY-MM-DD") or "?" if <code>unknown</code>.
    */
   public String getIsoDate()
   {
      if (isUnknown())
      {
         return "?";
      }
      
      int year = getYear();
      if (year < -999 || year > 9999)
      {
         // this is a strange thing (among other) that I discovered in Progress
         
         ErrorManager.recordOrThrowError(0, "");
         return "?";
      }
      
      int month = getMonth();
      int day = getDay();
      
      StringBuilder sb = new StringBuilder();
      if (year < 1000)
      {
         if (year < 100)
         {
            if (year < 10)
            {
               sb.append('0');
            }
            sb.append('0');
         }
         sb.append('0');
      }
      sb.append(year);
      sb.append("-");
      if (month < 10)
      {
         sb.append('0');
      }
      sb.append(month);
      sb.append("-");
      if (day < 10)
      {
         sb.append('0');
      }
      sb.append(day);

      return sb.toString();
   }
   
   /**
    * Parses this ISO 8601 value as a date.
    *  
    * @param    val
    *           The date to parse.
    * 
    * @throws   DateTimeParseException
    *           If the format is not ISO 8601 compliant.
    */
   public void fromIso(String val)
   {
      LocalDate od = LocalDate.parse(val, ISO_LOCAL_DATE);
      Date date = Date.from(od.atStartOfDay()
                      .atZone(ZoneId.systemDefault())
                      .toInstant());
      assign(new date(date));
   }
   
   /**
    * Parses the literal encountered in a INITIAL option. Does the best effort to parse the text of the
    * literal taking into account the current session settings (like the date format and the windowing year
    * and the moment when the method is invoked.
    * <p>
    * {@code today} is also supported, in which case the current date is returned. 
    * 
    * @param   text
    *          The text of the literal to be parsed. 
    * 
    * @return  A {@code date} representing the respective value, taking into account the current session
    *          settings (like the date format and the windowing year). 
    */
   public static date parseInitial(String text)
   {
      if ("today".equalsIgnoreCase(text))
      {
         return today();
      }
      
      String df = getDynamicFormat();
      if (df != null)
      {
         // when dynamic format is not null, the date may be in any format the user selected in
         // SESSION:DATE-FORMAT
         return new date(text, df, getWindowingYear());
      }
      
      // if not in dynamic format, the date format is constrained to American format (MDY), even if other
      // format may be active in SESSION:DATE-FORMAT.
      return new date(text, DEFAULT_DATE_FORMAT, DEFAULT_DATE_WINDOWING_YEAR);
   }
   
   /**
    * Validate specified values and return error information if they don't consist of correct date components.
    * 
    * @param   month
    *          The month.
    * @param   day
    *          The day.
    * @param   year
    *          The year.
    * @param   forceFourDigits
    *          Flag for converting the year to 4 digits before processing.
    *          
    * @return   <code>null</code> if components are valid and error information otherwise.
    */
   public static ErrorEntry validate(int month, int day, int year, boolean forceFourDigits)
   {
      if (forceFourDigits)
      {
         year = windowingImpl(year);
      }

      if (month < 1 || month > 12)
      {
         return new ErrorEntry(80, "The month of a date must be from 1 to 12");
      }
      
      // some fixups to match the J2SE format
      if (year < 0)
         year++;
      
      // J2SE months are 0-indexed
      month--;
      
      // we are using the default timezone
      GregorianCalendar gc = nullGC.get();
      
      // we want to detect invalid day of month without rewriting the
      // calendar ourselves, so we must take a step-wise approach rather
      // than the more obvious simple constructor approach (note: setLenient
      // cannot be used because it doesn't trigger any exception when
      // invalid input is used, even though the documentation explicitly
      // states that it will do so)
      gc.clear();
      gc.set(Calendar.YEAR, year);
      gc.set(Calendar.MONTH, month);
      
      // now that the month and year are set, the maximum can be calculated
      int limit = gc.getActualMaximum(Calendar.DAY_OF_MONTH);
      
      if (day < 1 || day > limit)
      {
         return new ErrorEntry(81, "Day in month is invalid");
      }

      return null;
   }

   /**
    * Obtains the text form of the given date instance in ISO 8601 standard format.
    * 
    * @param    date
    *           The date to format as text.
    *
    * @return   A string that is the ISO representation of this date
    *           (YYYY-MM-DD) or "?" if <code>unknown</code>.
    */
   public static character isoDate(date date)
   {
      return date.isUnknown() ? new character() : new character(date.getIsoDate());
   }

   /**
    * Parses this ISO 8601 value as a date, datetime or datetime-tz, depending on which format matches.  
    * The parsing is LENIENT, and this API is meant to be used in cases where this behavior is required
    * (like for SOAP or REST requests, when parsing arguments). 
    *  
    * @param    val
    *           The date to parse.
    *           
    * @return   The date value or <code>null</code> if it can't be parsed.
    */
   public static final date parseIso8601(String val)
   {
      try
      {
         // check for date
         date d = new date();
         d.fromIso(val);
         return d;
      }
      catch (DateTimeParseException e)
      {
         // ignore
      }
      
      try
      {
         // check for a datetime
         datetime dt = new datetime();
         dt.fromIso(val);
         return dt;
      }
      catch (DateTimeParseException e)
      {
         // ignore
      }

      try
      {
         // check for a datetime-tz
         datetimetz dtz = new datetimetz();
         dtz.fromIso(val);
         return dtz;
      }
      catch (DateTimeParseException e)
      {
         // ignore
      }
      
      return null;
   }
   
   /**
    * Parses a ISO-8601 like literal as a date.  The format is always <code>ymd</code>.
    * <p>
    * This API relies on {@link #instantiateFromStringWorker} to parse the literal.
    * 
    * @param    val
    *           The date to parse.
    *           
    * @return   The date value.
    */
   public static date parseLiteral(String val)
   {
      return new date(val, DEFAULT_DATETIME_FORMAT, -1);
   }
   
   /**
    * Obtains the timezone offset in minutes from UTC that corresponds to the
    * given timezone specification. This is a form of the TIMEZONE built-in function.
    * @param spec 
    *        timezone spec
    *
    * @return   The timezone offset in minutes from UTC for the given spec.
    */
   public static integer getOffsetForSpec(character spec)
   {
      if (spec.isUnknown())
      {
         return new integer();
      }
      
      return getOffsetForSpec(spec.toStringMessage());
   }
   
   /**
    * Obtains the timezone offset in minutes from UTC that corresponds to the
    * given timezone specification. This is a form of the TIMEZONE built-in
    * function.
    *
    * @param spec 
    *        timezone spec
    * @return   The timezone offset in minutes from UTC for the given spec.
    */
   public static integer getOffsetForSpec(String spec)
   {
      if (spec == null)
      {
         return new integer();
      }
      
      return new integer(specToOffset(spec));
   }
   
   /**
    * Obtains the effective default timezone offset in minutes from UTC for
    * this user's context. This will get the default timezone as specified in
    * the SESSION:TIMEZONE offset or if not specified there, the default JVM
    * timezone's "raw offset" (in milliseconds) converted to minutes.  This is
    * a form of the TIMEZONE built-in function.
    *
    * @return   The effective default timezone offset in minutes from UTC.
    */
   public static integer getDefaultTimeZoneOffset()
   {
      return new integer(getDefaultOffset());
   }
   
   /**
    * Get the default timezone as specified in the SESSION:TIMEZONE offset or if not specified
    * there, the default JVM timezone's "offset" (raw + dst) converted from milliseconds to
    * minutes.
    *
    * @return   The default timezone.
    */
   public static int getDefaultOffset()
   {
      return getDefaultOffset(null);
   }
   
   /**
    * Get the default timezone for the date received as parameter. In case of null,
    * the default JVM timezone's "offset" (raw + dst) converted from milliseconds to 
    * minutes.
    * 
    * @param    d
    *           The date for which we want to calculate the timezone's offset.
    * 
    * @return   The default timezone.
    */
   public static int getDefaultOffset(Date d)
   {
      integer tzAttr = work.obtain().timezoneOffset; // date.getSessionTimeZone()
      int     offset = 0;
      
      if (tzAttr.isUnknown())
      {
         // get the raw offset in millis and convert it to minutes
         TimeZone defTz = TimeZone.getDefault(); 
         // it seems that in Progress the timezone also contains the DST
         offset = defTz.getRawOffset();
         if (d == null && defTz.inDaylightTime(new Date()) || 
             d != null && defTz.inDaylightTime(d))
         {
            offset += defTz.getDSTSavings();
         }
         offset = offset / (1000 * 60);
      }
      else
      {
         offset = tzAttr.intValue();
      }
      
      return offset;
   }
   
   /**
    * Get the default timezone as specified in the SESSION:TIMEZONE offset or
    * if not specified there, the default JVM timezone.
    *
    * @return   The default timezone.
    */
   public static TimeZone getDefaultTimeZone()
   {
      return getZoneFromOffset(getDefaultOffset());
   }
   
   /**
    * Convert the offset to a timezone ID and return the resulting timezone
    * instance.
    *
    * @param    offset
    *           The offset in minutes (positive or negative) from UTC.
    *
    * @return   The corresponding timezone.
    */
   public static TimeZone getZoneFromOffset(int offset)
   {
      TimeZone tz = offset2tz.get(offset);
      
      if (tz == null)
      {
         offset2tz.put(offset, tz = TimeZone.getTimeZone(offsetToZoneId(offset)));
      }
      
      return tz;
   }
   
   /**
    * Remove unnecessary characters and extra zero digits from input that represents
    * a date.  This compensates for "tricks" that can be done in the 4GL which
    * have no useful value (they don't store any data).
    *
    * @param    input
    *           The text to cleanup.
    *
    * @return   The cleaned up text.
    */
   public static String cleanFormattedDate(String input)
   {
      // remove leading whitespace
      String clean = StringHelper.safeTrimLeading(input);
      
      int end = clean.length();
      int i;
      boolean hasSeparators = false;
      
      // remove trash at end of the string (except for trailing separator
      // characters)
      for (i = 0; i < end; i++)
      {
         char ch = clean.charAt(i);
         
         if (Character.isDigit(ch))
         {
            continue;
         }
         else if (ch == '-' || ch == '/' || ch == '.')
         {
            hasSeparators = true;
            continue;
         }
         // first invalid char exits
         break;
      }
      String cleaner = clean.substring(0, i);
      
      // any trailing separator chars have no purpose and must be ignored
      for (i = cleaner.length() - 1; i >= 0; i--)
      {
         char ch = cleaner.charAt(i);
         
         if (ch != '-' && ch != '/' && ch != '.')
         {
            // first non-separator exits
            break;
         }
      }
      String cleanest = cleaner.substring(0, i + 1);
      
      StringBuilder sb = new StringBuilder(cleanest);
      
      if (hasSeparators)
      {
         // if input does not have separators, keep all 0s
         int next = 0;
         
         while (true)
         {
            next = trimZeros(sb, next);
            
            if (next == -1)
            {
               break;
            }
         }
      }
      
      return sb.toString();
   }
   
   /**
    * Identify if this data is dependent upon the context local. This identifies
    * if the data is safe to be cached in cross-session places or the data should
    * not leak from the context. If is is not safe to be shared cross-session, then
    * this should return {@code null}. If other representation is suitable for caching,
    * then this can return another object that wraps internal values that are not
    * dependent upon the context (e.g. date, date-time, date-time-tz).
    * 
    * @return   Always. {@code null}.
    */
   @Override
   public Object getIndependentFromContext()
   {
      return new ContextIndependentDate(this.unknown, this.julian);
   }
   
   /**
    * Obtains the timezone offset in minutes from UTC that corresponds to the
    * given timezone specification.
    * <p>
    * The string cannot be empty and it must start with a sign character.
    * 
    * @param spec
    *           see above
    *
    * @return   The timezone offset in minutes from UTC for the given spec or
    *           INVALID_TZ on any error.
    */
   protected static int specToOffset(String spec)
   {
      int    len = spec.length();
      char[] txt = spec.toCharArray();
      
      // default this to a character that will fail for empty string
      char sign = (len > 0) ? txt[0] : ' '; 
      
      // empty string or not a sign generates this error
      if (sign != '-' && sign != '+')
      {
         ErrorManager.recordOrThrowError(11869, "Missing sign in timezone expression");
         return INVALID_TZ;
      }
      
      if (len < 5 || len > 6)
      {
         ErrorManager.recordOrThrowError(11872, "Invalid timezone expression");
         return INVALID_TZ;
      }
      
      // digit : or digit digit : must be next
      if (!Character.isDigit(txt[1]) ||
          (!Character.isDigit(txt[2]) && txt[2] != ':'))
      {
         ErrorManager.recordOrThrowError(11871, "Invalid digit in timezone expression");
         return INVALID_TZ;
      }
      
      int idx = spec.indexOf(":");
      
      // check on colon char
      if ((len == 5 && idx != 2) || (len == 6 && idx != 3))
      {
         ErrorManager.recordOrThrowError(11870, "Invalid colon in timezone expression");
         return INVALID_TZ;
      }
      
      // last 2 characters must be digits
      if (!Character.isDigit(txt[len - 2]) || !Character.isDigit(txt[len - 1]))
      {
         ErrorManager.recordOrThrowError(11872, "Invalid timezone expression");
         return INVALID_TZ;
      }
      
      // final parsing and calc
      String hh  = spec.substring(1, idx);
      String mm  = spec.substring(idx + 1);
      
      int hours  = Integer.parseInt(hh);
      int mins   = Integer.parseInt(mm) + (hours * 60);
      
      return mins * ((sign == '-') ? -1 : 1);
   }
   
   /**
    * Convert the Progress-style timezone offset (in minutes) to a JVM-style
    * timezone ID that can be used to obtain a <code>TimeZone</code> instance.
    *
    * @param    offset
    *           The offset in minutes (positive or negative) from UTC. Must be
    *           between -840 and 840 inclusive.
    *
    * @return   The corresponding timezone ID.
    */
   private static String offsetToZoneId(int offset)
   {
      boolean sign = (offset >= 0);
      int absol    = Math.abs(offset);
      int hours    = absol / 60;             // must be between 0 and 23 incl
      int minutes  = (absol - (hours * 60)); // must be between 0 and 59 incl
      
      // error processing should be done in the caller due to different error
      // messages needed at the call sites
      
      return String.format("GMT%s%02d:%02d", sign ? "+" : "-", hours, minutes);
   }
   
   /**
    * Get the timezone-specific calendar out of the JVM-wide cache or if one
    * is not yet in the cache, create it and add it to the cache before
    * returning.
    *
    * @param    zone
    *           The timezone the calendar must support.
    *
    * @return   The timezone-specific calendar.
    */
   protected static GregorianCalendar getZoneCalendar(TimeZone zone)
   {
      if (tzcals == null)
      {
         tzcals = new HashMap<>();
      }
       
      ZoneCalendar zc = tzcals.get(zone);
      
      if (zc == null)
      {
         zc = new ZoneCalendar(zone);
         tzcals.put(zone, zc);
      }
      
      return zc.get();
   }
   
   /**
    * Accesses the SESSION:TIMEZONE used as the default offset in minutes from
    * UTC for this user's context.
    *
    * @return  A safe copy of timezone offset. If this has never been set, then {@code unknown}
    *          will be returned.
    */
   static integer getTimeZone()
   {
      return integer.of(work.obtain().timezoneOffset);
   }
   
   /**
    * Obtains the currently configured DISPLAY-TIMEZONE attribute for the current
    * user's context. This attribute is used for displaying DATETIME-TZ values that do not have 
    * the timezone specified in the display format. 
    *
    * @return   The display-timezone attribute.
    */
   static integer getDisplayTimeZone()
   {
      return new integer(work.obtain().displayTimezone);
   }
   
   /**
    * Obtains the currently configured TIME-SOURCE attribute for the current
    * user's context.  This attribute is can be read and written but it is
    * not honored for date/time processing in P2J.  The intention of the
    * attribute is to define the system from which time values will be obtained.
    * In P2J, time values are always obtained from the application server. 
    * <p>
    * The default value is "local" which in Progress means that the client
    * machine is the source  (this is the same as the empty string "").  It is
    * common to use a database name in the 4GL, which makes the application
    * use the database server as the time source.
    *
    * @return   The time source attribute.
    */
   static character getTimeSource()
   {
      return new character(work.obtain().timeSource);
   }

   /**
    * Generate the output string for the condition where any component of the 
    * date cannot fit into the specified format string.  For a format of
    * "99/99/99", the result will be "??/??/??".  The error message is
    * also processed here.
    *
    * @param    fmt
    *           The format string upon which to pattern the returned result. 
    *
    * @return   The output string corresponding to a failure.
    */
   private String genFailedString(String fmt)
   {
      String err = String.format("Value %s cannot be displayed using %s",
                                 toStringExport(),
                                 fmt);
      
      if (ErrorManager.isSilentError())
      {
         // we don't throw an error here, as we have to return a string
         // even when one of these two failures occurs, however if silent
         // mode is enabled we do need to add the error text to the list
         ErrorManager.addError(74, err, false);
      }
      else
      {
         // TODO: Determine if this code is needed (a previous comment noted
         //       that it was needed, BUT a testcase is not known that proves
         //       the idea. Note that this code *is* needed for NumberType
         //       but possibly not here. Certainly in the case of unknown
         //       value, this display code should not be executed. For now,
         //       we leave it disabled until we can determine all the paths
         //       through and know for sure the behavior needed.
         
         // DO NOT RAISE AN ERROR CONDITION!  Simply display this on the
         // terminal's message line.
         // ErrorManager.displayError(ErrorManager.buildErrorText(74, err));
      }
      
      return fmt.replaceAll("9", "?");
   }
   
   /**
    * Initializes an new instance based on explicit calendar components.
    * This routine will set <code>unknown value</code> if the calculated
    * Julian date in invalid.
    *
    * @param    yy
    *           The Progress 4GL year which may be a negative number which
    *           represents the BC era or a positive AD era year. The year
    *           0 is invalid in Progress but in Java it is equivalent to the
    *           1st year BC.  All BC years are offset in J2SE by 1 from the
    *           Progress equivalent.  Thus, the value is fixed up to be
    *           J2SE compatible.
    * @param    mm
    *           The month from 1 to 12 (Progress format) which is then
    *           fixed up to be J2SE compatible before use (0-based).
    * @param    dd
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less depending on the month and year).
    *
    * @throws   ErrorConditionException
    *           If the month or day of month are invalid.
    */
   private void yymmddWorker(int yy, int mm, int dd)
   throws ErrorConditionException
   {
      yymmddWorker(yy, mm, dd, false);
   }
   
   /**
    * Initializes an new instance based on explicit calendar components. 
    * This routine will set <code>unknown value</code> if the calculated 
    * Julian date in invalid.
    *
    * @param    yy
    *           The Progress 4GL year which may be a negative number which
    *           represents the BC era or a positive AD era year. The year
    *           0 is invalid in Progress but in Java it is equivalent to the
    *           1st year BC.  All BC years are offset in J2SE by 1 from the
    *           Progress equivalent.  Thus, the value is fixed up to be
    *           J2SE compatible.
    * @param    mm
    *           The month from 1 to 12 (Progress format) which is then
    *           fixed up to be J2SE compatible before use (0-based).
    * @param    dd
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less depending on the month and year).
    * @param    forceFourDigits
    *           If true then 2-digit years will be windowed into the right
    *           century.
    *
    * @throws   ErrorConditionException
    *           If the month or day of month are invalid.
    */
   private void yymmddWorker(int yy, int mm, int dd, boolean forceFourDigits)
   throws ErrorConditionException
   {
      // convert 2-digit years into 4-digit
      int yr = forceFourDigits ? windowingImpl(yy) : yy;
      
      julian = instantiateDate(yr, mm, dd);
      
      if (julian == INVALID_DATE)
      {
         unknown = true;
      }
   }
   
   /**
    * Get the current context-local data structure. If this is the first time it is called the
    * value is cached for subsequent calls.
    * 
    * @return  the current context-local data structure.
    */
   private WorkArea getWorkArea()
   {
      if (workArea == null)
      {
         workArea = work.obtain();
      }
      return workArea;
   }
   
   /**
    * Returns the index position within the input string where the next separator
    * (following the starting position) can be found.  Any of the 3 possible separator
    * characters will be found ('/', '-' or '.').
    *
    * @param    input
    *           The text to scan.
    * @param    start
    *           The starting index for the search.  Only characters starting at or
    *           following that index will be considered.
    *
    * @return   The index of the next separator or -1 if no further separator can be
    *           found.
    */
   private static int findNextSeparator(StringBuilder input, int start)
   {
      int len = input.length();
      int idx = start < len ? start : len;
      
      while (idx < len)
      {
         char ch = input.charAt(idx);
         
         if (ch == '-' || ch == '/' || ch == '.')
         {
            return idx;
         }
         
         idx++;
      }
      
      return -1;
   }
   
   /**
    * Remove leading zeros from one segment of a string that represents a date.
    * All leading zeros from the given start index up to by not including the first
    * non-zero character OR the character just before the next separator or end
    * of text. The buffer passed in will be edited.
    *
    * @param    sb
    *           The buffer to edit.
    * @param    start
    *           The index from which to remove the zeros.
    *
    * @return   The index position just after the separator that was found or -1 if
    *           the end of the text was encountered.
    */
   private static int trimZeros(StringBuilder sb, int start)
   {
      int idx   = start;
      int sep   = findNextSeparator(sb, start);
      int end   = (sep != -1) ? (sep - 2) : (sb.length() - 4);
      
      while (idx < end)
      {
         char z = sb.charAt(idx);
         
         if (z == '0')
         {
            idx++;
         }
         else
         {
            break;
         }
      }
      
      if (idx != start)
      {
         // there is something to remove
         sb.delete(start, idx);
      }
      
      // if we removed something, we have to find the separator again
      sep = findNextSeparator(sb, start);
      
      return (sep != -1 && sep < sb.length()) ? ++sep : -1;
   }
   
   /**
    * Core worker that implements the default Progress 4GL string to date
    * conversion processing as documented by {@link #date(java.lang.String)}.
    * <p>
    * This implementation parses the given string based on the order of
    * date components (e.g. MDY, YMD...) defined by the second argument.
    *
    * @param    input
    *           The formatted string date to convert.
    * @param    dateFormat
    *           The format used for date conversion.
    * @param    windowingYear
    *           The year window used when year is represented with 2-digits.
    *
    * @return   The corresponding Julian day number.
    * 
    * @throws ErrorConditionException 
    */
   private long parseWorker(String input, byte[] dateFormat, int windowingYear)
   throws ErrorConditionException
   {
      if (input == null || StringHelper.safeTrimLeading(input).isEmpty() || "?".equals(input)) 
      {
         return INVALID_DATE;
      }
      else if ("today".equalsIgnoreCase(input))
      {
         return _today();
      }
      else if ("now".equalsIgnoreCase(input))
      {
         invalidInitializer("NOW");
         return INVALID_DATE;
      }
      
      // this processing would be better placed in a Progress-specific 
      // subclass of java.text.DateFormat
      int mm = 0;
      int dd = 0;
      int yy = 0;
      
      // initial cleanups
      String clean = cleanFormattedDate(input);
      if (StringHelper.safeTrimLeading(clean).isEmpty())
      {
         ErrorManager.recordOrThrowError(85, "Invalid date input");
         return INVALID_DATE;
      }
      
      // standardize/normalize our separators to be '-' characters (it can
      // be any mixture of '-' or '/' or '.' characters on input
      String cleaner = clean.replaceAll("[./]", "-");
      
      // handle the special case of no separators by adding them
      String cleanest = addSeparators(dateFormat, cleaner);
      
      String[] pieces = new String[3];
      
      int len  = cleanest.length();
      int pos  = 0;
      int chk  = 0;
      int seps = 0;
      for (int idx = 0; idx < len; idx++)
      {
         if (cleanest.charAt(idx) == '-' && 
             (idx > 0 || idx == len - 1 || (idx != 0 && cleanest.charAt(idx + 1) != '-')))
         {
            seps = seps + 1;
            while (idx < len && cleanest.charAt(idx + 1) == '-')
            {
               idx = idx + 1;
            }
         }
      }
      
      // a '-' char is not a separator when it is the negative sign in a
      // year; there may also be bogus trailing '-' chars at the end of
      // the proper date components; find the real separators and use them
      // to split the data into the 2 or 3 components that are specified
      for (int j = 0; j < 3; j++)
      {
         byte comp = dateFormat[j];
         
         // test for the special MM-DD parsing mode that ignores the year
         // component (and takes the current year instead)
         if (seps == 1 && comp == YEAR )
         {
            // bypass the year component
            continue;
         }
         
         // avoid treating a leading negative sign on the component, as a separator
         if (cleanest.charAt(chk) == '-')
         {
            chk++;
         }
         
         // find the next separator
         int idx = cleanest.indexOf('-', chk);
         
         // there must always be at least 1 separator and 2 components
         if (idx >= 0)
         {
            pieces[comp] = cleanest.substring(pos, idx);

            pos = idx + 1;
            chk = pos;

            // if the input data ends with a separator, we are done
            if (chk >= len)
               break;
         }
         else
         {
            // get the rest of the string and exit
            pieces[comp] = cleanest.substring(pos);
            
            pos = len;
            chk = pos;
            break;
         }
      }
      
      if (chk < len)
      {
         ErrorManager.recordOrThrowError(85, "Invalid date input");
         return INVALID_DATE;
      }
      
      // safety code for empty strings (probably should not ever happen
      // anyway, but let's be safe)
      for (int x = 0; x < 3; x++)
      {
         if (pieces[x] != null && pieces[x].length() == 0)
         {
            pieces[x] = null;
         }
      }
      
      // at this point our pieces array is indexed by our component types
      // and should hold 2 or 3 non-null strings
      
      if (pieces[DAY] == null || pieces[MONTH] == null)
      {
         if (pieces[MONTH] != null)
         {
            mm = Integer.parseInt(pieces[MONTH]);
            if (mm < 1 || mm > 12)
            {
               ErrorManager.recordOrThrowError(80, "The month of a date must be from 1 to 12");
               return INVALID_DATE;
            }
            
         }
         ErrorManager.recordOrThrowError(85, "Invalid date input");
         return INVALID_DATE;
      }
      
      dd = Integer.parseInt(pieces[DAY]);
      mm = Integer.parseInt(pieces[MONTH]);
      
      if (pieces[YEAR] != null)
      {
         yy = Integer.parseInt(pieces[YEAR]);
         
         if (pieces[YEAR].length() <= 2)
            yy = date.windowingImpl(yy, windowingYear);
         else if (yy == 0)
         {
            ErrorManager.recordOrThrowError(79, "Year is out of range or 0");
            return INVALID_DATE;
         }
      }
      else
      {
         // query the current year
         yy = date.today().getYear();
      }
      
      return instantiateDate(yy, mm, dd);
   }
   
   /**
    * Add separators to a string (which has no separators) where the data
    * represents a date.  Progress date parsing from a string has a special
    * mode in which there are no separators.  Special parsing rules apply.
    * This method adds the separators that are defined by those special rules.
    * <p>
    * The rules as they are known:
    * <p>
    * <pre>
    * Input Length  YMD Format  MDY Format  Notes
    * ------------  ----------  ----------  ---------------------------------
    * less than 3   n/a         n/a         "Invalid date format" error 85
    * 3             MMD         MMD
    * 4             MMDD        MMDD
    * 5             YYMMD       MMDDY
    * 6             YYMMDD      MMDDYY
    * 7             YYMMDD      MMDDYY      the 7th character is dropped
    * 8             YYYYMMDD    MMDDYYYY
    * more than 8   YYYYMMDD    MMDDYYYY    the 9th and following characters are dropped
    * </pre>
    * <p>
    * This method adds '-' separators and only deals with '-' separators.
    *
    * @param    dateFormat
    *           Caller provided dateFormat. Must not be <code>null</code>.
    * @param    input
    *           Text representation of a date with no separators. Must not
    *           be <code>null</code>.
    *
    * @return   A new text representation of a date with separators added if
    *           they had not already been present. If separators existed in
    *           the input string, then the input string will be returned unchanged.
    */
   private static String addSeparators(byte[] dateFormat, String input)
   {
      if (input.indexOf('-') < 0)
      {
         int len = input.length();
         
         if (len < 3)
         {
            if (!"".equals(input.trim()))
            {
               ErrorManager.recordOrThrowError(85, "Invalid date input");
            }
            return input;
         }
         
         StringBuilder sb = new StringBuilder();
         
         // all forms under 8 characters parse all components as 2 characters
         // (or less in the case of a 3 character length), so the order of
         // the components does not matter
         if (len < 8)
         {
            // special case of trimming
            if (len == 7)
            {
               // drop the extra character
               input = input.substring(0, 6);
               len   = 6;
            }
            
            // all of the under 8 sized cases have at least 2 fields with the
            // first field always being 2 characters in size
            sb.append(input.substring(0, 2)).append('-');
            
            if (len < 5)
            {
               // these cases only have 2 date components, read the rest
               sb.append(input.substring(2));
            }
            else
            {
               // these cases have 3 fields
               sb.append(input.substring(2, 4)).append('-').append(input.substring(4));
            }
         }
         else
         {
            // special case of trimming
            if (len > 8)
            {
               // drop the extra character(s)
               input = input.substring(0, 8);
               len   = 8;
            }
            
            int pos = 0;
            
            // the insertion of the separators must be made according to the
            // order of date components since the year component will be
            // parsed as 4 characters in this case
            for (int j = 0; j < 3; j++)
            {
               int sz  = (dateFormat[j] == YEAR) ? 4 : 2;
               int end = pos + sz;
               
               sb.append(input.substring(pos, end));
               pos = end;
               
               if (j < 2)
               {
                  sb.append('-');
               }
            }
         }
         
         return sb.toString();
      }
      
      return input;
   }
   
   /**
    * Convert the components of a date into a Julian day number that is Progress compatible.
    *
    * @param    year
    *           The Progress 4GL year which may be a negative number which
    *           represents the BC era or a positive AD era year. The value must
    *           be between -32768 and 32767 and it cannot be 0 since the year
    *           0 is invalid in Progress. In Java the year 0 is equivalent to
    *           the 1st year BC (all BC years are offset in J2SE by 1 from the
    *           Progress equivalent).  Any valid values are fixed up to be
    *           J2SE compatible.  
    * @param    month
    *           The month from 1 to 12 (Progress format) which is then
    *           fixed up to be J2SE compatible before use (0-based).
    * @param    day
    *           The day of the specified month between 1 and the largest
    *           possible date (31 or less depending on the month and year).
    *
    * @return   The Progress compatible Julian day number.
    * 
    * @throws ErrorConditionException 
    *
    * @throws   IllegalArgumentException
    *           If the month or day of month are invalid.
    */
   protected static long instantiateDate(int year, int month, int day)
   throws ErrorConditionException
   {
      if (year < -32768 || year > 32767 || year == 0)
      {
         ErrorManager.recordOrThrowError(79, "Year is out of range or 0");
         return INVALID_DATE;
      }
      
      if (month < 1 || month > 12)
      {
         ErrorManager.recordOrThrowError(80, "The month of a date must be from 1 to 12");
         return INVALID_DATE;
      }
      
      // some fixups to match the J2SE format
      if (year < 0)
         year++;
      
      // J2SE months are 0-indexed
      month--;
      
      // we are using the default timezone
      GregorianCalendar gc = nullGC.get();
      
      // we want to detect invalid day of month without rewriting the
      // calendar ourselves, so we must take a step-wise approach rather
      // than the more obvious simple constructor approach (note: setLenient
      // cannot be used because it doesn't trigger any exception when
      // invalid input is used, even though the documentation explicitly
      // states that it will do so)
      gc.clear();
      gc.set(Calendar.YEAR, year);
      gc.set(Calendar.MONTH, month);
      
      // now that the month and year are set, the maximum can be calculated
      int limit = gc.getActualMaximum(Calendar.DAY_OF_MONTH);
      
      if (day < 1 || day > limit)
      {
         ErrorManager.recordOrThrowError(81, "Day in month is invalid");
         return INVALID_DATE;
      }
      
      // insert the final piece of the puzzle
      gc.set(Calendar.DAY_OF_MONTH, day);
      
      // remove useless timezone-specific mods and return the Julian day
      return millisToJulianDay(calendarToLocalMillis(gc));
   }
   
   /**
    * Generate a string representation of the year based on the unusual
    * rules of the Progress 4GL.
    *
    * @param    min
    *           Specifies the minimum number of output characters. The result
    *           will be zero padded on the left as necessary.  This value
    *           cannot be greater than the 'max' parameter but can be set to
    *           the same value. Must be greater than zero.
    * @param    max
    *           Specifies the maximum number of output characters. If the
    *           result cannot fit into this number of characters, then
    *           a <code>null</code> string will be returned. As a special
    *           case, years within the Y2K window will be truncated to
    *           1 (if possible without losing data) or 2 characters if
    *           max is set to these values respectively.  All other values
    *           must 'naturally' fit inside the maximum.  Must be greater
    *           than zero and larger than the 'min' parameter.
    * @param    force
    *           Determines whether values within the Y2K window will be
    *           forced to 2 digits on output.  This is only used for the
    *           unusual 'export' format.
    *
    * @return   The formatted string or <code>null</code> if the result is
    *           too large to fit.
    */
   protected String toYearString(int min, int max, boolean force)
   {
      NumberFormat nf  = NumberFormat.getInstance(); 
      
      // make sure the laws of logic hold
      if (min > max || min < 1 || max < 1)
         return null;
      
      int yy     = getYear();
      int actual = min;
      
      // if we have a negative number, we can only pad up to min - 1 chars
      // except when force is enabled
      if (yy < 0 && !force)
         actual--;
      
      nf.setMinimumIntegerDigits(actual);
      nf.setGroupingUsed(false);
      
      String year = nf.format(yy);
      
      // non-export mode windowing allows truncation
      if (!force && inY2KWindow() && max < 3)
      {
         // even truncation doesn't help if we lose data
         if ((yy % 100) > 9 && max == 1)
            return null;
         
         // truncate
         if (year.length() > max)
            return year.substring(year.length() - max);
      }
      else
      {
         // test if we fit
         if (year.length() > max)
            return null;
      }
      
      // do we need to force a 2 digit output during the Y2K window?
      if (force)
      {
         year = inY2KWindow() ? year.substring(year.length() - 2) : year;
      }
      
      return year;
   }
   
   /**
    * Method that calcultes the year, month and day stored in an integer array.
    * For calculating these values it uses the Julian's number.
    *
    * @return Array of ints that contains in order the year, month and day.
    */
   protected int[] getYearMonthDay()
   {
      long              millis = julianDayToMillis(julian);
      GregorianCalendar gc     = neutralMillisToCalendar(millis, null);
      
      int[] dateParts = new int[3];
      //calculate the year
      dateParts[0] = gc.get(Calendar.YEAR);
      
      if (gc.get(Calendar.ERA) == GregorianCalendar.BC)
         dateParts[0] = -dateParts[0];
      
      //calculate the month
      dateParts[1] = gc.get(Calendar.MONTH) + 1;
      
      //calculate the day
      dateParts[2] = gc.get(Calendar.DAY_OF_MONTH);
      
      return dateParts;
   }
   
   /**
    * Simple 'factory' method to create instances of a specific date using the
    * <code>GregorianCalendar</code> and handling the 0-based month processing on behalf of the
    * caller (the caller uses a 1-based month index).
    * 
    * @param yy
    *        The year.
    * @param mm
    *        The month from 1 - 12.
    * @param dd
    *        The day of the month.
    * 
    * @return A date object.
    */
   protected static Date createDate(int yy, int mm, int dd)
   {      
      // some fixups to match the J2SE format
      if (yy < 0)
         yy++;
      
      // J2SE months are 0-indexed
      mm--;
      
      GregorianCalendar gc = new GregorianCalendar(yy, mm, dd);
      return gc.getTime();
   }
   
   /** 
    * Determines the order of the 3 date sub-components by reading a 3
    * character string from the directory where there is one character each
    * for "D" (day), "M" (month) and "Y" (year).  The default value is "MDY"
    * in the case where the value is not encoded in the directory.
    * <p>
    * This string encoding of this order will be converted into a byte array
    * with 3 elements.  Each element is one of the <code>DAY</code>,
    * <code>MONTH</code> or <code>YEAR</code> constants of this class. Each
    * element may only appear once. The resulting array will be returned and
    * it defines the order of the components in a date when converted to/from
    * a string.
    * <p>
    * The value returned may have been found via a search algorithm that
    * is account (user or process) specific or group specific
    * within the current server or a global default for all servers. 
    * <p>
    * The implementation iteratively looks up the directory node under:
    * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/dateFormat
    * <p>
    * If no user/process or group nodes are present, then this is checked:
    * /server/&lt;serverID&gt;/runtime/default/dateFormat
    * <p>
    * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
    * (it is the global default area for all servers):
    * /server/default/runtime/&lt;account_or_group&gt;/dateFormat
    * <p>
    * Finally, if no user/process or group nodes are present in the global
    * default area, then this is checked:
    * /server/default/runtime/default/dateFormat
    * <p>
    * If no value is found via this lookup, then the Progress 4GL default  
    * of MDY will be returned.
    *
    * @param    fmt
    *           The explicit date format string to use or <code>null</code>
    *           to read from the directory.
    *
    * @return   The encoded date component order.
    */
   protected static byte[] initFormat(String fmt)
   {
      if (fmt == null)
      {
         fmt = SessionUtils._startupParameters().getDateFormat();
      }
      
      if (fmt == null)
      {
         Directory dir = DirectoryManager.getInstance();
         fmt = dir.getString(Directory.ID_RELATIVE, "dateFormat", "MDY");
      }
      
      if (fmt == null || fmt.length() != 3)
      {
         genInvalidDateOrder();
         return ORDER_MDY;
      }
      
      // act case-insensitively
      fmt = fmt.toUpperCase();
      
      byte[] order = new byte[3];
      
      int didx = fmt.indexOf('D');
      int midx = fmt.indexOf('M');
      int yidx = fmt.indexOf('Y');
      
      editOrder(order, didx, DAY, 'D');
      editOrder(order, midx, MONTH, 'M');
      editOrder(order, yidx, YEAR, 'Y');
      
      if (didx == midx || midx == yidx || didx == yidx)
      {
         genInvalidDateOrder();
         return ORDER_MDY;
      }
      
      return order;
   }
   
   /**
    * Returns the beginning year of the 100 year period where 2 digit dates
    * implicitly correspond to 4 year dates.  This value is context-specific
    * to the current user's session.
    * <p>
    * The value returned may have been found via a search algorithm that
    * is account (user or process) specific or group specific
    * within the current server or a global default for all servers. 
    * <p>
    * The implementation iteratively looks up the directory node under:
    * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/windowingYear
    * <p>
    * If no user/process or group nodes are present, then this is checked:
    * /server/&lt;serverID&gt;/runtime/default/windowingYear
    * <p>
    * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
    * (it is the global default area for all servers):
    * /server/default/runtime/&lt;account_or_group&gt;/windowingYear
    * <p>
    * Finally, if no user/process or group nodes are present in the global
    * default area, then this is checked:
    * /server/default/runtime/default/windowingYear
    * <p>
    * If no value is found via this lookup, then the Progress 4GL default  
    * of 1950 will be returned.
    *
    * @return   The start of the 100 year range for Y2K windowing.
    */
   private static int initWindowingYear()
   {
      Directory dir = DirectoryManager.getInstance();
      return dir.getInt(Directory.ID_RELATIVE, "windowingYear", 1950);
   }
   
   /**
    * Returns the configured value in the directory for the default offset in
    * minutes from UTC (SESSION:TIMEZONE).  This value is context-specific to
    * the current user's session.
    * <p>
    * The value returned will have been found via a search algorithm that
    * is account (user or process) specific or group specific
    * within the current server or a global default for all servers. 
    * <p>
    * The implementation iteratively looks up the directory node under:
    * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/timezoneOffset
    * <p>
    * If no user/process or group nodes are present, then this is checked:
    * /server/&lt;serverID&gt;/runtime/default/timezoneOffset
    * <p>
    * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
    * (it is the global default area for all servers):
    * /server/default/runtime/&lt;account_or_group&gt;/timezoneOffset
    * <p>
    * Finally, if no user/process or group nodes are present in the global
    * default area, then this is checked:
    * /server/default/runtime/default/timezoneOffset
    *
    * @return   The offset in minutes from UTC or <code>unknown</code> if not
    *           set in the directory.
    */
   private static integer initTimezoneOffset()
   {
      Directory dir = DirectoryManager.getInstance();
      
      int tz = dir.getInt(Directory.ID_RELATIVE, "timezoneOffset", INVALID_TZ);
      
      return (tz < MIN_VALID_TZ || tz > MAX_VALID_TZ) ? new integer() : new integer(tz); 
   }
   
   /**
    * Returns the configured value in the directory for the display timezone 
    * (<code>SESSION:DISPLAY-TIMEZONE</code>).  This value is context-specific to
    * the current user's session and it's used to display datetime-tz values whose format
    * does not include the timezone specifiers.
    * <p>
    * The value returned will have been found via a search algorithm that is account 
    * (user or process) specific or group specific within the current server or a global
    * default for all servers. 
    * <p>
    * The implementation iteratively looks up the directory node under:
    * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/displayTimezone
    * <p>
    * If no user/process or group nodes are present, then this is checked:
    * /server/&lt;serverID&gt;/runtime/default/displayTimezone
    * <p>
    * If no /server/&lt;serverID&gt;/runtime node exists, this is checked (it is the global 
    * default area for all servers): 
    * /server/default/runtime/&lt;account_or_group&gt;/displayTimezone
    * <p>
    * Finally, if no user/process or group nodes are present in the global default area, then 
    * this is checked:
    * /server/default/runtime/default/displayTimezone
    *
    * @return   The display timezone or <code>unknown</code> if not set in the directory.
    */
   private static integer initDisplayTimezone()
   {
      Directory dir = DirectoryManager.getInstance();
      int tz = dir.getInt(Directory.ID_RELATIVE, "displayTimezone", INVALID_TZ);
      return tz == INVALID_TZ ? new integer() : new integer((short)tz); 
   }
   
   /**
    * Returns the system from which time values will be obtained.  This value
    * is context-specific to the current user's session.  In actuality, the
    * application server will always be the source of time values.
    * <p>
    * The value returned may have been found via a search algorithm that
    * is account (user or process) specific or group specific
    * within the current server or a global default for all servers. 
    * <p>
    * The implementation iteratively looks up the directory node under:
    * /server/&lt;serverID&gt;/runtime/&lt;account_or_group&gt;/timeSource
    * <p>
    * If no user/process or group nodes are present, then this is checked:
    * /server/&lt;serverID&gt;/runtime/default/timeSource
    * <p>
    * If no /server/&lt;serverID&gt;/runtime node exists, this is checked
    * (it is the global default area for all servers):
    * /server/default/runtime/&lt;account_or_group&gt;/timeSource
    * <p>
    * Finally, if no user/process or group nodes are present in the global
    * default area, then this is checked:
    * /server/default/runtime/default/timeSource
    * <p>
    * If no value is found via this lookup, then the Progress 4GL default
    * of "local" will be returned.
    *
    * @return   The system from which time values are obtained.
    */
   private static String initTimeSource()
   {
      Directory dir = DirectoryManager.getInstance();
      return dir.getString(Directory.ID_RELATIVE, "timeSource", "LOCAL");
   }

   /**
    * Helper to maintain the component order array.  This method exists to
    * centralize the error checking and handling logic for this simple
    * operation.
    *
    * @param    order
    *           The array which determines the order of date components.
    * @param    idx
    *           The index position to set.
    * @param    type
    *           The component to set at the index.
    * @param    comp
    *           The descriptive character specifying the component.
    */
   private static void editOrder(byte[] order, int idx, byte type, char comp)
   {
      if (idx < 0 || idx >= order.length)
      {
         String spec = "Missing '%c' component in date format.";
         String msg  = String.format(spec, comp);
         throw new RuntimeException(msg);
      }
      else
      {
         order[idx] = type;
      }
   }
   
   /**
    * Generate the error for the condition where the specification of the
    * date component order is not valid.
    */
   private static void genInvalidDateOrder()
   {
      String err = "The DATE argument must be one of: mdy, myd, dmy, dym, ymd, or ydm";
      ErrorManager.recordOrThrowError(299, err);
   }
   
   /**
    * Provides a thread local cache for a default calendar.
    * <p>
    * Allows the caching and reuse of a calendar since construction of
    * these are much more expensive than just reusing the same instance
    * and resetting its state.
    */
   private static class NullCalendar
   extends ThreadLocal<GregorianCalendar>
   {
      /**
       * Instantiate the calendar on first access for the current thread.
       *
       * @return   The calendar.
       */
      @Override
      protected GregorianCalendar initialValue()
      {
         // TODO this looks right but may be wrong:
         // return new GregorianCalendar(getDefaultTimeZone());
         return new GregorianCalendar();
      }
   }
   
   /**
    * Provides a thread local cache for a GMT calendar.
    * <p>
    * Allows the caching and reuse of a calendar since construction of
    * these are much more expensive than just reusing the same instance
    * and resetting its state.
    */
   private static class GMTCalendar
   extends ThreadLocal<GregorianCalendar>
   {
      /**
       * Instantiate the calendar on first access for the current thread.
       *
       * @return   The calendar.
       */
      @Override
      protected GregorianCalendar initialValue()
      {
         return new GregorianCalendar(TimeZone.getTimeZone("GMT"));
      }
   }
   
   /**
    * Provides a thread local cache for a timezone-specific calendar.
    * <p>
    * Allows the caching and reuse of a calendar since construction of
    * these are much more expensive than just reusing the same instance
    * and resetting its state.
    */
   private static class ZoneCalendar
   extends ThreadLocal<GregorianCalendar>
   {
      /** The timezone for which a calendar must be returned. */
      private TimeZone zone = null;
      
      /**
       * Construct a new instance of a timezone-specific calendar cache.
       *
       * @param    zone
       *           The timezone.
       */
      public ZoneCalendar(TimeZone zone)
      {
         this.zone = zone;
      }
      
      /**
       * Instantiate the calendar on first access for the current thread.
       *
       * @return   The calendar.
       */
      @Override
      protected GregorianCalendar initialValue()
      {
         return new GregorianCalendar(zone);
      }
   }
   
   /**
    * Stores global data relating to the state of the current context.
    */
   private static class WorkArea
   {
      /** Track if initialization has occurred yet. */
      private boolean initialized = false;
      
      /** The start of the 100 year range for Y2K windowing. */
      private int windowingYear = -1;

      /** Order of date components for conversion to/from strings. */
      private byte[] dateFormat = null;
      
      /** Default offset in minutes from UTC (SESSION:TIMEZONE attribute). */
      private integer timezoneOffset = null;
      
      /** Timezone to be used when displaying timedate-tz values (DISPLAY-TIMEZONE attribute). */
      private integer displayTimezone = null;
      
      /** System from which to get the time (SESSION:TIME-SOURCE attribute). */
      private String timeSource = "LOCAL";
      
      /**
       * Stores the base time for calculating elapsed milliseconds for this user's session.
       */
      private long baseMillis;
      
      /**
       * During temp-table-prepare() method, the date* literals will use the session date-format instead of
       * DEFAULT_DATE_FORMAT and DEFAULT_DATETIME_FORMAT.
       */
      private String dynamicFormat = null;
      
      /**
       * Initialize this instance.
       */
      private WorkArea()
      {
         // this is always set at the session startup
         baseMillis = System.currentTimeMillis();
      }
   }
   
   /**
    * Simple container that stores and returns a context-local instance of
    * the global work area.
    */
   private static class ContextContainer
   extends ContextLocal<WorkArea>
   {
      /**
       * Obtains the context-local instance of the contained global work
       * area.
       *
       * @return   The work area associated with this context.
       */
      public synchronized WorkArea obtain() 
      {
         WorkArea wa = this.get();
         
         // normally, we would place this init logic in the initialValue()
         // method OR inline in the WorkArea constructor/init (which is also
         // done during initialValue(); however this is can cause a deadlock;
         // the get() MUST be separated from the initialization routines since
         // on the client these routines call the server and can deadlock with
         // other classes which are needed to complete the communications path;
         // once the get() returns, the access to the context-local maps is
         // complete and it is safe to trigger processing that may call the
         // server
         if (!wa.initialized)
         {
            wa.windowingYear   = date.initWindowingYear();
            wa.dateFormat      = date.initFormat(null);
            wa.timezoneOffset  = date.initTimezoneOffset();
            wa.displayTimezone = date.initDisplayTimezone();
            wa.timeSource      = date.initTimeSource();
            wa.initialized     = true;
         }
         
         return wa;
      }
      
      /**
       * Initializes the work area, the first time it is requested within a
       * new context.
       *
       * @return   The newly instantiated work area.
       */
      @Override
      protected synchronized WorkArea initialValue()
      {
         return new WorkArea();
      }   
   }
   
   /**
    * Convenient class that wraps the low-level {@link #julian} parameter
    * to avoid pinning the whole context-local of the date.
    */
   public static class ContextIndependentDate
   {
      /** Stores the date for this instance in Progress 4GL format. */
      long julian;
      
      /** Flag to indicate this value is unknown */
      boolean unknown;
      
      /**
       * Basic c'tor.
       * 
       * @param   unknown
       *          {@code true} if this value represents an unknown value.
       * @param   julian
       *          The date for this instance in Progress 4GL format.
       */
      public ContextIndependentDate(boolean unknown, long julian)
      {
         this.unknown = unknown;
         this.julian = julian;
      }

      /**
       * Implementation of equals.
       */
      @Override
      public boolean equals(Object other)
      {
         if (this == other)
         {
            return true;
         }
         
         if (other instanceof unknown && this.isUnknown())
         {
            // if this is unknown then it is equal to typeless unknown 
            return true;
         }
         if (!(other instanceof ContextIndependentDate) || 
              (other instanceof datetime.ContextIndependentDateTime))
         {
            // a date can be compared ONLY WITH other instances of date
            return false;
         }
         
         ContextIndependentDate that = (ContextIndependentDate) other; 
         boolean u1 = this.isUnknown();
         boolean u2 = that.isUnknown();
         
         if (u1 && u2)
         {
            // two unknown are equals
            return true;
         }
         if (u1 || u2)
         {
            // only of is unknown, the other is not
            return false;
         }
         
         return this.julian == that.julian;
      }
      
      /**
       * Implementation of hash code.
       */
      @Override
      public int hashCode()
      {
         int result = 17;
         if (isUnknown())
         {
            return result;
         }
         
         result = 37 * result + (int) (julian ^ (julian >>> 32));
         
         return result;
      }
      
      /**
       * Check if this is unknown.
       * 
       * @return  {@code true} if this is unknown.
       */
      boolean isUnknown()
      {
         return unknown;
      }
   }
   
   /**
    * Simple testcase wrapper to check Julian day number calculations.
    */
   private static class TestCase 
   {
      /** The date instance to test. */
      private date date; 
      
      /** The resulting formatted output string to match to the testcase. */
      private String result;
      
      /** The month to compare against. */
      private int mm;
      
      /** The day of month to compare against. */
      private int dd;
      
      /** The weekday to compare against. */
      private int ww;
      
      /** The year to compare against. */
      private int yy;
      
      /**
       * The format in which to generate output, <code>null</code> for
       * 'export' format.
       */
      private String fmt;
      
      /**
       * Construct an instance of a test case with all data needed to test
       * the passed instance of <code>date</code>.
       *
       * @param    date
       *           The instance to test.
       * @param    result
       *           The result from the string formatting functions.
       * @param    fmt
       *           The Progress compatible format string to use or
       *           <code>null</code> for export format.
       * @param    mm
       *           The month that the instance should report.
       * @param    dd
       *           The day of month that the instance should report.
       * @param    ww
       *           The weekday that the instance should report.
       * @param    yy
       *           The year that the instance should report.
       */
      private TestCase(date date, String result, String fmt, int mm, int dd, int ww, int yy)
      {
         this.date   = date;
         this.result = result;
         this.fmt    = fmt;
         this.mm     = mm;
         this.dd     = dd;
         this.ww     = ww;
         this.yy     = yy;
      }
      
      /** 
       * Run the test.
       *
       * @return   <code>true</code> if the created date's exported string 
       *           matches the expected result.
       */
      private boolean test()
      {  
         String compare = null;
         
         if (fmt == null)
         {
            compare = date.toStringExport();
         }
         else
         {
            compare = date.toString(fmt);
         }
         
         if ( !( result.equals(compare) &&
                  date.getMonth()   == mm  &&
                  date.getDay()     == dd  &&
                  date.getWeekday() == ww  &&
                  date.getYear()    == yy ))
         {
            System.out.printf("julian %d res '%s' comp '%s' getMonth %d mm %d getDay %d dd %d getWeekDay %d ww %d getYear %d yy %d\n", 
                  (int) date.julian, result, compare, date.getMonth(), mm, date.getDay(), dd, date.getWeekday(), ww, date.getYear(), yy); 
         }
         
         return ( result.equals(compare) &&
                  date.getMonth()   == mm  &&
                  date.getDay()     == dd  &&
                  date.getWeekday() == ww  &&
                  date.getYear()    == yy );
      }
      
      /**
       * Renders this instance as a string.
       *
       * @return   The text representation of this test.
       */
      @Override
      public String toString()
      {
         return "day # = " + date.julian +
                "; string = " + date.toString(fmt) +
                "; result = " + result +
                "; month = " + date.getMonth() +
                "; day = " + date.getDay() +
                "; weekday = " + date.getWeekday() +
                "; year = " + date.getYear() +
                ";";
      }
   }
   
   /**
    * Command line interface which provides a test harness to regression
    * test this class.  No parameters are necessary.
    * <p>
    * If parameters are passed, then an alternate mode is used which simply
    * displays the output of {@link #toString(String)} and
    * {@link #toStringExport}.
    * @param args 
    *        command line args.
    */
   public static void main(String[] args)
   {            
      if (args.length > 1)
      {
         try
         {
            date d = new date(args[0]);
            
            System.out.println("Formatted = '" + d.toString(args[1]) + 
                               "', Export = " + d.toStringExport());
         }
         catch (Exception exc)
         {
            LOG.severe("", exc);
         }
         
         System.exit(0);
      }
      
      // if still true after all tests, everything passed
      boolean flag = true;
      
      // need a calendar with the current time
      GregorianCalendar gc = new GregorianCalendar(); 
      
      // setup comparison values for tests dependent upon the current date
      int mm = gc.get(Calendar.MONTH) + 1;
      int dd = gc.get(Calendar.DAY_OF_MONTH);
      int ww = gc.get(Calendar.DAY_OF_WEEK);
      int yy = gc.get(Calendar.YEAR);
      
      NumberFormat nf = NumberFormat.getInstance(); 
      nf.setGroupingUsed(false);
      nf.setMinimumIntegerDigits(2);
      
      String todayMDY = nf.format(mm) + "/" +
                        nf.format(dd) + "/" +
                        Integer.toString(yy);
      
      String todayYMD = Integer.toString(yy) + "/" +
                        nf.format(mm)        + "/" +
                        nf.format(dd);
                        
      // setup comparison values for tests dependent upon the current year
      gc.set(Calendar.MONTH, 0);    // January == 0
      gc.set(Calendar.DAY_OF_MONTH, 1);
      
      int    year       = gc.get(Calendar.YEAR);
      int    wday       = gc.get(Calendar.DAY_OF_WEEK);
      String yr4        = Integer.toString(year);
      String yr2        = yr4.substring(yr4.length() - 2);
      String dynyearMDY = "01/01/" + yr2;
      String dynyearYMD = yr2 + "/01/01";
      
      // force MDY mode
      work.obtain().dateFormat = ORDER_MDY;
      
      // define our MDY tests
      TestCase[] mdyTests =
      {
         // test of default constructor and the default date constructor
         new TestCase(date.today(),           todayMDY, "99/99/9999", mm, dd, ww, yy),
         new TestCase(new date(date.today()), todayMDY, "99/99/9999", mm, dd, ww, yy),
         
         // test the date constructor with specific dates
         new TestCase(new date(createDate(-4716,  4,  5)), "04/05/-4716", null, 4, 5, 2, -4716),
         new TestCase(new date(createDate(-4714, 12, 30)), "12/30/-4714", null, 12, 30, 7, -4714),
         new TestCase(new date(createDate(-4714, 12, 31)), "12/31/-4714", null, 12, 31, 1, -4714),
         new TestCase(new date(createDate(-4713,  1,  1)), "01/01/-4713", null, 1, 1, 2, -4713),
         new TestCase(new date(createDate( -100, 12, 31)),  "12/31/-100", null, 12, 31, 1, -100),
         new TestCase(new date(createDate(  -99,  1,  1)),  "01/01/-099", null, 1, 1, 2, -99),
         new TestCase(new date(createDate(   -1,  1,  1)),  "01/01/-001", null, 1, 1, 5, -1),
         new TestCase(new date(createDate(   -1, 12, 31)),  "12/31/-001", null, 12, 31, 6, -1),
         new TestCase(new date(createDate(    1,  1,  1)),   "01/01/001", null, 1, 1, 7, 1),
         new TestCase(new date(createDate( 1582, 10,  4)),  "10/04/1582", null, 10, 4, 5, 1582),
         new TestCase(new date(createDate( 1582, 10,  5)),  "10/15/1582", null, 10, 15, 6, 1582),
         new TestCase(new date(createDate( 1582, 10, 15)),  "10/15/1582", null, 10, 15, 6, 1582),
         new TestCase(new date(createDate( 1582, 10, 14)),  "10/24/1582", null, 10, 24, 1, 1582),
         new TestCase(new date(createDate( 1918,  3, 30)),  "03/30/1918", null, 3, 30, 7, 1918),
         new TestCase(new date(createDate( 1918,  3, 31)),  "03/31/1918", null, 3, 31, 1, 1918),
         new TestCase(new date(createDate( 1918,  4,  1)),  "04/01/1918", null, 4, 1, 2, 1918),
         new TestCase(new date(createDate( 1918, 10, 26)),  "10/26/1918", null, 10, 26, 7, 1918),
         new TestCase(new date(createDate( 1918, 10, 27)),  "10/27/1918", null, 10, 27, 1, 1918),
         new TestCase(new date(createDate( 1918, 10, 28)),  "10/28/1918", null, 10, 28, 2, 1918),
         new TestCase(new date(createDate( 1949, 12, 31)),  "12/31/1949", null, 12, 31, 7, 1949),
         new TestCase(new date(createDate( 1950,  1,  1)),    "01/01/50", null, 1, 1, 1, 1950),
         new TestCase(new date(createDate( 1970,  1,  1)),    "01/01/70", null, 1, 1, 5, 1970),
         new TestCase(new date(createDate( 1999, 12, 31)),    "12/31/99", null, 12, 31, 6, 1999),
         new TestCase(new date(createDate( 2000,  1,  1)),    "01/01/00", null, 1, 1, 7, 2000),
         new TestCase(new date(createDate( 2005,  1,  1)),    "01/01/05", null, 1, 1, 7, 2005),
         new TestCase(new date(createDate( 2005,  4, 13)),    "04/13/05", null, 4, 13, 4, 2005),
         new TestCase(new date(createDate( 2049, 12, 31)),    "12/31/49", null, 12, 31, 6, 2049),
         new TestCase(new date(createDate( 2050,  1,  1)),  "01/01/2050", null, 1, 1, 7, 2050),
         
         // tests based on hard-coded day numbers
/*30*/   new TestCase(new date(  -1000), "04/05/-4716", null, 4, 5, 2, -4716),
         new TestCase(new date(     -1), "12/30/-4714", null, 12, 30, 7, -4714),
         new TestCase(new date(      0), "12/31/-4714", null, 12, 31, 1, -4714),
         new TestCase(new date(      1), "01/01/-4713", null, 1, 1, 2, -4713),
         new TestCase(new date(1685264),  "12/31/-100", null, 12, 31, 1, -100),
         new TestCase(new date(1685265),  "01/01/-099", null, 1, 1, 2, -99),
         new TestCase(new date(1721424),  "12/31/-001", null, 12, 31, 6, -1),
         new TestCase(new date(1721425),   "01/01/001", null, 1, 1, 7, 1),
         new TestCase(new date(2299161),  "10/04/1582", null, 10, 4, 5, 1582),
         new TestCase(new date(2299162),  "10/15/1582", null, 10, 15, 6, 1582),
         new TestCase(new date(2421684),  "03/30/1918", null, 3, 30, 7, 1918),
         new TestCase(new date(2421685),  "03/31/1918", null, 3, 31, 1, 1918),
         new TestCase(new date(2421686),  "04/01/1918", null, 4, 1, 2, 1918),
         new TestCase(new date(2421894),  "10/26/1918", null, 10, 26, 7, 1918),
         new TestCase(new date(2421895),  "10/27/1918", null, 10, 27, 1, 1918),
         new TestCase(new date(2421896),  "10/28/1918", null, 10, 28, 2, 1918),
         new TestCase(new date(2433283),  "12/31/1949", null, 12, 31, 7, 1949),
         new TestCase(new date(2433284),    "01/01/50", null, 1, 1, 1, 1950),
         new TestCase(new date(2440589),    "01/01/70", null, 1, 1, 5, 1970),
         new TestCase(new date(2451545),    "12/31/99", null, 12, 31, 6, 1999),
         new TestCase(new date(2451546),    "01/01/00", null, 1, 1, 7, 2000),
         new TestCase(new date(2453475),    "04/13/05", null, 4, 13, 4, 2005),
         new TestCase(new date(2469808),    "12/31/49", null, 12, 31, 6, 2049),
         new TestCase(new date(2469809),  "01/01/2050", null, 1, 1, 7, 2050),
         
         // tests based on string parsing and dynamic year assignments
         new TestCase(new date("01/01"    ), dynyearMDY, "99/99/99", 1, 1, wday, year),
         new TestCase(new date("1.1"      ), dynyearMDY, "99/99/99", 1, 1, wday, year),
         new TestCase(new date("0101"     ), dynyearMDY, "99/99/99", 1, 1, wday, year),
         new TestCase(new date("  01/01. "), dynyearMDY, "99/99/99", 1, 1, wday, year),
         
         // tests based on string parsing
         new TestCase(new date("01-01.05" ),    "01/01/05", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("01-1/5"   ),    "01/01/05", "99/99/99", 1, 1, 7, 2005),
/*60*/   new TestCase(new date("1/1-2005" ),    "01/01/05", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("010105"   ),    "01/01/05", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("01012005" ),    "01/01/05", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date(" 01/01/2005trash"), "01/01/05", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("01/01/05   trash"), "01/01/05", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("01.01.05 ---"), "01/01/05", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("01-01--01"),    "01/01/-1", "99/99/99", 1, 1, 5, -1),
         
         // hard coded (yy, mm, dd) combinations
         new TestCase(new date(1, 1, 2005), "01/01/2005", "99/99/9999", 1, 1, 7, 2005),
         new TestCase(new date(1, 1, 5),   "01-01.0005", "99-99.9999", 1, 1, 5, 5),
         new TestCase(new date(1, 1, 5),    "01.01/0005", "99.99/9999", 1, 1, 5, 5),
         new TestCase(new date(1, 1, 5),    "?????",       "9-9-9", 1, 1, 5, 5), // TODO: fails in FWD
         new TestCase(new date(1, 1, 5),    "00001.00001.0005", "99999.9999.99999", 1, 1, 5, 5), // TODO: fails in FWD
         new TestCase(new date(10, 5, 1582),  "10/15/1582", "99/99/9999", 10, 15, 6, 1582),
         new TestCase(new date(10, 14, 1582), "10/24/1582", "99/99/9999", 10, 24, 1, 1582),
         
         new TestCase(new date(9, 17, -4624), "09/17/-4624", null, 9, 17, 2, -4624),
      };  
      
      // run the MDY tests
      for (int i = 0; i < mdyTests.length; i++)
      {      
         boolean result = false;
         
         try
         {
            result = mdyTests[i].test();
         }
         
         catch (Exception exc)
         {
            LOG.severe("", exc);
         }
         
         if (!result)
         {
            flag = false;
            String msg = "MDY Test " + i + " failed (" + mdyTests[i].toString() + ").";
            System.out.println(msg);
         }
      }
      
      // force YMD mode
      work.obtain().dateFormat = ORDER_YMD;
      
      // define our YMD tests
      TestCase[] ymdTests =
      {
         // test of default constructor and the default date constructor
         new TestCase(date.today(),           todayYMD, "9999/99/99", mm, dd, ww, yy),
         new TestCase(new date(date.today()), todayYMD, "9999/99/99", mm, dd, ww, yy),
         
         // test the date constructor with specific dates
         new TestCase(new date(createDate(-4716,  4,  5)), "-4716/04/05", null, 4, 5, 2, -4716),
         new TestCase(new date(createDate(-4714, 12, 30)), "-4714/12/30", null, 12, 30, 7, -4714),
         new TestCase(new date(createDate(-4714, 12, 31)), "-4714/12/31", null, 12, 31, 1, -4714),
         new TestCase(new date(createDate(-4713,  1,  1)), "-4713/01/01", null, 1, 1, 2, -4713),
         new TestCase(new date(createDate( -100, 12, 31)),  "-100/12/31", null, 12, 31, 1, -100),
         new TestCase(new date(createDate(  -99,  1,  1)),  "-099/01/01", null, 1, 1, 2, -99),
         new TestCase(new date(createDate(   -1,  1,  1)),  "-001/01/01", null, 1, 1, 5, -1),
         new TestCase(new date(createDate(   -1, 12, 31)),  "-001/12/31", null, 12, 31, 6, -1),
         new TestCase(new date(createDate(    1,  1,  1)),   "001/01/01", null, 1, 1, 7, 1),
         new TestCase(new date(createDate( 1582, 10,  4)),  "1582/10/04", null, 10, 4, 5, 1582),
         new TestCase(new date(createDate( 1582, 10,  5)),  "1582/10/15", null, 10, 15, 6, 1582),
         new TestCase(new date(createDate( 1582, 10, 15)),  "1582/10/15", null, 10, 15, 6, 1582),
         new TestCase(new date(createDate( 1582, 10, 14)),  "1582/10/24", null, 10, 24, 1, 1582),
         new TestCase(new date(createDate( 1918,  3, 30)),  "1918/03/30", null, 3, 30, 7, 1918),
         new TestCase(new date(createDate( 1918,  3, 31)),  "1918/03/31", null, 3, 31, 1, 1918),
         new TestCase(new date(createDate( 1918,  4,  1)),  "1918/04/01", null, 4, 1, 2, 1918),
         new TestCase(new date(createDate( 1918, 10, 26)),  "1918/10/26", null, 10, 26, 7, 1918),
         new TestCase(new date(createDate( 1918, 10, 27)),  "1918/10/27", null, 10, 27, 1, 1918),
         new TestCase(new date(createDate( 1918, 10, 28)),  "1918/10/28", null, 10, 28, 2, 1918),
         new TestCase(new date(createDate( 1949, 12, 31)),  "1949/12/31", null, 12, 31, 7, 1949),
         new TestCase(new date(createDate( 1950,  1,  1)),    "50/01/01", null, 1, 1, 1, 1950),
         new TestCase(new date(createDate( 1970,  1,  1)),    "70/01/01", null, 1, 1, 5, 1970),
         new TestCase(new date(createDate( 1999, 12, 31)),    "99/12/31", null, 12, 31, 6, 1999),
         new TestCase(new date(createDate( 2000,  1,  1)),    "00/01/01", null, 1, 1, 7, 2000),
         new TestCase(new date(createDate( 2005,  1,  1)),    "05/01/01", null, 1, 1, 7, 2005),
         new TestCase(new date(createDate( 2005,  4, 13)),    "05/04/13", null, 4, 13, 4, 2005),
         new TestCase(new date(createDate( 2049, 12, 31)),    "49/12/31", null, 12, 31, 6, 2049),
         new TestCase(new date(createDate( 2050,  1,  1)),  "2050/01/01", null, 1, 1, 7, 2050),
         
         // tests based on hard-coded day numbers
         new TestCase(new date(  -1000), "-4716/04/05", null, 4, 5, 2, -4716),
         new TestCase(new date(     -1), "-4714/12/30", null, 12, 30, 7, -4714),
         new TestCase(new date(      0), "-4714/12/31", null, 12, 31, 1, -4714),
         new TestCase(new date(      1), "-4713/01/01", null, 1, 1, 2, -4713),
         new TestCase(new date(1685264),  "-100/12/31", null, 12, 31, 1, -100),
         new TestCase(new date(1685265),  "-099/01/01", null, 1, 1, 2, -99),
         new TestCase(new date(1721424),  "-001/12/31", null, 12, 31, 6, -1),
         new TestCase(new date(1721425),   "001/01/01", null, 1, 1, 7, 1),
         new TestCase(new date(2299161),  "1582/10/04", null, 10, 4, 5, 1582),
         new TestCase(new date(2299162),  "1582/10/15", null, 10, 15, 6, 1582),
         new TestCase(new date(2421684),  "1918/03/30", null, 3, 30, 7, 1918),
         new TestCase(new date(2421685),  "1918/03/31", null, 3, 31, 1, 1918),
         new TestCase(new date(2421686),  "1918/04/01", null, 4, 1, 2, 1918),
         new TestCase(new date(2421894),  "1918/10/26", null, 10, 26, 7, 1918),
         new TestCase(new date(2421895),  "1918/10/27", null, 10, 27, 1, 1918),
         new TestCase(new date(2421896),  "1918/10/28", null, 10, 28, 2, 1918),
         new TestCase(new date(2433283),  "1949/12/31", null, 12, 31, 7, 1949),
         new TestCase(new date(2433284),    "50/01/01", null, 1, 1, 1, 1950),
         new TestCase(new date(2440589),    "70/01/01", null, 1, 1, 5, 1970),
         new TestCase(new date(2451545),    "99/12/31", null, 12, 31, 6, 1999),
         new TestCase(new date(2451546),    "00/01/01", null, 1, 1, 7, 2000),
         new TestCase(new date(2453475),    "05/04/13", null, 4, 13, 4, 2005),
         new TestCase(new date(2469808),    "49/12/31", null, 12, 31, 6, 2049),
         new TestCase(new date(2469809),  "2050/01/01", null, 1, 1, 7, 2050),
         
         // tests based on string parsing and dynamic year assignments
         new TestCase(new date("01/01"    ), dynyearYMD, "99/99/99", 1, 1, wday, year),
         new TestCase(new date("1.1"      ), dynyearYMD, "99/99/99", 1, 1, wday, year),
         new TestCase(new date("0101"     ), dynyearYMD, "99/99/99", 1, 1, wday, year),
         new TestCase(new date("  01/01. "), dynyearYMD, "99/99/99", 1, 1, wday, year),
         
         // tests based on string parsing
         new TestCase(new date("05-01.01" ),    "05/01/01", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("5-01/1"   ),    "05/01/01", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("2005/1-1" ),    "05/01/01", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("050101"   ),    "05/01/01", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("20050101" ),    "05/01/01", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date(" 2005/01/01trash"), "05/01/01", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("05/01/01   trash"), "05/01/01", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("05.01.01 ---"), "05/01/01", "99/99/99", 1, 1, 7, 2005),
         new TestCase(new date("-01-01-01"),    "-1/01/01", "99/99/99", 1, 1, 5, -1),
         
         // hard coded (yy, mm, dd) combinations
         new TestCase(new date(1, 1, 2005), "2005/01/01", "9999/99/99", 1, 1, 7, 2005),
         new TestCase(new date(1, 1, 5),   "0005-01.01", "9999-99.99", 1, 1, 5, 5),
         new TestCase(new date(1, 1, 5),    "0005.01/01", "9999.99/99", 1, 1, 5, 5),
         new TestCase(new date(1, 1, 5),    "?????",       "9-9-9", 1, 1, 5, 5), // TODO: fails in FWD
         new TestCase(new date(1, 1, 5),    "0005.00001.00001", "99999.9999.99999", 1, 1, 5, 5), // TODO: fails in FWD
         new TestCase(new date(10, 5, 1582),  "1582/10/15", "9999/99/99", 10, 15, 6, 1582),
         new TestCase(new date(10, 14, 1582), "1582/10/24", "9999/99/99", 10, 24, 1, 1582),
         
         new TestCase(new date(9, 17, -4624), "-4624/09/17", null, 9, 17, 2, -4624),
      };  
      
      // run the YMD tests
      for (int i = 0; i < ymdTests.length; i++)
      {
         boolean result = false;
         
         try
         {
            result = ymdTests[i].test();
         }
         
         catch (Exception exc)
         {
            LOG.severe("", exc);
         }
         
         if (!result)
         {
            flag = false;
            String msg = "YMD Test " + i + " failed (" + ymdTests[i].toString() + ").";
            System.out.println(msg);
         }
      }
      
      if (flag)
      {
         System.out.println("All tests passed successfully.");
      }
   }
}