# 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.");
          }
       }
    }