# datetime.java

    /*
    ** Module   : datetime.java
    ** Abstract : Progress 4GL compatible datetime object
    **
    ** Copyright (c) 2013-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ------------------------------------Description------------------------------------------
    ** 001 GES 20130128 Initial commit with basic functionality.
    ** 002 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 finished.
    ** 003 OM  20130422 Added int64 support.
    ** 004 OM  20130329 Massive update, implemented missing features.
    **                  Added time components constants for datetime formatter. Added components
    **                  validation function.
    ** 005 MAG 20140602 Fixed the BDT c'tor.
    ** 006 OM  20141016 Fixed equality of unknown datetime/-tz to typeless unknown literal.
    ** 007 OM  20150202 Replaced StringBuffer with StringBuilder.
    ** 008 OM  20150312 Fixed special conversions to/from empty string.
    ** 009 OM  20150623 Fixed conversion from text to datetime in polymorphic cases.
    **                  Added support for custom decimal separator for millis (display, toString).
    ** 010 OM  20150701 Improved support for custom decimal separator for millis (parsing).
    ** 011 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 012 GES 20160622 Fixed parsing "?" values (should lead to unknown value as a result, except
    **                  when there are embedded spaces which should cause an error).
    ** 013 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.
    ** 014 GES 20171207 Removed explicit bypass on pending error (new silent error mode).
    ** 015 OM  20190206 Added implementation of field size needed by record-length.
    ** 016 CA  20190615 Added fromIso() to parse a ISO-8601 date.
    ** 017 CA  20190628 Javadoc fix.
    ** 018 OM  20190626 Granted public access to getIsoDate().
    ** 019 CA  20191206 Fixed assign/deepAssign when the value is null or unknown.
    ** 020 CA  20200519 ISO 8601 format allows single-digit fields in 4GL.
    ** 021 GES 20200806 Reduced ErrorManager context-local usage.
    ** 022 IAS 20201007 Added Type enum
    **     CA  20210304 Fixed date, datetime and datetimetz literal parsing.
    **     CA  20210628 Fixed DATETIME and DATETIME-TZ toStringExport() usage.
    **     CA  20210710 Fixed DATETIME ISO-8601 parsing (in OpenEdge, separators can be DOT, COMMA and whatever
    **                  the number decimal separator is set).
    **     CA  20210823 instantiateFromStringWorker must default to SESSION:DATE-FORMAT if the format is null.
    *      IAS 20210907 Do not throw IllegalArgumentException in <code>toString(String fmt)</code>
    **     AL2 20220319 Added value proxy check in assign.
    **     CA  20220329 'fromIso' method was changed to public.
    **     OM  20220622 Improved date interval operations.
    **     CA  20220730 Added datetime(BDT, BDT) constructor.
    ** 023 CA  20230215 'duplicate()' method returns the real type instead of BDT.
    ** 024 CA  20230322 Cache the pattern used by 'fromLiteral', for ISO 8601 literals.
    ** 025 CA  20230501 'mtime(character)' will raise a 'Unacceptable datatype for MTIME argument.' ERROR.
    ** 026 AB  20230907 Added resetTime(Date d) and modified instantiateFromStringWorker() to set the timezone
    **                  using the date created from Julian's number.
    **     AB  20230907 Removed resetTime(Date). Modified instantiateFromStringWorker().
    ** 027 AI  20231215 Added support for Zulu (Z) time.
    ** 028 CA  20240318 INITIAL value of a datetime(-tz) variable will always have the date 'mdy' format.
    ** 029 CA  20240320 Cache the NumberFormat instance.
    ** 030 CA  20240323 Removed the NumberFormat usage (and the cache) from 'getIsoDate', as this is expensive.
    ** 031 OM  20240509 If no tz was specified in instantiateFromStringWorker(), use the default for the date.
    **         20240510 Added auto detection for date format, depending on the context and a quick heuristic
    **                  analysis of the text.
    ** 032 OM  20240524 Refined algorithm from H031. It also caused regressions.
    ** 033 CA  20241002 Fixed assignment to java types.
    ** 034 ICP 20250129 Used int64.of to leverage caches instances.
    ** 035 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.regex.*;

    import com.goldencode.p2j.util.BaseDataType.*;
    import com.goldencode.p2j.util.date.ContextIndependentDate;

    /**
     * A class that represents a Progress 4GL compatible datetime 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>
     * This class extends the <code>date</code> class so all methods should work
     * directly. If they don't they will be overridden. Also should the following
     * static implementations:
     * <p>
     * <pre>
     * +        operator (datetime, integer)              {@link #plusMillis(datetime, NumberType)}
     * -        operator (datetime, integer)              {@link #minusMillis(datetime, NumberType)}
     * -        operator (datetime, datetime)             {@link #differenceNum(datetime, datetime)}
     * string   function (default format)                 {@link #toString()}
     *                   (user-defined format)            {@link #toString(String)}
     *                   (export format)                  {@link #toStringExport()}
     * now      function                                  {@link #now()}
     * datetime function (date)                           {@link #datetime(date)}
     * datetime function (date, integer)                  {@link #datetime(date, NumberType)}
     * datetime function (integer m, d, y, h, mm, s, ms)  {@link #datetime(NumberType, NumberType, NumberType, NumberType, NumberType, NumberType, NumberType)}
     * datetime function (string parameter)               {@link #datetime(String)}
     *                   [literal]                        {@link #fromLiteral(String)}
     * </pre>
     * <p>
     * Default data type display formats: DATETIME 99/99/9999 HH:MM:SS.SSS
     *
     * @author    OM
     */
    public class datetime
    extends date
    {
       /** Represents the hours component of the string formatted datetime(-tz). */
       public static final byte HOURS = 3;

       /** Represents the minutes component of the string formatted datetime(-tz). */
       public static final byte MINUTES = 4;

       /** Represents the seconds component of the string formatted datetime(-tz). */
       public static final byte SECONDS = 5;

       /** Represents the milliseconds component of the string formatted datetime(-tz). */
       public static final byte MILLIS = 6;

       /** Represents the AM/PM string component of  */
       public static final byte AMPM = 7;

       /** Represents the hours time zone component of the string formatted datetime(-tz). */
       public static final byte TZ_HOURS = 8;

       /** Represents the minutes time zone component of the string formatted datetime(-tz). */
       public static final byte TZ_MINS = 9;

       /** The specific ISO-8601 time format used by 4GL. */
       public static final DateTimeFormatter ISO_LOCAL_TIME =
          new DateTimeFormatterBuilder().appendValue(ChronoField.HOUR_OF_DAY, 1, 2, SignStyle.NEVER)
                                        .appendLiteral(':')
                                        .appendValue(ChronoField.MINUTE_OF_HOUR, 1, 2, SignStyle.NEVER)
                                        .optionalStart()
                                        .appendLiteral(':')
                                        .appendValue(ChronoField.SECOND_OF_MINUTE, 1, 2, SignStyle.NEVER)
                                        .optionalStart()
                                        .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
                                        .toFormatter()
                                        .withResolverStyle(ResolverStyle.LENIENT)
                                        .withChronology(IsoChronology.INSTANCE);

       /** The specific ISO-8601 datetime format used by 4GL. */
       protected static final DateTimeFormatter ISO_LOCAL_DATE_TIME =
          new DateTimeFormatterBuilder().parseCaseInsensitive()
                                        .append(ISO_LOCAL_DATE)
                                        .appendLiteral('T')
                                        .append(ISO_LOCAL_TIME)
                                        .toFormatter()
                                        .withResolverStyle(ResolverStyle.LENIENT)
                                        .withChronology(IsoChronology.INSTANCE);

       /** The default format string. */
       private static final String defaultFormat = "99/99/9999 HH:MM:SS.sss";

       /** The pattern format for ISO 8601 dates. */
       private static final String DT_PATTERN_STRING =
                // mandatory part
                // year has exactly 4 digits left-filled with 0 if needed, negative not allowed
                // month and day are 2 digits, prepended with a 0 if needed
                // hour is in 24H format, also with 2 digits format always
                // minutes it the only field from mandatory part that can use one or two digits
                "(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[tT](\\d\\d):(\\d\\d)" +
                // optional seconds and millisecond part
                // seconds can be represented on one or two digits
                // the fractions of the seconds can be represented on any number of digits, but
                // only first 3 of them are taken into consideration
                "(:(\\d\\d?)(\\.(\\d)+)?)?";

       /** The pattern to parse ISO 8601 formats. */
       private static final Pattern DT_PATTERN = Pattern.compile(DT_PATTERN_STRING);

       /** The milliseconds since midnight. */
       private int time = 0;

       /**
        * Default constructor which creates an unknown instance.
        */
       public datetime()
       {
          super(); // the default constructor of date will set the object to unknown
       }

       /**
        * 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 datetime(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
          {
             if (value instanceof date)
             {
                assign(value);
             }
             else if (value instanceof Text)
             {
                String strVal = ((Text) value).getValue();

                // make a first attempt to parse the string as ISO literal, save as backup
                datetime   isoDt = datetime.fromLiteral(strVal);
                datetime[] tmp   = new datetime[1];

                Runnable code = () -> { tmp[0] = new datetime(strVal); };

                // if ISO parsing was successful, ignore further errors otherwise we don't have the
                // backup value so the ErrorManager will handle the bad syntax of date-format
                try
                {
                   if (isoDt != null)
                   {
                      ErrorManager.ignore(code);
                   }
                   else
                   {
                      code.run();
                   }
                }
                finally
                {
                   if (tmp[0] != null && tmp[0].isUnknown() && isoDt == null)
                   {
                      int[] errCodes = { 5729, 5678 };
                      String[] errMsgs = {
                         "Incompatible datatypes found during runtime conversion",
                         "Unable to do run-time conversion of datatypes"
                      };
                      ErrorManager.recordOrShowError(errCodes, errMsgs, false, false);
                   }
                }

                // at this moment either the  P4GL date-format parsing was successful or it failed but
                // we have a isoDt as backup (that put the ErrorManager into ignore state)
                if (!tmp[0].isUnknown())
                {
                   // P4GL date-format parsing was successful use this value
                   assign(tmp[0]);
                }
                else
                {
                   assign(isoDt);
                }
             }
             else if (value instanceof jobject)
             {
                assign((jobject<?>) value);
             }
             else
             {
                incompatibleTypesOnConversion();
             }
          }
       }

       /**
        * Creates an instance from a <code>java.util.Date</code> instance.
        * <p>
        * <b>WARNING: this will only work if the calendar in which this datetime
        * 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 com.goldencode.p2j.util.datetime#datetime(Date,TimeZone)} version.
        *
        * @param    d
        *           The <code>Date</code> instance from which to pattern the instance.
        */
       public datetime(Date d)
       {
           this(d, TimeZone.getDefault());
       }

       /**
        * Creates an instance from a <code>java.util.Date</code> instance.
        *
        * @param    d
        *           The <code>Date</code> instance from which to pattern the instance.
        * @param    zone
        *           The <code>TimeZone</code> for the calendar to be used for <code>d</code>.
        */
       public datetime(Date d, TimeZone zone)
       {
           long millis = dateToLocalMillis(d, zone);
           long julian = millisToJulianDay(millis);
           long remain = millis - julianDayToMillis(julian);

           instantiateFromDateTime(new date(julian), (int) remain);
       }

       /**
        * 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 DATETIME() built-in function when called with a
        * <code>date</code> subclass.
        *
        * @param    d
        *           The instance from which to copy state.
        */
       public datetime(date d)
       {
          assign(d);
       }

       /**
        * Constructs an instance with the exact same state as the passed
        * instance and having the specified time. This is an implementation of the
        * DATETIME() built-in function.
        * <p>
        * The time value (positive or negative) is added to the date and the
        * resulting value will be used to determine the date and time based on
        * the default timezone.  In other words, the date value can change in
        * either direction by an arbitrary magnitude.  This is how it is done in
        * the 4GL.
        *
        * @param    d
        *           The date from which to copy.
        * @param    time
        *           The milliseconds since midnight.
        */
       public datetime(BaseDataType d, BaseDataType time)
       {
          this(new date(d), new decimal(time));
       }

       /**
        * Constructs an instance with the exact same state as the passed
        * instance and having the specified time. This is an implementation of the
        * DATETIME() built-in function.
        * <p>
        * The time value (positive or negative) is added to the date and the
        * resulting value will be used to determine the date and time based on
        * the default timezone.  In other words, the date value can change in
        * either direction by an arbitrary magnitude.  This is how it is done in
        * the 4GL.
        *
        * @param    d
        *           The date from which to copy.
        * @param    time
        *           The milliseconds since midnight.
        */
       public datetime(date d, NumberType time)
       {
          if (d.isUnknown() || time.isUnknown())
          {
             setUnknown();
          }
          else
          {
             super.deepAssign(d);
             setTime(time.longValue());
          }
       }

       /**
        * Constructs an instance with the exact same state as the passed
        * instance and having the specified time. This is an implementation of the
        * DATETIME() built-in function.
        * <p>
        * The time value (positive or negative) is added to the date and the
        * resulting value will be used to determine the date and time based on
        * the default timezone.  In other words, the date value can change in
        * either direction by an arbitrary magnitude.  This is how it is done in
        * the 4GL.
        *
        * @param    d
        *           The date from which to copy.
        * @param    time
        *           The milliseconds since midnight.
        */
       public datetime(date d, double time)
       {
          this(d, new decimal(time));
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME() built-in function.
        *
        * @param    month
        *           The month number from 1 to 12 inclusive.
        * @param    day
        *           The day of the month from 1 to the maximum days in that month,
        *           inclusive
        * @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 windowed into the
        *           right century if it is 2 digits.  It will be used unchanged if
        *           it is 3 or more digits.
        * @param    hours
        *           The hour of the day from 0 to 23 inclusive.
        * @param    minutes
        *           The minute of the hour from 0 to 59 inclusive.
        * @param    seconds
        *           The seconds of the minute from 0 to 62 inclusive. This accounts
        *           for leap seconds.  Please note that Progress documents 61 as
        *           the limit. It turns out that 62 is the real limit.
        * @param    millis
        *           The milliseconds of the second from 0 to 999 inclusive.
        */
       public datetime(double month,
                       double day,
                       double year,
                       double hours,
                       double minutes,
                       double seconds,
                       double millis)
       {
          // the super class will take care of the date part
          super(month, day, year);

          setTime((int) hours, (int) minutes, (int) seconds, (int) millis);
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME() built-in function.
        *
        * @param    month
        *           The month number from 1 to 12 inclusive.
        * @param    day
        *           The day of the month from 1 to the maximum days in that month,
        *           inclusive
        * @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 windowed into the
        *           right century if it is 2 digits.  It will be used unchanged if
        *           it is 3 or more digits.
        * @param    hours
        *           The hour of the day from 0 to 23 inclusive.
        * @param    minutes
        *           The minute of the hour from 0 to 59 inclusive.
        * @param    seconds
        *           The seconds of the minute from 0 to 62 inclusive. This accounts
        *           for leap seconds.  Please note that Progress documents 61 as
        *           the limit. It turns out that 62 is the real limit.
        */
       public datetime(double month,
                       double day,
                       double year,
                       double hours,
                       double minutes,
                       double seconds)
       {
          // the super class will take care of the date part
          super(month, day, year);

          setTime((int) hours, (int) minutes, (int) seconds, 0);
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME() built-in function.
        *
        * @param    month
        *           The month number from 1 to 12 inclusive.
        * @param    day
        *           The day of the month from 1 to the maximum days in that month,
        *           inclusive
        * @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 windowed into the
        *           right century if it is 2 digits.  It will be used unchanged if
        *           it is 3 or more digits.
        * @param    hours
        *           The hour of the day from 0 to 23 inclusive.
        * @param    minutes
        *           The minute of the hour from 0 to 59 inclusive.
        */
       public datetime(double month,
                       double day,
                       double year,
                       double hours,
                       double minutes)
       {
          // the super class will take care of the date part
          super(month, day, year);

          setTime((int) hours, (int) minutes, 0, 0);
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component of the date and with a time value of 0.
        *
        * @param    month
        *           The month number from 1 to 12 inclusive.
        * @param    day
        *           The day of the month from 1 to the maximum days in that month,
        *           inclusive
        * @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 windowed into the
        *           right century if it is 2 digits.  It will be used unchanged if
        *           it is 3 or more digits.
        */
       public datetime(double month, double day, double year)
       {
          // the super class will take care of the date part
          super(month, day, year);

          setTime(0, 0, 0, 0);
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME() built-in function. Any
        * component that is <code>unknown</code> will result in the instance being
        * <code>unknown</code>.
        *
        * @param    month
        *           The month number from 1 to 12 inclusive.
        * @param    day
        *           The day of the month from 1 to the maximum days in that month,
        *           inclusive
        * @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 windowed into the
        *           right century if it is 2 digits.  It will be used unchanged if
        *           it is 3 or more digits.
        * @param    hours
        *           The hour of the day from 0 to 23 inclusive.
        * @param    minutes
        *           The minute of the hour from 0 to 59 inclusive.
        * @param    seconds
        *           The seconds of the minute from 0 to 62 inclusive. This accounts
        *           for leap seconds.  Please note that Progress documents 61 as
        *           the limit. It turns out that 62 is the real limit.
        * @param    millis
        *           The milliseconds of the second from 0 to 999 inclusive.
        */
       public datetime(NumberType month,
                       NumberType day,
                       NumberType year,
                       NumberType hours,
                       NumberType minutes,
                       NumberType seconds,
                       NumberType millis)
       {
          // the super class will take care of the date part
          super(month, day, year);

          // use the version of setTime that handles unknown properly
          setTime(hours, minutes, seconds, millis);
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME() built-in function. Any
        * component that is <code>unknown</code> will result in the instance being
        * <code>unknown</code>.
        *
        * @param    month
        *           The month number from 1 to 12 inclusive.
        * @param    day
        *           The day of the month from 1 to the maximum days in that month,
        *           inclusive
        * @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 windowed into the
        *           right century if it is 2 digits.  It will be used unchanged if
        *           it is 3 or more digits.
        * @param    hours
        *           The hour of the day from 0 to 23 inclusive.
        * @param    minutes
        *           The minute of the hour from 0 to 59 inclusive.
        * @param    seconds
        *           The seconds of the minute from 0 to 62 inclusive. This accounts
        *           for leap seconds.  Please note that Progress documents 61 as
        *           the limit. It turns out that 62 is the real limit.
        */
       public datetime(NumberType month,
                       NumberType day,
                       NumberType year,
                       NumberType hours,
                       NumberType minutes,
                       NumberType seconds)
       {
          // the super class will take care of the date part
          super(month, day, year);

          // use the version of setTime that handles unknown properly
          setTime(hours, minutes, seconds, int64.of(0L));
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME() built-in function. Any
        * component that is <code>unknown</code> will result in the instance being
        * <code>unknown</code>.
        *
        * @param    month
        *           The month number from 1 to 12 inclusive.
        * @param    day
        *           The day of the month from 1 to the maximum days in that month,
        *           inclusive
        * @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 windowed into the
        *           right century if it is 2 digits.  It will be used unchanged if
        *           it is 3 or more digits.
        * @param    hours
        *           The hour of the day from 0 to 23 inclusive.
        * @param    minutes
        *           The minute of the hour from 0 to 59 inclusive.
        */
       public datetime(NumberType month,
                       NumberType day,
                       NumberType year,
                       NumberType hours,
                       NumberType minutes)
       {
          // the super class will take care of the date part
          super(month, day, year);

          // use the version of setTime that handles unknown properly
          setTime(hours, minutes, int64.of(0L), int64.of(0L));
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component of the date and with a time value of 0. Any <code>unknown</code>
        * input will result in an instance that is <code>unknown</code>.
        *
        * @param    month
        *           The month number from 1 to 12 inclusive.
        * @param    day
        *           The day of the month from 1 to the maximum days in that month,
        *           inclusive
        * @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 windowed into the
        *           right century if it is 2 digits.  It will be used unchanged if
        *           it is 3 or more digits.
        */
       public datetime(NumberType month, NumberType day, NumberType year)
       {
          // the super class will take care of the date part
          super(month, day, year);

          resetTime();
       }

       /**
        * Creates a new instance from the given date and time specification. This
        * is one form of the DATETIME built-in function. This is also the form of
        * the constructor that is used to handle initializer expressions (literals).
        * <p>
        * The specification must match the following format rules:
        * <p>
        * <ol>
        *   <li> Date value (this is not optional).  The string cannot start with
        *        whitespace. The first input is a date value that has at least a
        *        month and day specified or a month, day and year. The values
        *        are parsed in order as specified in SESSION:DATE-FORMAT. Zero
        *        padding is allowed but not required.
        *   <li> Date and time delimiter (not optional). Whitespace or a letter T
        *        (upper or lowercase) which is followed by whitespace. If the letter
        *        T is used, it cannot be preceded by whitespace. No other characters
        *        are allowed.
        *   <li> Time value (optional). If not specified, it is set to 0. It is
        *        specified in HH:MM:SS.sss format, however right side components
        *        can be left off (so HH, HH:MM, HH:MM:SS and HH:MM:SS.sss are all
        *        valid forms). Zero padding is allowed but not required, so hours
        *        can be H or HH, minutes M or MM, seconds S or SS and milliseconds
        *        can be s, ss or sss. Delimiters can only be there if the following
        *        component is included.
        *   <li> Whitespace (optional).
        *   <li> Timezone specification (optional). This is in the form of a sign
        *        (+ or -) then the hours (1 or 2 digits) a colon delimiter and then
        *        minites (1 or 2 digits). Zero padding is allowed but not required.
        *        The minutes can be left off but if so, then the delimiter must
        *        also be left off.  If specified, the instance will be set using
        *        the timezone specified EVEN THOUGH THE TIMEZONE OFFSET IS NOT
        *        STORED (since this is not a datetimetz).
        *   <li> Whitespace (optional).
        * </ol>
        *
        * @param    spec
        *           The date and time specification.
        */
       public datetime(String spec)
       throws ErrorConditionException
       {
          instantiateFromStringWorker(spec, null/*auto*/, -1);
       }

       /**
        * Converts a string to a datetime using a specified format and an optional windowing year for
        * date part. 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     datetime#datetime(String)
        *
        * @param   spec
        *          The date and time specification.
        * @param   format
        *          The format string used for date conversion.
        * @param   windowingYear
        *          The start of the 100 year window used for conversion when year is in 2-digits.
        */
       public datetime(String spec, String format, int windowingYear)
       throws ErrorConditionException
       {
          instantiateFromStringWorker(spec, format, windowingYear);
       }

       /**
        * Creates a new instance from the given date and time specification. This
        * is one form of the DATETIME built-in function. If the input is
        * <code>unknown</code> the instance will be <code>unknown</code>.
        * <p>
        * The specification must match the following format rules:
        * <p>
        * <ol>
        *   <li> Date value (this is not optional).  The string cannot start with
        *        whitespace. The first input is a date value that has at least a
        *        month and day specified or a month, day and year. The values
        *        are parsed in order as specified in SESSION:DATE-FORMAT. Zero
        *        padding is allowed but not required.
        *   <li> Date and time delimiter (not optional). Whitespace or a letter T
        *        (upper or lowercase) which is followed by whitespace. If the letter
        *        T is used, it cannot be preceded by whitespace. No other characters
        *        are allowed.
        *   <li> Time value (optional). If not specified, it is set to 0. It is
        *        specified in HH:MM:SS.sss format, however right side components
        *        can be left off (so HH, HH:MM, HH:MM:SS and HH:MM:SS.sss are all
        *        valid forms). Zero padding is allowed but not required, so hours
        *        can be H or HH, minutes M or MM, seconds S or SS and milliseconds
        *        can be s, ss or sss. Delimiters can only be there if the following
        *        component is included.
        *   <li> Whitespace (optional).
        *   <li> Timezone specification (optional). This is in the form of a sign
        *        (+ or -) then the hours (1 or 2 digits) a colon delimiter and then
        *        minutes (1 or 2 digits). Zero padding is allowed but not required.
        *        The minutes can be left off but if so, then the delimiter must
        *        also be left off.  If specified, the instance will be set using
        *        the timezone specified EVEN THOUGH THE TIMEZONE OFFSET IS NOT
        *        STORED (since this is not a datetime-tz).
        *   <li> Whitespace (optional).
        * </ol>
        *
        * @param    spec
        *           The date and time specification.
        */
       public datetime(character spec)
       throws ErrorConditionException
       {
          if (spec.val().isUnknown())
          {
             setUnknown();
          }
          else
          {
             instantiateFromStringWorker(spec.val().toStringMessage(), null/*auto*/, -1);
          }
       }

       /**
        * Parses this ISO 8601 value as a datetime.
        * <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)
       {
          datetime d = new datetime();
          d.instantiateFromStringWorker(val, DEFAULT_DATETIME_FORMAT, -1);
          return d;
       }

       /**
        * Creates an instance that represents current date and time using the default timezone.
        * This is the equivalent of the NOW built-in function.
        */
       public static datetime now()
       {
          TimeZone sessionTz = date.getDefaultTimeZone();
          GregorianCalendar now = getZoneCalendar(sessionTz);

          return new datetime(date.today(), datetime.millisSinceMidnight(now));
       }

       /**
        * Parse the INITIAL string of a datetime var definition.
        *
        * @param    literal
        *           The datetime literal, which will always have the date part in 'mdy' format.
        *
        * @return   The datetime instance initialized with this literal.
        */
       public static datetime init(String literal)
       {
          return datetime.parseInitial(literal);
       }

       /**
        * Check if the <code>str</code> is a Progress datetime-tz literal (in ISO8601 format string
        * representation), parse it and return the <code>datetimetz</code> object.
        * The recognized formats are:
        * <pre>
        *    YYYY-MM-ddTHH:mm[:S[S].s[s[s[...]]]]
        * </pre>
        * where the date-separator can be either 't' or 'T'. The hours and minutes are mandatory in
        * 2 digits and seconds can be represented as 1 or 2 digits. The seconds parts can be tenths
        * of seconds (one digit), hundredths (2 digits) and milliseconds (3 digits after '.').
        * If more digits appear after '.' they are ignored.
        *
        * Normally the timezone should be present, otherwise P2J parser wouldn't classified it as
        * a datetime-tz literal.
        *
        * @param   str
        *          The text to be parsed.
        *
        * @return  a datetime-tz object whose string literal representation was given as argument on
        *          success or null if the passed argument does not fit the known pattern.
        */
       public static datetime fromLiteral(String str)
       {
          Matcher matcher = DT_PATTERN.matcher(str);
          if (!matcher.matches())
          {
             // failed, spec is not in ISO8601 format
             return null;
          }

          int year = 0;
          int month = 0;
          int day = 0;
          int hour = 0;
          int minutes = 0;
          int seconds = 0;
          int millis = 0;

          // parse captured tokens
          try
          {
             // mandatory parts:
             year = Integer.parseInt(matcher.group(1));
             month = Integer.parseInt(matcher.group(2));
             day = Integer.parseInt(matcher.group(3));
             hour = Integer.parseInt(matcher.group(4));
             minutes = Integer.parseInt(matcher.group(5));

             // optional components:
             if (matcher.group(7) != null)
             {
                seconds = Integer.parseInt(matcher.group(7));
             }
             if (matcher.group(8) != null)
             {
                String milliDigits = matcher.group(8).substring(1); // skip the .;
                if (milliDigits.length() > 3)
                {
                   // more than 3 digits are allowed for milliseconds,
                   // but only the first 3 digits are used, the other are dropped
                   milliDigits = milliDigits.substring(0, 3);
                }
                millis = Integer.parseInt(milliDigits);
                if (milliDigits.length() == 1)
                {
                   // if only one digit is used for millis they are tenths instead
                   millis *= 100;
                }
                else if (milliDigits.length() == 2)
                {
                   // if only two digits are used for millis they are hundredths instead
                   millis *= 10;
                }
             }
          }
          catch (NumberFormatException e)
          {
             // this should not happen, anyway since groups should be made of digits only
             // if cannot parse than it's not in the correct format
             return null;
          }

          datetime ret = new datetime();
          long julian = ret.instantiateDate(year, month, day);
          if (julian == INVALID_DATE)
          {
             return null;
          }
          ret.setDayNumber(julian);
          ret.setTime(hour, minutes, seconds, millis);

          // set parsed groups to internal data of the object
          return ret;
       }

       /**
        * Get the type
        * @return type
        */
       public Type getType()
       {
          return Type.DATETIME;
       }

       /**
        * Returns the number of milliseconds difference between the datetimes 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 datetimes or
        *           <code>unknown value</code> if any input is <code>unknown value</code>.
        */
       public static int64 differenceNum(datetime op1, datetime op2)
       {
          if (op1.isUnknown() || op2.isUnknown())
          {
             return new int64();
          }

          return new int64(op1.getAbsoluteTimeOffset() - op2.getAbsoluteTimeOffset());
       }

       /**
        * Obtain the millis since midnight for this instance. This is an
        * implementation of the <code>TIME()</code> built-in function.
        *
        * @return   The milliseconds since midnight.
        */
       public int64 getTimeNum()
       {
          return isUnknown() ? new int64() : new int64(time);
       }

       /**
        * Obtain the millis from midnight.
        *
        * @return  milliseconds passed from midnight.
        */
       public int getTime()
       {
          return time;
       }

       /**
        * Sets the millis from midnight.
        *
        * @param   millis
        *          Milliseconds passed from midnight.
        */
       public void setTime(NumberType millis)
       {
          if (millis.isUnknown())
          {
             setUnknown();
          }
          else
          {
             setTime(millis.longValue());
          }
       }

       /**
        * Sets the milliseconds since midnight. If millis is negative a previous day is computed.
        * If millis &gt; date.MILLIS_PER_DAY, the extra milliseconds are combined in days in the future.
        *
        * @param    millis
        *           Milliseconds since midnight.
        */
       public void setTime(long millis)
       {
          // Progress warps the amount of time in 32bit:
          int newMillis = (int) millis;
          long days = longValue();

          if (newMillis < 0)
          {
             // must go back in time!
             long daysOff = newMillis / date.MILLIS_PER_DAY;
             newMillis   %= date.MILLIS_PER_DAY;
             if (newMillis != 0)
             {
                newMillis += date.MILLIS_PER_DAY;
                daysOff--;
             }

             days += daysOff;
          }
          else if (newMillis >= date.MILLIS_PER_DAY)
          {
             // go forward in time
             days += newMillis / date.MILLIS_PER_DAY;
             newMillis %= date.MILLIS_PER_DAY;
          }
          else
          {
             if (time != newMillis)
             {
                checkUndoable(true);
             }

             // this is in fact the basic case when the day does not need adjustments
             this.time = newMillis;
             return;
          }

          if (time != newMillis)
          {
             checkUndoable(true);
          }

          // put-back the obtained values
          setDayNumber(days);
          this.time = newMillis;
       }

       /**
        * Extract the hours time-element from this datetime.
        *
        * @return   The hour of this datetime object.
        */
       public int getHours()
       {
          return time / (1000 * 60 * 60);
       }

       /**
        * Extract the hours time-element from this datetime.
        *
        * @return  the hour of this datetime object or unknown value if this
        *          is an unknown object.
        */
       public int64 getHoursNum()
       {
          return isUnknown() ? new int64() : new int64(getHours());
       }

       /**
        * Extract the minutes time-element from this datetime.
        *
        * @return the minute of this datetime object
        */
       public int getMinutes()
       {
          int timeInMinutes = time / (1000 * 60);
          return timeInMinutes % 60;
       }

       /**
        * Extract the minutes time-element from this datetime.
        *
        * @return  the minute of this datetime object or unknown value if this
        *          is an unknown object.
        */
       public int64 getMinutesNum()
       {
          return isUnknown() ? new int64() : new int64(getMinutes());
       }

       /**
        * Extract the seconds time-element from this datetime.
        *
        * @return the second of this datetime object
        */
       public int getSeconds()
       {
          int timeInSeconds = time / 1000;
          return timeInSeconds % 60;
       }

       /**
        * Extract the seconds time-element from this datetime.
        *
        * @return  the second of this datetime object or unknown value if this
        *          is an unknown object.
        */
       public int64 getSecondsNum()
       {
          return isUnknown() ? new int64() : new int64(getSeconds());
       }

       /**
        * Extract the milliseconds time-element from this datetime.
        *
        * @return the millisecond of this datetime object
        */
       public int getMilliseconds()
       {
          return time % 1000;
       }

       /**
        * Extract the milliseconds time-element from this datetime.
        *
        * @return  the millisecond of this datetime object or unknown value if
        *          this is an unknown object.
        */
       public int64 getMillisecondsNum()
       {
          return isUnknown() ? new int64() : new int64(getMilliseconds());
       }

       /**
        * Obtains the minutes of timezone offset. This is only used for displaying when forced
        * by using a format that contains timezone specifiers.
        *
        * @return  The minutes of timezone of the session.
        */
       public int getTzMinutes()
       {
          int defaultOffset = datetimetz.getDefaultTimezoneOffset(this) / (1000 * 60);
          return Math.abs(defaultOffset % 60); //always positive
       }

       /**
        * Obtains the hours of timezone offset. This is only used for displaying when forced
        * by using a format that contains timezone specifiers.
        *
        * @return  The hours of timezone of the session.
        */
       public int getTzHours()
       {
          int defaultOffset = datetimetz.getDefaultTimezoneOffset(this) / (1000 * 60);
          return defaultOffset / 60;
       }

       /**
        * Obtains the minutes of timezone offset.
        *
        * @return  The minutes of timezone offset or unknown value if this
        *          datetime-tz is unknown.
        */
       public int64 getTzMinutesNum()
       {
          return isUnknown() ? new int64() : new int64(getTzMinutes());
       }

       /**
        * Obtains the hours of timezone offset.
        *
        * @return  The hours of timezone offset or unknown value if this
        *          datetime-tz is unknown.
        */
       public int64 getTzHoursNum()
       {
          return isUnknown() ? new int64() : new int64(getTzHours());
       }

       /**
        * Sets the internal time. This method will validate the parameters and
        * raise an error if they are incorrect.Any input that is
        * <code>unknown</code> will cause this instance to be <code>unknown</code>.
        *
        * @param    hours
        *           The hour of the day from 0 to 23 inclusive.
        * @param    mins
        *           The minute of the hour from 0 to 59 inclusive.
        * @param    secs
        *           The seconds of the minute from 0 to 62 inclusive. This accounts
        *           for leap seconds.  Please note that Progress documents 61 as
        *           the limit. It turns out that 62 is the real limit.
        * @param    millis
        *           The milliseconds of the second from 0 to 999 inclusive.
        */
       public void setTime(NumberType hours, NumberType mins, NumberType secs, NumberType millis)
       {
          if (hours.isUnknown() || mins.isUnknown()  || secs.isUnknown()  || millis.isUnknown())
          {
             setUnknown();
          }
          else
          {
             setTime(hours.longValue(), mins.longValue(), secs.longValue(), millis.longValue());
          }
       }

       /**
        * Sets the internal time. This method will validate the parameters and
        * raise an error if they are incorrect.
        *
        * @param    hours
        *           The hour of the day from 0 to 23 inclusive.
        * @param    mins
        *           The minute of the hour from 0 to 59 inclusive.
        * @param    secs
        *           The seconds of the minute from 0 to 62 inclusive. This accounts
        *           for leap seconds.  Please note that Progress documents 61 as
        *           the limit. It turns out that 62 is the real limit.
        * @param    millis
        *           The milliseconds of the second from 0 to 999 inclusive.
        */
       public void setTime(long hours, long mins, long secs, long millis)
       {
          if (hours < 0 || hours > 23)
          {
             ErrorManager.recordOrThrowError(11260, "Invalid hours in datetime value");
             return;
          }

          if (mins < 0 || mins > 59)
          {
             ErrorManager.recordOrThrowError(11256, "Invalid minutes in datetime value");
             return;
          }

          if (secs < 0 || secs > 62)
          {
             ErrorManager.recordOrThrowError(11253, "Invalid seconds in datetime value");
             return;
          }

          if (millis < 0 || millis > 999)
          {
             ErrorManager.recordOrThrowError(11252, "Invalid milliseconds in datetime value");
             return;
          }

          int mtime = (int) (millis + 1000 * (secs + 60 * (mins + 60 * hours)));

          if (time != mtime)
          {
             checkUndoable(true);
          }

          time = mtime;
       }

       /**
        * TODO: perhaps this should be moved to DateOps
        * Adds <code>milliseconds</code> to the datetime of the left operand and
        * returns a new instance representing that datetime.
        *
        * @param   dt
        *          The left operand.
        * @param   millis
        *          The right operand (the number of milliseconds to add.
        *          Note that this can be a negative number to effectively
        *          subtract instead of add.
        *
        * @return  The date which is <code>millis</code> different from the
        *          left operand or <code>unknown value</code> if <code>dt</code>
        *          is <code>unknown value</code>.
        */
       @SuppressWarnings("unchecked")
       public static <T extends datetime> T plusMillis(T dt, long millis)
       {
          if (dt.isUnknown())
          {
             return (T) dt.instantiateUnknown();
          }

          // Apparently in java 1.7, private field dt.time is not accessible from static context.
          // The cause is dt is of type T not datetime, so it does make sense a little
          long newMillis = dt.getTime() + millis;
          long days = dt.longValue();

          if (newMillis < 0)
          {
             // must go back in time!
             long daysOff = newMillis / date.MILLIS_PER_DAY;
             newMillis    = newMillis % date.MILLIS_PER_DAY;

             if (newMillis != 0)
             {
                newMillis += date.MILLIS_PER_DAY;
                daysOff--;
             }

             days += daysOff;
          }
          else if (newMillis >= date.MILLIS_PER_DAY)
          {
             // go forward in time
             days += newMillis / date.MILLIS_PER_DAY;
             newMillis = newMillis % date.MILLIS_PER_DAY;
          }

          T ret = (T) dt.duplicate();
          ret.setDayNumber(days);
          ret.setTime(newMillis);

          return ret;
       }

       /**
        * TODO: perhaps this should be moved to DateOps
        * Adds <code>milliseconds</code> to the datetime of the left operand and
        * returns a new instance representing that datetime.
        *
        * @param   dt
        *          The left operand.
        * @param   millis
        *          The right operand (the number of milliseconds to add.
        *          Note that this can be a negative number to effectively
        *          subtract instead of add.
        *
        * @return  The date which is <code>millis</code> different from the
        *          left operand or <code>unknown value</code> if any input is
        *          <code>unknown value</code>.
        */
       @SuppressWarnings("unchecked")
       public static <T extends datetime> T plusMillis(T dt, NumberType millis)
       {
          return (dt.isUnknown() || millis.isUnknown()) ?
                 (T) dt.instantiateUnknown() :
                 plusMillis(dt, millis.longValue());
       }

       /**
        * TODO: perhaps this should be moved to DateOps
        * Subtracts <code>milliseconds</code> to the datetime of the left operand
        * and returns a new instance representing that datetime.
        *
        * @param   dt
        *          The left operand.
        * @param   millis
        *          The right operand (the number of milliseconds to subtract.
        *          Note that this can be a negative number to effectively
        *          add instead of subtract.
        *
        * @return  The date which is <code>millis</code> different from the
        *          left operand or <code>unknown value</code> if <code>dt</code>
        *          is <code>unknown value</code>.
        */
       @SuppressWarnings("unchecked")
       public static <T extends datetime> T minusMillis(T dt, long millis)
       {
          return dt.isUnknown() ? (T) dt.instantiateUnknown() : plusMillis(dt, -millis);
       }

       /**
        * TODO: perhaps this should be moved to DateOps
        * Subtracts <code>milliseconds</code> to the datetime of the left operand
        * and returns a new instance representing that datetime.
        *
        * @param   dt
        *          The left operand.
        * @param   millis
        *          The right operand (the number of milliseconds to subtract.
        *          Note that this can be a negative number to effectively
        *          add instead of subtract.
        *
        * @return  The date which is <code>millis</code> different from the
        *          left operand or <code>unknown value</code> if any input is
        *          <code>unknown value</code>.
        */
       @SuppressWarnings("unchecked")
       public static <T extends datetime> T minusMillis(T dt, NumberType millis)
       {
          return (dt.isUnknown() || millis.isUnknown()) ?
                 (T) dt.instantiateUnknown() :
                 plusMillis(dt, -millis.longValue());
       }

       /**
        * Returns the time in milliseconds (MTIME, similar to TIME, which
        * returns seconds since midnight).
        *
        * @return  the time in milliseconds.
        */
       public static integer millisecondsSinceMidnight()
       {
          return new integer(datetime.now().time);
       }

       /**
        * Returns the time in milliseconds (MTIME, similar to TIME, which
        * returns seconds since midnight).
        *
        * @param   dt
        *          the datetime or datetime-tz to evaluate.
        *
        * @return  the time in milliseconds.
        */
       public static integer millisecondsSinceMidnight(datetime dt)
       {
          return dt.isUnknown() ? new integer() : new integer(dt.time);
       }

       /**
        * Returns the time in milliseconds (MTIME, similar to TIME, which
        * returns seconds since midnight).
        *
        * @param   dt
        *          the datetime or datetime-tz to evaluate.
        *
        * @return  the time in milliseconds.
        */
       public static integer millisecondsSinceMidnight(character dt)
       {
          if (dt.isUnknown())
          {
             return new integer();
          }

          ErrorManager.recordOrThrowError(12117, "Unacceptable datatype for MTIME argument.", false);

          return new integer();
       }

       /**
        * Creates a new <code>datetime</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 datetime instantiateUnknownDatetime()
       {
          return new datetime();
       }

       /**
        * 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>.
        */
       public BaseDataType instantiateUnknown()
       {
          return new datetime();
       }

       /**
        * Creates a new instance of the same type that represents the default initialized value.
        *
        * @return   An instance that represents the default value.
        */
       public BaseDataType instantiateDefault()
       {
          return new datetime(); // unknown value
       }

       /**
        * Return the default display format string for this type.
        *
        * @return   The default format string.
        */
       public String defaultFormatString()
       {
          return defaultFormat;
       }

       /**
        * 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;
          }

          if (super.intValue() == RAW_EPOCH)
          {
             // during the RAW_EPOCH, the time is the only serialized data, luckily, this is relative
             // to midnight and its size follows integer size
             return 1 + new integer(time).getSize();
          }

          // for all other dates, the time has a 4 bytes reserved and the space needed by the day is
          // given by the super class
          return super.getSize() + 5;
       }

       /**
        * 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>datetime</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, 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 conversion is meant to handle the cases of built-in functions and methods
        * in the 4GL which have polymorphic return types (e.g. DYNAMIC-FUNCTION()).
        *
        * @param    value
        *           The instance from which to copy state.
        */
       public void assign(BaseDataType value)
       {
          if (value == null)
          {
             super.assign(value);
             return;
          }

          // the value may be a proxy; make sure to unwrap and work with the real value
          value = value.val();
          if (value.isUnknown() || value instanceof date || value instanceof character)
          {
             super.assign(value);
          }
          else if (value instanceof jobject)
          {
             assign((jobject<?>) value);
          }
          else
          {
             setUnknown();

             incompatibleTypesOnConversion();
          }
       }

       /**
        * Sets the state of this instance based on the state of the passed instance.
        * If time information is not available (<code>value</code> is a <code>date</code>) 00:00 is
        * assumed. Extra information available in <code>datetimetz</code> (time-zone offset) is
        * ignored.
        *
        * @param    value
        *           The instance from which to copy state.
        */
       @Override
       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;
          }

          if (value instanceof datetimetz)
          {
             // convert value to local time before assigning
             deepAssign(value.toLocalDatetime());
          }
          else if (value instanceof datetime)
          {
             deepAssign((datetime) value);
          }
          else
          {
             checkUndoable(value);

             super.deepAssign(value);
             resetTime();
          }
       }

       /**
        * 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(datetime value)
       {
          if (value == null || value.isUnknown())
          {
             setUnknown();
             return;
          }

          checkUndoable(value);

          super.deepAssign(value);
          time = value.time;
       }

       /**
        * 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>.
        */
       public int compareTo(Object obj)
       {
          if (!(obj instanceof date) || obj instanceof datetimetz)
          {
             // datetime-tz values are NOT comparable with datetime.
             // in special conditions date values are compatible, see below
             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" directly;
          // However, if the compare operator receives different kind of date-related only the
          // "common parts" are compared - ie. ignore additional fields from derived classes.
          date thatDate = (date) obj;

          if (!(thatDate instanceof datetime))
          {
             // must be a date, not datetime let it handle the comparison
             // and return the mirrored result
             return -thatDate.compareTo(this);
          }

          datetime that = (datetime) thatDate;

          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;
          }

          // compare the actual days of both values
          long thisDays = this.longValue();
          long thatDays = that.longValue();
          if (thisDays > thatDays)
          {
             return 1;
          }
          else if (thisDays < thatDays)
          {
             return -1;
          }

          // in case of equality, compare the time
          if (this.time < that.time)
          {
             return -1;
          }
          else if (this.time > that.time)
          {
             return 1;
          }

          // otherwise they are equals
          return 0;
       }

       /**
        * 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)
        */
       public boolean equals(Object o)
       {
          if (this == o)
          {
             // common case
             return true;
          }
          if (o instanceof unknown && this.isUnknown())
          {
             // if this is unknown then it is equal to typeless unknown
             return true;
          }
          if (!(o instanceof datetime) || o instanceof datetimetz)
          {
             // a datetime can be compared ONLY WITH other instances of datetime
             return false;
          }

          datetime that = (datetime) 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.longValue() == that.longValue()) && (this.time == that.time);
       }

       /**
        * Hash code implementation which is consistent with {@link
        * BaseDataType#equals}.
        *
        * @return  Hash code value for this object instance.
        */
       public int hashCode()
       {
          int result = 17;
          if (isUnknown())
          {
             return result;
          }

          result = 37 * result + (int) (longValue() ^ (longValue() >>> 32));
          result = 37 * result + time;

          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.
        */
       public datetime duplicate()
       {
          return new datetime(this);
       }

       /**
        * 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(longValue()) + time, override);
       }

       /**
        * Creates a SQL compatible string representation of the instance data
        * (the format is "YYYY-MM-DD HH:MM:SS.zzz").
        * <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.
        */
       @Override
       public String toStringSQL()
       {
          NumberFormat nf = NumberFormat.getInstance();
          nf.setMinimumIntegerDigits(2);
          nf.setGroupingUsed(false);

          StringBuilder sb = new StringBuilder();

          sb.append(super.toStringSQL()); // let the super class do its job

          sb.append(' ');   // this is the date-time separator

          sb.append(nf.format(getHours()));
          sb.append(':');
          sb.append(nf.format(getMinutes()));
          sb.append(':');
          sb.append(nf.format(getSeconds()));
          sb.append('.');
          nf.setMinimumIntegerDigits(3);
          sb.append(nf.format(getMilliseconds()));

          return sb.toString();
       }

       /**
        * 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.
        * The time part uses the standard format of 2 digits per components separated by ":", except
        * for milliseconds which have three digits and are separated by ".".
        * <p>
        * This implementation creates the given string based on the order of
        * date components (e.g. MDY, YMD...) defined for this context.
        *
        * @return   The 'message' formatted string.
        */
       @Override
       public String toStringMessage()
       {
          if (isUnknown())
          {
             return "?";
          }

          byte[] dateOrder = date.getDateComponentOrder();
          NumberFormat nf = NumberFormat.getInstance();
          nf.setGroupingUsed(false);
          nf.setMinimumIntegerDigits(2);

          StringBuilder sb = new StringBuilder();

          for (int i = 0; i < 3; i++)
          {
             if (dateOrder[i] == MONTH)
             {
                sb.append(nf.format(getMonth()));
             }
             else if (dateOrder[i] == DAY)
             {
                sb.append(nf.format(getDay()));
             }
             else
             {
                sb.append(toYearString(3, 6, false));
             }

             // add separators between the 1st/2nd and 2nd/3rd components
             if (i < 2)
             {
                sb.append('/');
             }
          }

          sb.append(' '); // date-time separator

          sb.append(nf.format(getHours()));
          sb.append(':');
          sb.append(nf.format(getMinutes()));
          sb.append(':');
          sb.append(nf.format(getSeconds()));
          sb.append(NumberType.getDecimalSeparator()); // default is '.';);
          nf.setMinimumIntegerDigits(3);
          sb.append(nf.format(getMilliseconds()));

          return sb.toString();
       }

       /**
        * 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-DDTHH:MM:SS.SSS+HH:MM") or "?" if <code>unknown</code>.
        */
       @Override
       public String toStringExport()
       {
          if (isUnknown())
          {
             return "?";
          }

          return getIsoDate();
       }

       /**
        * Create a string representation of this date using a Progress 4GL style
        * date format string such as 99/99/99 HH:MM:SS.SSS or 99-99-9999 HH:MM:SS.SSS.
        * The '/', '-', '.' and ':' are valid separators.  All 7 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 obtained using <code>defaultFormatString()</code>.
        *
        * @return   The formatted string or the 'failure' string if the date is
        *           too large to fit in the specified format.
        *
        */
       @Override
       public String toString(String fmt)
       {
          String saveFmt = fmt;
          // set our default format as needed
          if (fmt == null || fmt.trim().length() == 0)
          {
             fmt = defaultFormatString();
          }

          if (isUnknown())
          {
             char[] blanks = new char[fmt.length()];
             Arrays.fill(blanks, ' ');
             return new String(blanks);
          }

          char timedateSep = ' ';
          if (fmt.contains("t"))
          {
             fmt = fmt.replace('t', ' ');
             timedateSep = 't';
          }
          else if (fmt.contains("T"))
          {
             fmt = fmt.replace('T', ' ');
             timedateSep = 'T';
          }

          int k = fmt.indexOf(' '); // look for date / time separator

          if (k == -1)
          {
             // if not found assume format contain only date part
             return super.toString(fmt);
          }

          StringBuilder sb = new StringBuilder();

          NumberFormat nf = NumberFormat.getInstance();
          nf.setGroupingUsed(false);
          nf.setMinimumIntegerDigits(2);

          String datePartFormat = fmt.substring(0, k);    // first k digits
          String timePartFormat = fmt.substring(k + 1).toUpperCase();   // everything after digit k

          if (datePartFormat.isEmpty() || timePartFormat.isEmpty())
          {
             throw new ErrorConditionException(-1, "Invalid datetime format: " + saveFmt);
    //         ErrorManager.recordOrThrowError(-1, "Invalid datetime format: " + saveFmt);
          }
          String datePart = super.toString(datePartFormat); // let date class format its part
          if (datePart.startsWith("??"))
          {
             // date failed to be represented on designed format; fail all:
             return fmt.replaceAll(".", "?");
          }
          sb.append(datePart);
          sb.append(timedateSep);

          boolean uppercaseAM = timePartFormat.contains("A");
          boolean lowercaseAM = timePartFormat.contains("a");
          timePartFormat = timePartFormat.toUpperCase();

          if (timePartFormat.startsWith("HH"))
          {
             // hours
             int hours = getHours();
             // if am/pm is specified adjust hours in 0..12 interval
             if (uppercaseAM || lowercaseAM)
             {
                if (hours >= 12)
                {
                   hours -= 12;
                }
                if (hours == 0)
                {
                   hours = 12;
                }
             }
             sb.append(nf.format(hours));
             timePartFormat = timePartFormat.substring(2);

             if (timePartFormat.startsWith(":MM"))
             {
                // minutes
                sb.append(':').append(nf.format(getMinutes()));
                timePartFormat = timePartFormat.substring(3);

                if (timePartFormat.startsWith(":SS"))
                {
                   // seconds
                   sb.append(':').append(nf.format(getSeconds()));
                   timePartFormat = timePartFormat.substring(3);

                   char decimalSep = NumberType.getDecimalSeparator(); // default is '.';
                   if (timePartFormat.startsWith(".SSS"))
                   {
                      // milli - seconds
                      nf.setMinimumIntegerDigits(3);
                      sb.append(decimalSep).append(nf.format(getMilliseconds()));
                      timePartFormat = timePartFormat.substring(4);
                      nf.setMinimumIntegerDigits(2);
                   }
                   else if (timePartFormat.startsWith(".SS"))
                   {
                      // hundreds of seconds
                      int millis = getMilliseconds();
                      sb.append(decimalSep).append(nf.format(millis / 10));
                      timePartFormat = timePartFormat.substring(3);
                   }
                   else if (timePartFormat.startsWith(".S"))
                   {
                      // tenths of seconds
                      int millis = getMilliseconds();
                      nf.setMinimumIntegerDigits(1);
                      sb.append(decimalSep).append(nf.format(millis / 100));
                      timePartFormat = timePartFormat.substring(2);
                      nf.setMinimumIntegerDigits(2);
                   }
                }
             }
          }

          // am/pm and timezone seem to behave exclusively, however, AM is always before TZ
          String ampm = null;
          if (timePartFormat.startsWith(" AP"))
          {
             // two letter am/pm
             ampm = getHours() < 12 ? " AM" : " PM";
             timePartFormat = timePartFormat.substring(3);
          }
          else if (timePartFormat.startsWith(" A"))
          {
             // one letter am/pm
             ampm = getHours() < 12 ? " A" : " P";
             timePartFormat = timePartFormat.substring(2);
          }
          if (ampm != null)
          {

             if (lowercaseAM)
             {
                ampm = ampm.toLowerCase();
             }
             sb.append(ampm);
          }

          // check the timezone
          if (timePartFormat.startsWith("+HH") || timePartFormat.startsWith("-HH"))
          {
             // tz hours
             sb.append(getTzHours() < 0 ? '-' : '+');
             sb.append(nf.format(Math.abs(getTzHours())));
             timePartFormat = timePartFormat.substring(3);

             if (timePartFormat.startsWith(":MM"))
             {
                // tz minutes
                sb.append(':').append(nf.format(Math.abs(getTzMinutes())));
                timePartFormat = timePartFormat.substring(3);
             }
          }

          // now timePartFormat should be an empty string
          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.
        */
       public void readExternal(ObjectInput in)
       throws IOException,
              ClassNotFoundException
       {
          // let the date class handle date component
          super.readExternal(in);

          // read the time component:
          this.time = in.readInt();
       }

       /**
        * 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.
        */
       public void writeExternal(ObjectOutput out)
       throws IOException
       {
          // let the date class handle date component
          super.writeExternal(out);

          // write the time component:
          out.writeInt(this.time);
       }

       /**
        * Return an object equal to this expressed in local time (current timezone offset).
        *
        * @return  a copy of this object (it's already in local time)
        */
       public datetime toLocalDatetime()
       {
          return new datetime(this);
       }

       /**
        * 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-DDTHH:MM:SS.SSS") or "?" if <code>unknown</code>.
        */
       @Override
       public String getIsoDate()
       {
          if (isUnknown())
          {
             return "?";
          }

          String isoDate = super.getIsoDate();
          if ("?".equals(isoDate))
          {
             return "?";
          }

          int hours = getHours();
          int minutes = getMinutes();
          int seconds = getSeconds();
          int millis = getMilliseconds();

          StringBuilder sb = new StringBuilder(isoDate);
          sb.append("T");
          if (hours < 10)
          {
             sb.append('0');
          }
          sb.append(hours);
          sb.append(":");
          if (minutes < 10)
          {
             sb.append('0');
          }
          sb.append(minutes);
          sb.append(":");
          if (seconds < 10)
          {
             sb.append('0');
          }
          sb.append(seconds);
          sb.append(".");
          if (millis < 100)
          {
             if (millis < 10)
             {
                sb.append('0');
             }
             sb.append('0');
          }
          sb.append(millis);

          return sb.toString();
       }

       /**
        * Reset the time for this instance.
        */
       protected void resetTime()
       {
          this.time = 0;
       }

       /**
        * Parses this ISO 8601 value as a datetime.
        *
        * @param    val
        *           The date to parse.
        *
        * @throws   DateTimeParseException
        *           If the format is not ISO 8601 compliant.
        */
       @Override
       public void fromIso(String val)
       {
          LocalDateTime odt = LocalDateTime.parse(val, ISO_LOCAL_DATE_TIME);
          Date date = Date.from(odt.atZone(ZoneId.systemDefault()).toInstant());
          date d = new date(date);
          int millis = odt.get(ChronoField.MILLI_OF_DAY);
          assign(new datetime(d, millis));
       }

       /**
        * 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. The main purpose of this method is to select the proper
        * date format and then create the proper {@code datetime} object with respective values.
        * <p>
        * {@code now} is also supported, in which case the current timestamp is returned.
        *
        * @param   text
        *          The text of the literal to be parsed.
        *
        * @return  A {@code datetime} representing the respective value, taking into account the current session
        *          settings (like the date format and the windowing year).
        */
       public static datetime parseInitial(String text)
       {
          if ("now".equalsIgnoreCase(text))
          {
             return now();
          }

          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 datetime(text, df, getWindowingYear());
          }

          if (text.length() >= 10)
          {
             // parsing a string literal is more lenient
             if ((text.charAt(2) == '/' || text.charAt(2) == '-') &&
                 (text.charAt(5) == '/' || text.charAt(5) == '-'))
             {
                // identified dd/dd/dddd, must be MDY
                return new datetime(text, DEFAULT_DATE_FORMAT, DEFAULT_DATE_WINDOWING_YEAR);
             }

             // assumed dddd/dd/dd, always in YMD
             return new datetime(text, DEFAULT_DATETIME_FORMAT, DEFAULT_DATE_WINDOWING_YEAR);
          }

          // probably an incorrectly formed literal
          return new datetime(text);
       }

       /**
        * 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 ContextIndependentDateTime(isUnknown(), this.longValue(), this.time);
       }

       /**
        * Returns the absolute time in milliseconds since Java 'Epoch' or 0
        * if this is unknown.
        *
        * @return   The absolute time in milliseconds since the Java epoch.
        */
       @Override
       protected long getAbsoluteTimeOffset()
       {
          return isUnknown() ? 0 : (super.getAbsoluteTimeOffset() + time);
       }

       /**
        * Worker to set the date (in the superclass) and the time (in this class)
        * given all the basic inputs.
        *
        * @param    d
        *           The date instance to copy from.
        * @param    time
        *           Milliseconds since midnight.
        */
       protected void instantiateFromDateTime(date d, long time)
       {
          checkUndoable(() -> new datetime(d,  time));

          super.deepAssign(d);
          if (!isUnknown())
          {
             this.time = (int) time;
          }
       }

       /**
        * Generate a parsing error. The object is left in unknown state after setting the error code
        * and associated message.
        * The routine automatically generates the error message based on the error code provided.
        *
        * @param   errCode
        *          The code of error to be raised.
        */
       protected void generateParsingError(int errCode)
       {
          String msg = "";
          switch (errCode)
          {
             case 11841:
                msg = "Invalid characters in datetime/datetime-tz value";
                break;
             case 11256:
                msg = "Invalid minutes in datetime value";
                break;
             case 11254:
                msg = "Invalid minute offset in the timezone portion of a datetime-tz value";
                break;
             case 11257:
                msg = "Missing colon between hour and minute in datetime value";
                break;
             case 11259:
                msg = "Invalid hour offset in the timezone portion of a datetime-tz value";
                break;
             case 11260:
                msg = "Invalid hours in datetime value";
                break;
             case 11253:
                msg = "Invalid seconds in datetime value";
                break;
             case 11252:
                msg = "Invalid milliseconds in datetime value";
                break;
             case 0:
                return; //do nothing
          }

          ErrorManager.recordOrThrowError(errCode, msg);
          setUnknown();
       }

       /**
        * Parses the given date and time specification and assigns the values into the instance.  The result will
        * be a date and time value that is "local" to the default timezone.  If a different timezone is specified
        * in the input, then the date and time values that are specified will be translated into values "local"
        * to the default timezone, before assignment.
        * <p>
        * The special string "NOW" is honored as the specification.  This is equivalent to the built-in function
        * {@code now}.
        * <p>
        * When the specification is of a datetime literal, it must match the following format rules:
        * <p>
        * <ol>
        *   <li> Date value (this is not optional).  The string cannot start with whitespace. The first input is
        *        a date value that has at least a month and day specified or a month, day and year. The values
        *        are parsed in order as specified in SESSION:DATE-FORMAT. Zero padding is allowed but not
        *        required.
        *   <li> Date and time delimiter (not optional). Whitespace or a letter T (upper or lowercase) which is
        *        followed by whitespace. If the letter T is used, it cannot be preceded by whitespace. No other
        *        characters are allowed.
        *   <li> Time value (optional). If not specified, it is set to 0. It is specified in HH:MM:SS.sss format,
        *        however right side components can be left off (so HH, HH:MM, HH:MM:SS and HH:MM:SS.sss are all
        *        valid forms). Zero padding is allowed but not required, so hours can be H or HH, minutes M or
        *        MM, seconds S or SS and milliseconds can be s, ss or sss. Delimiters can only be there if the
        *        following component is included.
        *   <li> Whitespace (optional).
        *   <li> Timezone specification (optional). This is in the form of a sign (+ or -) then the hours (1 or
        *        2 digits) a colon delimiter and then minutes (1 or 2 digits). Zero padding is allowed but not
        *        required.  The minutes can be left off but if so, then the delimiter must also be left off.  If
        *        specified, the instance will be set using the timezone specified EVEN THOUGH THE TIMEZONE OFFSET
        *        IS NOT STORED (since this is not a datetimetz).
        *   <li> Whitespace (optional).
        * </ol>
        * If the {@code spec} string is empty or if it matches '?' (with no leading or trailing spaces), an
        * unknown datetime object is instantiated.
        *
        * @param    spec
        *           The date and time specification.
        * @param    dateFormat
        *           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.
        */
       @Override
       protected void instantiateFromStringWorker(String spec, String dateFormat, int windowingYear)
       {
          if (spec == null || spec.trim().isEmpty() || spec.equals("?"))
          {
             setUnknown();
             return;
          }

          // special cases where the input is not a dt literal
          if ("now".equalsIgnoreCase(spec))
          {
             assign(datetimetz.now());
             return;
          }
          else if ("today".equalsIgnoreCase(spec))
          {
             invalidInitializer("TODAY");
             setUnknown();
             return;
          }

          // at this point, if the input includes ? character but had other characters also
          // included (e.g. leading or trailing spaces like " ?" or "? "), then it is improperly
          // formed
          if (spec.indexOf('?') >= 0)
          {
             ErrorManager.recordOrThrowError(85, "Invalid date input");
             return;
          }

          // auto detect date format
          if (dateFormat == null/*auto*/)
          {
             dateFormat = date.getDateOrderNative();
          }

          spec = spec.trim();
          boolean hasZuluTime = spec.endsWith("Z");
          if (hasZuluTime)
          {
             spec = spec.substring(0, spec.length() - 1);
          }

          final String decSep = String.valueOf(NumberType.getDecimalSeparator());
          final String regexpDecimalSep = (".".equals(decSep)) ? "\\." : decSep;

          // global sanity check:
          String copy = spec.toUpperCase()
                            .replaceAll("[TAPM:,\\." + regexpDecimalSep + "\\-\\+0123456789/]", " ").trim();
          if (!copy.isEmpty())
          {
             generateParsingError(11841);
             return;
          }

          // Switching to uppercase will assure date-time separator is 'T' or space and AM/PM are
          // also uppercased. We replace the (first) occurrence of 'T' with space to simplify date and
          // time separation.
          // Also, spaces at start/end seem to be ignored, except if the date portion includes a ?
          // which is not allowed and is handled above.
          spec = spec.toUpperCase().trim();

          int k = spec.indexOf('T'); // look for date / time separator
          if (k == -1)
          {
             // if 'T' is not the separator, then it's probably space:
             k = spec.indexOf(' ');
          }
          else if (k > 0 && spec.charAt(k - 1) == ' ')
          {
             // if space appears just before 'T' separator we have an invalid format
             generateParsingError(11841);
             return;
          }
          if (k == -1)
          {
             String fspec = spec;
             String fDateFormat = dateFormat;
             checkUndoable(() -> new datetime(fspec, fDateFormat, windowingYear));

             // if no separators have been found assume format contain only date part
             super.instantiateFromStringWorker(spec, dateFormat, windowingYear);
             // so time and tz are null:
             if (getType() == Type.DATETIMETZ)
             {
                int[] dateParts = getYearMonthDay();
                ((datetimetz) this).resetTime(date.createDate(dateParts[0], dateParts[1], dateParts[2]));
             }
             else if (getType() == Type.DATETIME)
             {
                resetTime();
             }

             return;
          }

          String datePartSpec = spec.substring(0, k);    // first k digits is the date
          // date part can use date.instantiateFromStringWorker()
          super.instantiateFromStringWorker(datePartSpec, dateFormat, windowingYear);
          if (isUnknown())
          {
             // date parsing failed, do not continue
             return;
          }

          String timePartSpec = spec.substring(k + 1).trim();
          // acceptable formats: h[:m[:s]] [am] [+h[:m]]

          String timeStr = "";
          String zoneStr = "";

          // locate am/pm (if exists)
          char ampmSpec = '0';
          int ampmOffset = -1;
          ampmOffset = timePartSpec.indexOf("PM");

          if (ampmOffset != -1)
          {
             ampmSpec = 'P';
          }
          else
          {
             ampmOffset = timePartSpec.indexOf("P");
             if (ampmOffset != -1)
             {
                ampmSpec = 'P';
             }
             else
             {
                ampmOffset = timePartSpec.indexOf("AM");
                if (ampmOffset != -1)
                {
                   ampmSpec = 'A';
                }
                else
                {
                   ampmOffset = timePartSpec.indexOf("A");
                   if (ampmOffset != -1)
                   {
                      ampmSpec = 'A';
                   }
                }
             }
          }

          //locate timezone (if exists)
          int tzOffset = timePartSpec.indexOf("+");
          if (tzOffset == -1)
          {
             tzOffset = timePartSpec.indexOf("-");
          }
          boolean hasTimeZone = (tzOffset != -1);

          if (hasTimeZone && hasZuluTime)
          {
             generateParsingError(11841);
             return;
          }

          if (ampmSpec != '0')
          {
             timeStr = timePartSpec.substring(0, ampmOffset);

             if (hasTimeZone)
             {
                if (ampmOffset > tzOffset)
                {
                   generateParsingError(11841);
                   return;
                }

                // has all three components
                zoneStr = timePartSpec.substring(tzOffset);
             }
          }
          else if (hasTimeZone)
          {
             // time and timezone
             timeStr = timePartSpec.substring(0,  tzOffset);
             zoneStr = timePartSpec.substring(tzOffset);
          }
          else
          {
             // no ampm and no timezone
             timeStr = timePartSpec;
          }

          // sanity check, round 2: timeStr must contain only : . and digits
          // additionally + and - are ignored at this stage
          copy = timeStr.toUpperCase().replaceAll("[,\\.\\+\\-" + regexpDecimalSep + ":0123456789/]", " ").trim();
          if (!copy.isEmpty())
          {
             generateParsingError(11841);
             return;
          }

          // sanity check, round 3: zoneStr must contain only  + - : and digits
          copy = zoneStr.toUpperCase().replaceAll("[\\+\\-:0123456789/]", " ").trim();
          if (!copy.isEmpty())
          {
             generateParsingError(11841);
             return;
          }

          long hours = 0;
          long minutes = 0;
          long seconds = 0;
          long millis = 0;
          long zoneOffset = datetimetz.INVALID_TZ;

          int errCode = 0;
          try
          {
             // catching only number parsing exceptions
             String decSeps = ",." + decSep;
             StringTokenizer st = new StringTokenizer(timeStr.trim(), (decSep + ",.: "), true);
             if (st.hasMoreTokens())
             {
                // prepare hours parsing exceptions:
                errCode = 11841;
                hours = Long.parseLong(st.nextToken());
                if (st.hasMoreTokens())
                {
                   if (!":".equals(st.nextToken())) {
                      generateParsingError(11841);
                      return;
                   }
                   if (!st.hasMoreTokens())
                   {
                      generateParsingError(11256);
                      return;
                   }

                   // prepare minutes parsing exceptions:
                   errCode = 11256;
                   minutes = Long.parseLong(st.nextToken());
                   if (st.hasMoreTokens())
                   {
                      if (!":".equals(st.nextToken())) {
                         generateParsingError(11841);
                         return;
                      }
                      if (!st.hasMoreTokens())
                      {
                         generateParsingError(11253);
                         return;
                      }
                      // prepare seconds parsing exceptions:
                      errCode = 11253;
                      seconds = Long.parseLong(st.nextToken());
                      if (st.hasMoreTokens())
                      {
                         if (!decSeps.contains(st.nextToken())) {
                            generateParsingError(11841);
                            return;
                         }
                         if (!st.hasMoreTokens())
                         {
                            generateParsingError(11252);
                            return;
                         }
                         String millisStr = st.nextToken();
                         if (millisStr.length() > 3)
                         {
                            // if there are more than 3 digits, only the first 3 are processed,
                            // the others are discarded
                            millisStr = millisStr.substring(0, 3);
                         }
                         errCode = 11252;
                         millis = Long.parseLong(millisStr);

                         if (millisStr.length() == 2)
                         {
                            millis *= 10;
                         }
                         if (millisStr.length() == 1)
                         {
                            millis *= 100;
                         }

                         if (st.hasMoreTokens())
                         {
                            // that should have been the last token
                            generateParsingError(11841);
                            return;
                         }
                      }
                   }
                }
             }

             // adjust AM/PM hours format
             if (ampmSpec != '0')
             {
                if (hours == 12) //in AM/PM there is no 0 hour
                {
                   hours = 0;
                }
                else if (hours > 12)
                {
                   // hour 12+ in AM/PM mode ?
                   generateParsingError(11260);
                   return;
                }
                if (ampmSpec == 'P') // PM
                {
                   hours += 12;
                }
             }

             // set up read time (do this first, to validate the values)
             setTime(hours, minutes, seconds, millis);

             // read timezone and validate (only if the time is valid)
             if (!zoneStr.isEmpty())
             {
                // timezone spec can use date.specToOffset()
                st = new StringTokenizer(zoneStr.trim(), ":+-", true);
                String sign = st.nextToken();

                if (!st.hasMoreTokens())
                {
                   generateParsingError(11259);
                   return;
                }

                // prepare hours parsing exceptions:
                errCode = 11259;
                long tzHours = Long.parseLong(st.nextToken());
                long tzMinutes = 0;

                if (st.hasMoreElements())
                {
                   if (!":".equals(st.nextToken()))
                   {
                      generateParsingError(11257);
                      return;
                   }
                   if (!st.hasMoreTokens())
                   {
                      generateParsingError(11254);
                      return;
                   }
                   // prepare hours parsing exceptions:
                   errCode = 11254;
                   tzMinutes = Long.parseLong(st.nextToken());

                   if (st.hasMoreTokens())
                   {
                      // that should have been the last token
                      generateParsingError(11841);
                      return;
                   }
                }

                if (tzMinutes < 0 || tzMinutes > 59)
                {
                   generateParsingError(11254);
                   return;
                }
                if (tzHours < -14 || tzHours > 14)
                {
                   generateParsingError(11259);
                   return;
                }
                zoneOffset = tzHours * 60 + tzMinutes;
                if ("-".equals(sign))
                {
                   zoneOffset = -zoneOffset;
                }
                //adjust time with zoneOffset
             }
          }
          catch (NumberFormatException e)
          {
             generateParsingError(errCode);
             return;
          }

          if (hasTimeZone)
          {
             setTimeZoneOffset(zoneOffset);
          }
          else if (hasZuluTime)
          {
             setTimeZoneOffset(0);
          }
          else
          {
             setTimeZoneOffset(datetimetz.getDefaultTimezoneOffset(this));
          }
       }

       /**
        * Set the offset in minutes from UTC for this instance and adjust to localtime.
        * This should only called from <code>instantiateFromStringWorker()</code>.
        *
        * @param    offset
        *           The offset, must be between -840 and 840 inclusive.
        */
       protected void setTimeZoneOffset(long offset)
       {
          if (offset < MIN_VALID_TZ || offset > MAX_VALID_TZ)
          {
             String err = "Invalid minute offset in datetime-tz value";
             ErrorManager.recordOrThrowError(11254, err);
             return;
          }

          // adjust to local time
          int localOffset = datetimetz.getDefaultTimezoneOffset(this);
          assign(plusMillis(this, (localOffset - offset) * 60 * 1000));
       }

       /**
        * Extract the portion of the calendar representing the number of
        * milliseconds since midnight.
        *
        * @param    cal
        *           The calendar instance to query.
        *
        * @return   The milliseconds since midnight.
        */
       private static int millisSinceMidnight(Calendar cal)
       {
          int hour   = cal.get(Calendar.HOUR_OF_DAY);
          int mins   = cal.get(Calendar.MINUTE);
          int secs   = cal.get(Calendar.SECOND);
          int millis = cal.get(Calendar.MILLISECOND);

          return (((((hour * 60) + mins) * 60) + secs) * 1000) + millis;
       }

       /**
        * Convenient class that wraps the low-level {@link #julian} and {@link #time} parameters
        * to avoid pinning the whole context-local of the datetime.
        */
       public static class ContextIndependentDateTime
       extends ContextIndependentDate
       {
          /** The milliseconds since midnight. */
          int time;

          /**
           * Basic c'tor.
           *
           * @param   unknown
           *          {@code true} if the value is unknown
           * @param   julian
           *          The date for this instance in Progress 4GL format.
           * @param   time
           *          The milliseconds since midnight.
           */
          public ContextIndependentDateTime(boolean unknown, long julian, int time)
          {
             super(unknown, julian);
             this.time = time;
          }

          /**
           * 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 ContextIndependentDateTime) ||
                  (other instanceof datetimetz.ContextIndependentDateTimeTz))
             {
                // a date can be compared ONLY WITH other instances of date
                return false;
             }

             ContextIndependentDateTime that = (ContextIndependentDateTime) 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 && (this.time == that.time);
          }

          /**
           * Implementation of hash code.
           */
          @Override
          public int hashCode()
          {
             int result = 17;
             if (isUnknown())
             {
                return result;
             }

             result = 37 * result + (int) (this.julian ^ (this.julian >>> 32));
             result = 37 * result + time;

             return result;
          }
       }
    }