# datetimetz.java

    /*
    ** Module   : datetimetz.java
    ** Abstract : Progress 4GL compatible datetime-tz 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  20130529 Massive update:
    **                    Added time-zone components constants for datetime-tz formatter.
    **                    Added components validation function.
    **                    Default c'tor builds a light/fast unknown instance.
    **                    Created static function for NOW 4GL functionality.
    **                    Added parsers for 4GL literal/SQL and ISO
    ** 004 MAG 20140602 Fixed the BDT c'tor.
    ** 005 OM  20150129 Group c'tors at top of file, fixed javadocs. Added support for parsing
    **                  negative years. Rewrote calendarValue(). Added main() test method that
    **                  will indirectly test date and datetime functionality as well.
    ** 006 OM  20150623 Fixed conversion from text to datetime in polymorphic cases.
    ** 007 ECF 20150715 Replace StringBuffer with StringBuilder.
    ** 008 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.
    ** 009 CA  20160812 Use date.getZoneFromOffset to build TimeZone instances (as this will be use
    **                  internal cache).
    ** 010 EVL 20171103 Fix for date copy for constructor with date as parameter.
    ** 011 GES 20171207 Removed explicit bypass on pending error (new silent error mode).
    ** 012 CA  20180503 Renamed/moved some SESSION attribute getters/setters to fix issues with using
    **                  the SESSION via a handle.
    ** 013 OM  20190206 Added implementation of field size needed by record-length. Fixed c'tor form.
    ** 014 CA  20190615 Added fromIso() to parse a ISO-8601 date.
    ** 015 CA  20190628 Javadoc fix.
    ** 016 OM  20190626 Granted public access to getIsoDate().
    ** 017 CA  20191206 Fixed assign/deepAssign when the value is null or unknown.
    ** 018 CA  20200503 Added datetimetz(NumberType,NumberType) stub.
    ** 019 CA  20200519 ISO 8601 format allows single-digit fields in 4GL.
    ** 020 OM  20191101 Added OffsetDateTime based constructor.
    ** 021 GES 20200806 Reduced ErrorManager context-local usage.
    **     CA  20200914 Fixed c'tor with explicit time offset.
    ** 022 IAS 20201007 Added Type enum
    **     CA  20210304 Fixed date, datetime and datetimetz literal parsing.
    **     CA  20210628 Fixed DATETIME and DATETIME-TZ toStringExport() usage.
    **     VVT 20211027 Fixed: time zone spec duplicated in toStringMessage(). See #5734.
    **     VVT 20211029 Fixed: toStringMessage(): should match the description in javadoc.
    **                  See #5734-7. Missing @Override annotations added.
    **     AL2 20220328 Added proxy checks for BDT.
    **     CA  20220329 'fromIso' method was changed to public.
    **     CA  20220513 timezone() builtin function must emit as a static method call if the argument is POLY.
    **                  Added POLY versions of the DATETIMETZ() builtin function.
    **     OM  20220622 Improve date/interval operations. Added implementation for the new methods.
    ** 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 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 026 AB  20230907 Overrided resetTime(Date). Modified resetTime() and assign(date).
    **     AB  20230907 Removed the overriding annotation for resetTime(Date). Modified assign(date).
    ** 027 CA  20240318 INITIAL value of a datetime(-tz) variable will always have the date 'mdy' format.
    ** 028 OM  20240509 Added auto detection for date format, depending on the context and a quick heuristic
    **                  analysis of the text.
    ** 029 OM  20240524 Refined algorithm from H028. It also caused regressions.
    ** 030 SB  20240904 For constructors where time components are set individually, if a time zone offset is
    **                  not provided, use the default time zone. Refs #9124.
    ** 031 CA  20241002 Fixed assignment to java types.
    ** 032 LS  20250127 Added datetimetz(Date,int).
    ** 033 ICP 20250129 Used int64.of to leverage caches instances.
    ** 034 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.ErrorManager.ErrorEntry;
    import com.goldencode.p2j.util.date.ContextIndependentDate;
    import com.goldencode.p2j.util.datetime.ContextIndependentDateTime;
    import com.goldencode.p2j.util.logging.*;

    /**
     * A class that represents a Progress 4GL compatible datetime-tz 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>datetime</code> class so all methods should work
     * directly. If they don't they will be overridden. Also should the following
     * static implementations:
     * <p>
     * <pre>
     * now           function (as initializer for datetime-tz)     {@link #now()}
     * datetime-tz   function (date, integer)                      {@link #datetimetz(date, NumberType)}
     * datetime-tz   function (datetime, integer)                  {@link #datetimetz(date, NumberType)}
     * datetime-tz   function (datetime-tz, integer)               {@link #datetimetz(date, NumberType)}
     * datetime-tz   function (date, integer, integer)             {@link #datetimetz(date, NumberType, NumberType)}
     * datetime-tz   function (integer m, d, y, h, mm, s, ms, tz)  {@link #datetimetz(NumberType, NumberType, NumberType, NumberType, NumberType, NumberType, NumberType, NumberType)}
     * datetime-tz   function (string parameter)                   {@link #datetimetz(String)}
     * timezone      function (datetime-tz parameter)              {@link #getTimeZoneOffset}
     *                        [literal]                            {@link #fromLiteral(String)}
     * </pre>
     * <p>
     * Default data type display format: 99/99/9999 HH:MM:SS.SSS+HH:MM
     */
    public class datetimetz
    extends datetime
    {
       /** The specific ISO-8601 datetime-tz format used by 4GL. */
       protected static final DateTimeFormatter ISO_OFFSET_DATE_TIME =
          new DateTimeFormatterBuilder().parseCaseInsensitive()
                                        .append(ISO_LOCAL_DATE_TIME)
                                        .appendOffsetId()
                                        .toFormatter()
                                        .withResolverStyle(ResolverStyle.LENIENT)
                                        .withChronology(IsoChronology.INSTANCE);

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

       /** The pattern format for ISO 8601 dates. */
       private static final String DTZ_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)+)?)?" +
                // timezone part
                // mandatory + / - sign
                // the hour represented on one or two digits; 0 prefix is optional if hour < 10
                // the minutes is optional and represented on max 2 digits.
                "(([+\\-])(\\d\\d?)(:(\\d\\d?))?)";

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

       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(datetimetz.class);

       /** Time zone offset in minutes from Coordinated Universal Time (UTC). */
       private int offset = INVALID_TZ;

       /**
        * Default constructor builds an unknown object.
        */
       public datetimetz()
       {
          super(); // the default constructor of date will set the object to unknown
       }

       /**
        * Constructs an instance using the inputs.
        * <p>
        * This is an implementation of 1 form of the DATETIME-TZ() 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 given 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.
        * <p>
        * Any input that is <code>unknown</code> will cause this instance to be
        * <code>unknown</code>.
        * <p>
        * All arguments for this constructor are POLY arguments.
        *
        * @param    d
        *           The date from which to copy.  This must be an instance of the
        *           <code>date</code> superclass and may NOT be an instance of the
        *           <code>datetime</code> or <code>datetimetz</code> since that is
        *           a 4GL error.
        * @param    time
        *           The milliseconds since midnight.
        * @param    offset
        *           The timezone offset from UTC in minutes. Must be between -840 and 840 inclusive.
        */
       public datetimetz(BaseDataType d, BaseDataType time, BaseDataType offset)
       {
          if (!(d instanceof date) || !(time instanceof NumberType) || !(offset instanceof NumberType))
          {
             ErrorManager.recordOrShowError(12117, "DATETIME-TZ", "");
             // Unacceptable datatype for DATETIME-TZ argument (12117)
             return;
          }

          initialize((date) d, (NumberType) time, (NumberType) offset);
       }

       /**
        * Constructs an instance using the inputs, with varying behavior depending
        * on the type of the first parameter.
        * <p>
        * If the first parameter is a <code>date</code> instance, then the date is
        * copied from that instance and the second parameter is treated as the
        * time value (number of milliseconds since midnight).
        * <p>
        * If the first parameter is a <code>datetime</code> or
        * <code>datetimetz</code> instance, then the date and time is copied from
        * the instance and the second parameter is treated as a positive or
        * negative timezone offset for the new instance.
        * <p>
        * This is an implementation of 3 forms of the DATETIME-TZ() 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.
        * <p>
        * Any input that is <code>unknown</code> will cause this instance to be
        * <code>unknown</code>.
        * <p>
        * All arguments for this constructor are POLY arguments.
        *
        * @param    d
        *           The date (or date subclass) from which to copy.
        * @param    timeOrOffset
        *           The milliseconds since midnight if the first parameter is a
        *           <code>date</code> or a timezone offset from UTC in minutes
        *           if the first parameter is a <code>datetime</code>.  As an
        *           offset, the value must be between -840 and 840.
        */
       public datetimetz(BaseDataType d, BaseDataType timeOrOffset)
       {
          if (!(d instanceof date) || !(timeOrOffset instanceof NumberType))
          {
             ErrorManager.recordOrShowError(12117, "DATETIME-TZ", "");
             // Unacceptable datatype for DATETIME-TZ argument (12117)
             return;
          }

          initialize((date) d, (NumberType) timeOrOffset);
       }

       /**
        * Constructs an instance using the inputs, with varying behavior depending
        * on the type of the first parameter.
        * <p>
        * If the first parameter is a <code>date</code> instance, then the date is
        * copied from that instance and the second parameter is treated as the
        * time value (number of milliseconds since midnight).
        * <p>
        * If the first parameter is a <code>datetime</code> or
        * <code>datetimetz</code> instance, then the date and time is copied from
        * the instance and the second parameter is treated as a positive or
        * negative timezone offset for the new instance.
        * <p>
        * This is an implementation of 3 forms of the DATETIME-TZ() 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.
        * <p>
        * Any input that is <code>unknown</code> will cause this instance to be
        * <code>unknown</code>.
        * <p>
        * The first parameter for this constructor is a POLY argument.
        *
        * @param    d
        *           The date (or date subclass) from which to copy.
        * @param    timeOrOffset
        *           The milliseconds since midnight if the first parameter is a
        *           <code>date</code> or a timezone offset from UTC in minutes
        *           if the first parameter is a <code>datetime</code>.  As an
        *           offset, the value must be between -840 and 840.
        */
       public datetimetz(BaseDataType d, NumberType timeOrOffset)
       {
          if (!(d instanceof date))
          {
             ErrorManager.recordOrShowError(12117, "DATETIME-TZ", "");
             // Unacceptable datatype for DATETIME-TZ argument (12117)
             return;
          }

          initialize((date) d, timeOrOffset);
       }

       /**
        * 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 datetimetz(BaseDataType value)
       {
          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
                datetimetz   isoDt = datetimetz.fromLiteral(strVal);
                datetimetz[] tmp   = new datetimetz[1];

                Runnable code = () -> tmp[0] = new datetimetz(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();
             }
          }
       }

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

       /**
        * Constructs an instance using the inputs, with varying behavior depending
        * on the type of the first parameter.
        * <p>
        * If the first parameter is a <code>date</code> instance, then the date is
        * copied from that instance and the second parameter is treated as the
        * time value (number of milliseconds since midnight).
        * <p>
        * If the first parameter is a <code>datetime</code> or
        * <code>datetimetz</code> instance, then the date and time is copied from
        * the instance and the second parameter is treated as a positive or
        * negative timezone offset for the new instance.
        * <p>
        * This is an implementation of 3 forms of the DATETIME-TZ() 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.
        * <p>
        * Any input that is <code>unknown</code> will cause this instance to be
        * <code>unknown</code>.
        *
        * @param    d
        *           The date (or date subclass) from which to copy.
        * @param    timeOrOffset
        *           The milliseconds since midnight if the first parameter is a
        *           <code>date</code> or a timezone offset from UTC in minutes
        *           if the first parameter is a <code>datetime</code>. As an
        *           offset, the value must be between -840 and 840.
        */
       public datetimetz(date d, NumberType timeOrOffset)
       {
          initialize(d, timeOrOffset);
       }

       /**
        * Create an instance from the given date and time, via numeric values.
        *
        * @param    d
        *           The date as a numeric value.
        * @param    timeOrOffset
        *           The time as a numeric value.
        */
       public datetimetz(NumberType d, NumberType timeOrOffset)
       {
          UnimplementedFeature.missing("datetimetz(NumberType, NumberType)");
       }

       /**
        * Constructs an instance using the inputs, with varying behavior depending
        * on the type of the first parameter.
        * <p>
        * If the first parameter is a <code>date</code> instance, then the date is
        * copied from that instance and the second parameter is treated as the
        * time value (number of milliseconds since midnight).
        * <p>
        * If the first parameter is a <code>datetime</code> or
        * <code>datetimetz</code> instance, then the date and time is copied from
        * the instance and the second parameter is treated as a positive or
        * negative timezone offset for the new instance.
        * <p>
        * This is an implementation of 3 forms of the DATETIME-TZ() 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.
        * <p>
        * Any input that is <code>unknown</code> will cause this instance to be
        * <code>unknown</code>.
        *
        * @param    d
        *           The date (or date subclass) from which to copy.
        * @param    timeOrOffset
        *           The milliseconds since midnight if the first parameter is a
        *           <code>date</code> or a timezone offset from UTC in minutes
        *           if the first parameter is a <code>datetime</code>.  As an
        *           offset, the value must be between -840 and 840.
        */
       public datetimetz(date d, double timeOrOffset)
       {
          initialize(d, new decimal(timeOrOffset));
       }

       /**
        * Constructs an instance using the inputs.
        * <p>
        * This is an implementation of 1 form of the DATETIME-TZ() 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 given 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.
        * <p>
        * Any input that is <code>unknown</code> will cause this instance to be
        * <code>unknown</code>.
        *
        * @param    d
        *           The date from which to copy.  This must be an instance of the
        *           <code>date</code> superclass and may NOT be an instance of the
        *           <code>datetime</code> or <code>datetimetz</code> since that is
        *           a 4GL error.
        * @param    time
        *           The milliseconds since midnight.
        * @param    offset
        *           The timezone offset from UTC in minutes. Must be between -840 and 840 inclusive.
        */
       public datetimetz(date d, NumberType time, NumberType offset)
       {
          initialize(d, time, offset);
       }

       /**
        * Constructs an instance using the inputs.
        * <p>
        * This is an implementation of 1 form of the DATETIME-TZ() 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 given 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.
        * <p>
        * Any input that is <code>unknown</code> will cause this instance to be
        * <code>unknown</code>.
        *
        * @param    d
        *           The date from which to copy.  This must be an instance of the
        *           <code>date</code> superclass and may NOT be an instance of the
        *           <code>datetime</code> or <code>datetimetz</code> since that is
        *           a 4GL error.
        * @param    time
        *           The milliseconds since midnight.
        * @param    offset
        *           The timezone offset from UTC in minutes. Must be between -840 and 840 inclusive.
        */
       public datetimetz(date d, NumberType time, double offset)
       {
          initialize(d, time, new decimal(offset));
       }

       /**
        * Constructs an instance using the inputs.
        * <p>
        * This is an implementation of 1 form of the DATETIME-TZ() 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 given 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.
        * <p>
        * Any input that is <code>unknown</code> will cause this instance to be
        * <code>unknown</code>.
        *
        * @param    d
        *           The date from which to copy.  This must be an instance of the
        *           <code>date</code> superclass and may NOT be an instance of the
        *           <code>datetime</code> or <code>datetimetz</code> since that is a 4GL error.
        * @param    time
        *           The milliseconds since midnight.
        * @param    offset
        *           The timezone offset from UTC in minutes. Must be between -840 and 840 inclusive.
        */
       public datetimetz(date d, double time, NumberType offset)
       {
          initialize(d, new decimal(time), offset);
       }

       /**
        * Constructs an instance using the inputs.
        * <p>
        * This is an implementation of 1 form of the DATETIME-TZ() 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 given 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.
        * <p>
        * Any input that is <code>unknown</code> will cause this instance to be
        * <code>unknown</code>.
        *
        * @param    d
        *           The date from which to copy.  This must be an instance of the
        *           <code>date</code> superclass and may NOT be an instance of the
        *           <code>datetime</code> or <code>datetimetz</code> since that is a 4GL error.
        * @param    time
        *           The milliseconds since midnight.
        * @param    offset
        *           The timezone offset from UTC in minutes. Must be between -840 and 840 inclusive.
        */
       public datetimetz(date d, double time, double offset)
       {
          initialize(d, new decimal(time), new decimal(offset));
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME-TZ() 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 datetimetz(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(new decimal(hours), new decimal(minutes), new decimal(seconds),
                  new decimal(millis), new int64(datetimetz.getDefaultTimezoneOffset(this)));
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME-TZ() 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.
        *           The offset in minutes from UTC. Must be between -840 and 840
        *           inclusive.
        */
       public datetimetz(double month,
                         double day,
                         double year,
                         double hours,
                         double minutes,
                         double seconds,
                         double millis,
                         double offset)
       {
          // the super class will take care of the date part
          super(month, day, year);

          setTime(new decimal(hours),
                  new decimal(minutes),
                  new decimal(seconds),
                  new decimal(millis),
                  new decimal(offset));
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME-TZ() 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 datetimetz(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(new decimal(hours), new decimal(minutes), new decimal(seconds),
                  int64.of(0L), int64.of((long)datetimetz.getDefaultTimezoneOffset(this)));
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME-TZ() 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 datetimetz(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(new decimal(hours), new decimal(minutes), int64.of(0L), int64.of(0L),
                  int64.of((long)datetimetz.getDefaultTimezoneOffset(this)));
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME-TZ() 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 datetimetz(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, int64.of(0L));
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME-TZ() 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.
        * @param    offset
        *           The offset in minutes from UTC. Must be between -840 and 840
        *           inclusive.
        */
       public datetimetz(NumberType month,
                         NumberType day,
                         NumberType year,
                         NumberType hours,
                         NumberType minutes,
                         NumberType seconds,
                         NumberType millis,
                         NumberType offset)
       {
          // 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, offset);
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME-TZ() 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 datetimetz(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),
                  int64.of((long)datetimetz.getDefaultTimezoneOffset(this)));
       }

       /**
        * Construct a datetime instance using explicit values for each discrete
        * component.  This is a variant of the DATETIME-TZ() 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 datetimetz(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, new int64(0), new int64(0), new int64(0));
       }

       /**
        * Creates a new instance from the given date and time specification. This
        * is one form of the DATETIME-TZ 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 datetimetz(String spec)
       throws ErrorConditionException
       {
          instantiateFromStringWorker(spec, null/*auto*/, -1);
          if (!isUnknown() && offset == INVALID_TZ)
          {
             offset = datetimetz.getDefaultTimezoneOffset(this);
          }
       }

       /**
        * Converts a string to a datetime-tz 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     datetimetz#datetimetz(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 datetimetz(String spec, String format, int windowingYear)
       throws ErrorConditionException
       {
          instantiateFromStringWorker(spec, format, windowingYear);
          if (!isUnknown() && offset == INVALID_TZ)
          {
             offset = datetimetz.getDefaultTimezoneOffset(this);
          }
       }

       /**
        * Creates a new instance from the given date and time specification. This
        * is one form of the DATETIME-TZ 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
        *        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 datetimetz(character spec)
       throws ErrorConditionException
       {
          if (spec.isUnknown())
          {
             setUnknown();
          }
          else
          {
             instantiateFromStringWorker(spec.toStringMessage(), null/*auto*/, -1);
             // set the correct timezone in case it is not part of the string:

             if (!isUnknown() && offset == INVALID_TZ)
             {
                offset = datetimetz.getDefaultTimezoneOffset(this);
             }
          }
       }

       /**
        * Creates a new instance from the given calendar value.
        *
        * @param   calendar
        *          The Calendar to be used for creating new instance.
        */
       public datetimetz(Calendar calendar)
       {
          if (calendar == null)
          {
             setUnknown();
             return;
          }

          super.deepAssign(new datetime(calendar.getTime(), calendar.getTimeZone()));
          TimeZone tz = calendar.getTimeZone();
          offset = (tz.getRawOffset() + tz.getDSTSavings()) / (60 * 1000);
       }

       /**
        * Creates a new instance from the given {@code OffsetDateTime} value.
        *
        * @param   odt
        *          The {code OffsetDateTime} to be used for creating new instance.
        */
       public datetimetz(OffsetDateTime odt)
       {
          this(odt.getMonthValue(), odt.getDayOfMonth(), odt.getYear(),
               odt.getHour(), odt.getMinute(), odt.getSecond(), odt.getNano() / 1000_000D,
               odt.getOffset().getTotalSeconds() / 60D);
       }

       /**
        * Creates an instance from a <code>java.util.Date</code> instance.
        * <p>
        * <b>WARNING: this will only work if the calendar in which this datetimetz
        * 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.datetimetz#datetimetz(Date,TimeZone)} version.
        *
        * @param    d
        *           The <code>Date</code> instance from which to pattern the instance.
        */
       public datetimetz(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    offset
        *           A timezone offset from UTC in minutes.
        */
       public datetimetz(Date d, int offset)
       {
          this(d, TimeZone.getTimeZone(ZoneOffset.ofTotalSeconds(offset * 60)));
       }

       /**
        * 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 datetimetz(Date d, TimeZone zone)
       {
          super(d, zone);
          offset = (zone.getRawOffset() + zone.getDSTSavings()) / (60 * 1000);
       }

       /**
        * Parses this ISO 8601 value as a datetime-tz.
        * <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)
       {
          datetimetz d = new datetimetz();
          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.
        *
        * @return  an instance of datetimetz representing the current moment.
        */
       public static datetimetz now()
       {
          // NOW uses the SESSION:TIME-ZONE if set or local-time otherwise
          integer sessionOffset = date.getTimeZone();
          int sessOffset = sessionOffset.isUnknown() ?
                datetimetz.getDefaultTimezoneOffset(null) :
                sessionOffset.intValue();

          return new datetimetz(datetime.now(), sessOffset);
       }

       /**
        * Obtains the timezone offset in minutes from UTC for this instance. This is the equivalent of the
        * TIMEZONE(datetime-tz) built-in function.
        *
        * @return   The timezone offset for this instance.
        */
       public static int64 getTimeZoneOffset(BaseDataType value)
       {
          if (value instanceof datetimetz)
          {
             return ((datetimetz) value).getTimeZoneOffset();
          }
          else if (value instanceof Text)
          {
             return date.getOffsetForSpec(((Text) value).toJavaType());
          }

          ErrorManager.recordOrShowError(11872, "", "");
          // Invalid timezone expression (11872)
          return new int64();
       }

       /**
        * Moves the current datetime-tz to another timezone offset. It is guaranteed to return another
        * object instance and this object remains unchanged.
        *
        * @param   offsetInMinutes
        *          The new timezone offset to move this object.
        *
        * @return  a new instance of the datetimetz shifted to requested timezone offset.
        */
       public datetimetz toOffset(int offsetInMinutes)
       {
          if (isUnknown())
          {
             return new datetimetz();
          }

          if (offsetInMinutes == this.offset)
          {
             return (datetimetz) this.duplicate();
          }

          datetimetz ret = datetimetz.plusMillis(this, (offsetInMinutes - offset) * 60 * 1000L);
          ret.offset = offsetInMinutes;
          return ret;
       }

       /**
        * Parse the INITIAL string of a datetime-tz var definition.
        *
        * @param    literal
        *           The datetime-tz literal, which will always have the date part in 'mdy' format.
        *
        * @return   The datetime-tz instance initialized with this literal.
        */
       public static datetimetz init(String literal)
       {
          return datetimetz.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[...]]]+H[[H]:M[M]]]
        * </pre>
        * where the date-separator can be either 't' or 'T' and timezone can be both positive '+'
        * or negative '-'. The hours and minutes are mandatory in 2 digits and seconds, tz-hours and
        * tz-minutes 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 classify 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 datetimetz fromLiteral(String str)
       {
          Matcher matcher = DTZ_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;
          int tzHours = 0;
          int tzMinutes = 0;
          boolean negativeTz = false;

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

             // optional components:
             if (matcher.group(8) != null)
             {
                seconds = Integer.parseInt(matcher.group(8));
             }
             if (matcher.group(9) != null)
             {
                String milliDigits = matcher.group(9).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;
                }
             }
             if (matcher.group(13) != null)
             {
                tzHours = Integer.parseInt(matcher.group(13));
             }
             if (matcher.group(15) != null)
             {
                tzMinutes = Integer.parseInt(matcher.group(15));
             }
             negativeTz = "-".equals(matcher.group(12));
          }
          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;
          }

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

          // honoring the timezone stuff when calculating the milliseconds
          int tzOffset = tzHours * 60 + tzMinutes;
          if (negativeTz)
          {
             tzOffset = -tzOffset;
          }
          ret.setTimeZoneOffset(tzOffset);

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

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

       /**
        * Creates a new instance of the same type that represents the <code>unknown value</code>.
        *
        * @return   An instance that represents the <code>unknown value</code>.
        */
       @Override
       public BaseDataType instantiateUnknown()
       {
          return new datetimetz();
       }

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

       /**
        * Return the default display format string for this type.
        *
        * @return   The default format string.
        */
       @Override
       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 (offset == 0 && intValue() == RAW_EPOCH && getTime() == 0)
          {
             return 1;
          }
          if (offset != 0 || intValue() != RAW_EPOCH )
          {
             return 13;
          }
          return 9 + new integer(getTime()).getSize();
       }

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

       /**
        * 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>datetimetz</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.
        */
       @Override
       public void assign(BaseDataType value)
       {
          super.assign(value);
       }

       /**
        * 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> or
        * <code>datetime</code>) time and timezome, 00:00 is assumed.
        *
        * @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)
          {
             deepAssign((datetimetz)value);
          }
          else
          {
             if (value instanceof datetime)
             {
                deepAssign((datetime)value);
             }
             else
             {
                deepAssign(value);
                setTime(0);
             }

             int[] dateParts = getYearMonthDay();
             int newOffset = date.getDefaultOffset(date.createDate(dateParts[0], dateParts[1], dateParts[2]));

             if (this.offset != newOffset)
             {
                checkUndoable(true);
             }

             this.offset = newOffset;
          }
       }

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

          checkUndoable(value);

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

       /**
        * 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.
        * <p>
        * Before comparing, both datetime-tz values are translated to UTC. There are no unique
        * objects: the following are equals:
        *    2001-06-30T22:59:59.999+05:00 EQ 2001-06-30T23:59:59.999+06:00
        * and
        *    2001-06-30T23:59:59.999+05:00 GT 2001-06-30T23:59:59.999+06:00
        * is positive.
        *
        * @param    obj
        *           The date instance to compare against.
        *
        * @return   -1, 0 or 1 depending on if this instance is less than, equal to or greater
        *           (respectively) than the specified instance <code>obj</code>.
        */
       @Override
       public int compareTo(Object obj)
       {
          boolean compatible = obj instanceof date;
          if ((obj instanceof datetime) && !(obj instanceof datetimetz))
          {
             // datetime-tz values are NOT comparable with datetime.
             // in special conditions date values are compatible, see below
             compatible = false;
          }
          if (!compatible)
          {
             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);
          }

          datetimetz that = (datetimetz) obj;

          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 milliseconds from epoch translated to UTC
          long thisAbsTime = this.longValue() * date.MILLIS_PER_DAY +
                             this.getTime() - this.offset * 60 * 1000L;
          long thatAbsTime = that.longValue() * date.MILLIS_PER_DAY +
                             that.getTime() - that.offset * 60 * 1000L;

          if (thisAbsTime > thatAbsTime)
          {
             return 1;
          }
          else if (thisAbsTime < thatAbsTime)
          {
             return -1;
          }

          // else 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)
        */
       @Override
       public boolean equals(Object o)
       {
          if (this == o)
          {
             return true;
          }

          if (o instanceof datetimetz)
          {
             datetimetz that = (datetimetz) o;

             boolean u1 = this.isUnknown();
             boolean u2 = that.isUnknown();

             if (u1 && u2)
             {
                return true;
             }

             if (u1 || u2)
             {
                return false;
             }

             long thisAbsTime = this.longValue() * date.MILLIS_PER_DAY +
                   this.getTime() - this.offset * 60 * 1000L;
             long thatAbsTime = that.longValue() * date.MILLIS_PER_DAY +
                   that.getTime() - that.offset * 60 * 1000L;
             return thisAbsTime == thatAbsTime;
          }

          return super.equals(o);
       }

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

          return (int) (37 * result + this.longValue() * date.MILLIS_PER_DAY +
                this.getTime() - this.offset * 60 * 1000);
       }

       /**
        * Does the same as standard <code>clone()</code> method but returns an instance of
        * <code>BaseDataType</code> and doesn't throw the <code>CloneNotSupportedException</code>.
        *
        * @return   A clone of this instance.
        */
       @Override
       public datetimetz duplicate()
       {
          return new datetimetz(this);
       }

       /**
        * Obtains the timezone offset in minutes from UTC for this instance. This
        * is the equivalent of the TIMEZONE(datetime-tz) built-in function.
        *
        * @return   The timezone offset for this instance.
        */
       public int64 getTimeZoneOffset()
       {
          return isUnknown() ? new int64() : new int64(offset);
       }

       /**
        * Returns the legacy type name of the this class.
        *
        * @return   Always "DATETIME-TZ".
        */
       @Override
       public String getTypeName()
       {
          return "DATETIME-TZ";
       }

       /**
        * Get the offset in minutes from UTC for this instance.
        *
        * @return   The offset. If valid it must be between -840 and 840 inclusive.
        *           It should not be invalid.
        */
       public int getOffset()
       {
          return offset;
       }

       /**
        * Set the offset in minutes from UTC for this instance.
        *
        * @param    offset
        *           The offset, must be between -840 and 840 inclusive.
        */
       @Override
       public 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;
          }

          if (this.offset != offset)
          {
             checkUndoable(true);
          }

          this.offset = (int) offset;
       }

       /**
        * Sets the internal time and offset. 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.
        * @param    offset
        *           The timezone offset in minutes from UTC.  Must be between
        *           -840 and 840 inclusive.
        */
       public void setTime(NumberType hours,
                           NumberType mins,
                           NumberType secs,
                           NumberType millis,
                           NumberType offset)
       {
          if (hours.isUnknown()  ||
              mins.isUnknown()   ||
              secs.isUnknown()   ||
              millis.isUnknown() ||
              offset.isUnknown())
          {
             setUnknown();
          }
          else
          {
             setTime(hours.longValue(),
                     mins.longValue(),
                     secs.longValue(),
                     millis.longValue(),
                     offset.longValue());
          }
       }

       /**
        * Sets the internal time and offset. 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.
        * @param    offset
        *           The timezone offset in minutes from UTC.  Must be between
        *           -840 and 840 inclusive.
        */
       public void setTime(long hours, long mins, long secs, long millis, long offset)
       {
          // let the superclass set the time value, do this before anything else to
          // allow the error processing to occur first
          super.setTime(hours, mins, secs, millis);

          setTimeZoneOffset(offset);
       }

       /**
        * Creates a SQL compatible string representation of the instance data
        * (the format is "YYYY-MM-DD HH:MM:SS.zzz+HH:MM").
        * <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 time-offset separator

          // add the timezone components
          sb.append((offset < 0) ? '-' : '+');

          sb.append(nf.format(Math.abs(getTzHours()))); // the sign has already been appended
          sb.append(':');
          sb.append(nf.format(Math.abs(getTzMinutes())));

          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 ".".
        * The timezone is also build using standard +/-HH:MM format.
        *
        * <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()
       {
          // return toString(defaultFormat);
          if (isUnknown())
          {
             return "?";
          }

          final StringBuilder sb = new StringBuilder();

          // let the super class handle date and time components
          sb.append(super.toStringMessage());

          // add the timezone components
          sb.append((offset < 0) ? '-' : '+');

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

          sb.append(nf.format(Math.abs(getTzHours()))); // the sign has already been appended
          sb.append(':');
          sb.append(nf.format(Math.abs(getTzMinutes())));

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

       /**
        * Replacement for the default object reading method. The latest state is read from the
        * input source.
        *
        * @param    in
        *           The input source from which fields will be restored.
        *
        * @throws   IOException
        *           In case of I/O errors.
        * @throws   ClassNotFoundException
        *           If payload can't be instantiated.
        */
       @Override
       public void readExternal(ObjectInput in)
       throws IOException,
              ClassNotFoundException
       {
          // let the datetime class handle date and time component
          super.readExternal(in);

          // read the timezone offset component:
          this.offset = 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.
        */
       @Override
       public void writeExternal(ObjectOutput out)
       throws IOException
       {
          // let the datetime class handle date and time component
          super.writeExternal(out);

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

       /**
        * Return an object equal to this expressed in local time (current timezone offset).
        *
        * @return  a datetime that is the representation of this object relative to local time.
        */
       @Override
       public datetime toLocalDatetime()
       {
          if (this.isUnknown())
          {
             return new datetime();
          }

          datetime ret = new datetime();
          ret.deepAssign(this);

          int localOffset = datetimetz.getDefaultTimezoneOffset(ret);
          return plusMillis(ret, (localOffset - offset) * 60 * 1000L);
       }

       /**
        * Obtains the text form of this date instance in ISO 8601 standard format.
        *
        * @return   A string that is the ISO representation of this date
        *           ("YYYY-MM-DDTHH:MM:SS.SSS+HH:MM") or "?" if <code>unknown</code>.
        */
       @Override
       public String getIsoDate()
       {
          if (isUnknown())
          {
             return "?";
          }

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

          StringBuilder sb = new StringBuilder(isoDatetime);

          // add the timezone components
          sb.append((offset < 0) ? '-' : '+');

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

          sb.append(nf.format(Math.abs(getTzHours()))); // the sign has already been appended
          sb.append(':');
          sb.append(nf.format(Math.abs(getTzMinutes())));

          return sb.toString();
       }

       /**
        * Parses this ISO 8601 value as a datetime-tz.
        *
        * @param    val
        *           The date to parse.
        *
        * @throws   DateTimeParseException
        *           If the format is not ISO 8601 compliant.
        */
       @Override
       public void fromIso(String val)
       {
          OffsetDateTime odtz = OffsetDateTime.parse(val, ISO_OFFSET_DATE_TIME);
          Date date = Date.from(Instant.from(odtz));
          date d = new date(date);
          int millis = odtz.get(ChronoField.MILLI_OF_DAY);
          int offset = odtz.get(ChronoField.OFFSET_SECONDS) / 60;
          assign(new datetimetz(d, millis, offset));
       }

       /**
        * 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 datetimetz} 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-tz} representing the respective value, taking into account the current
        *          session settings (like the date format and the windowing year).
        */
       public static datetimetz parseInitial(String text)
       {
          if ("now".equalsIgnoreCase(text))
          {
             return now();
          }

          String df = getDynamicFormat();
          if (df != null)
          {
             return new datetimetz(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 datetimetz(text, DEFAULT_DATE_FORMAT, DEFAULT_DATE_WINDOWING_YEAR);
             }

             // assumed dddd/dd/dd, always in YMD
             return new datetimetz(text, DEFAULT_DATETIME_FORMAT, DEFAULT_DATE_WINDOWING_YEAR);
          }
          // probably an incorrectly formed literal
          return new datetimetz(text);
       }

       /**
        * Computes the local offset (raw + DST) from UTC at a specified date.
        * TODO: remove 'Default' token from name ?
        *
        * @param   d
        *          The date for which the offset is computed. If null, the current JVM date is used.
        *
        * @return  The local offset from UTC (in minutes) at the specified date taking into consideration the
        *          DST at that moment.
        */
       public static int getDefaultTimezoneOffset(date d)
       {
          TimeZone tz = GregorianCalendar.getInstance().getTimeZone();
          // TimeZone tz = date.getDefaultTimeZone();
          Date time = (d == null) ? new Date() : d.dateValue();
          return tz.getOffset(time.getTime()) / (60 * 1000);
       }

       /**
        * Obtains the minutes of timezone offset.
        *
        * @return  The minutes of timezone offset.
        */
       @Override
       public int getTzMinutes()
       {
          return offset % 60;
       }

       /**
        * Obtains the hours of timezone offset.
        *
        * @return  The hours of timezone offset.
        */
       @Override
       public int getTzHours()
       {
          return offset / 60;
       }

       /**
        * Validate specified values and return error information if they don't consist of correct
        * datetime components.
        *
        * @param   month
        *          The month.
        * @param   day
        *          The day.
        * @param   year
        *          The year.
        * @param   forceFourDigits
        *          Flags that the year should be in 4 digits format.
        * @param   hours
        *          The hours.
        * @param   minutes
        *          The minutes.
        * @param   seconds
        *          The seconds.
        * @param   millis
        *          The milliseconds.
        * @param   ampm
        *          AM/PM indicator, or null/empty for 24H representation
        * @param   tz_hours
        *          Timezone hours (can be negative).
        * @param   tz_minutes
        *          Timezone minutes.
        *
        * @return   <code>null</code> if components are valid and error information otherwise.
        */
       public static ErrorEntry validate(int month,
                                         int day,
                                         int year,
                                         boolean forceFourDigits,
                                         int hours,
                                         int minutes,
                                         int seconds,
                                         int millis,
                                         String ampm,
                                         int tz_hours,
                                         int tz_minutes)
       {
          // check the date part using corresponding static function from date:
          ErrorEntry err = date.validate(month, day, year, forceFourDigits);
          if (err != null)
          {
             return err;
          }

          // now all that remained is to check the time parts:
          if (hours < 0 || hours > 23)
          {
             return new ErrorEntry(11260, "Invalid hours in datetime value");
          }
          if (minutes < 0 || minutes > 59)
          {
             return new ErrorEntry(11256, "Invalid minutes in datetime value");
          }
          if (seconds < 0 || seconds > 61) // NOTE: leap seconds are allowed here!
          {
             return new ErrorEntry(11253, "Invalid seconds in datetime value");
          }
          if (millis < 0 || millis > 999)
          {
             return new ErrorEntry(11252, "Invalid milliseconds in datetime value");
          }
          if (ampm != null && !ampm.isEmpty())
          {
             ampm = ampm.toUpperCase();
             if (!"AM".equals(ampm) && !"PM".equals(ampm) && !"A".equals(ampm) && !"P".equals(ampm))
             {
                // TODO: check this out
                return new ErrorEntry(0, "Invalid AM/PM specifier");
             }
             if (hours < 1 || hours > 12)
             {
                return new ErrorEntry(11260, "Invalid hours in datetime value");
             }
          }

          // now all that remained is to check the time parts:
          if (tz_hours < -14 || tz_hours > 14)
          {
             return new ErrorEntry(11259,
                   "Invalid hour offset in the timezone portion of a datetime-tz value");
          }
          if (tz_minutes < 0 || tz_minutes > 59)
          {
             return new ErrorEntry(11254, "Invalid minute offset in datetime-tz value");
          }

          // declare it valid
          return null;
       }

       /**
        * Reinitialize date and time components of this object from the date parameter. The timezone
        * offset remains unchanged.
        * <p>
        * This method is only used from <code>DatetimeTzUserType</code> when restoring an object form
        * database. As it is stored as a compound type, the initialization happens in two atomic
        * steps.
        *
        * @param   date
        *          The date to be used for restoring date and time subcomponents.
        */
       public void setTimestamp(Date date)
       {
          if (date == null)
          {
             setUnknown();
             return;
          }

          // this may not be the best approach but it's fast to implement
          super.deepAssign(new datetime(date));
       }

       /**
        * Converts this object to a Calendar value to be used by Hibernate persistence.
        *
        * @return  Calendar value of this object.
        */
       public Calendar calendarValue()
       {
          if (isUnknown())
          {
             return null;
          }

          TimeZone tz = date.getZoneFromOffset(offset);

          GregorianCalendar ret = new GregorianCalendar(tz);
          ret.setTime(dateValue(tz));

          return ret;
       }

       /**
        * Converts this object to a {@code OffsetDateTime} value to be used by Hibernate persistence.
        *
        * @return  {@code OffsetDateTime} value of this object.
        */
       public OffsetDateTime offsetDateTimeValue()
       {
          Calendar cal = calendarValue();
          if (cal == null)
          {
             return null;
          }
          return OffsetDateTime.ofInstant(cal.toInstant(), cal.getTimeZone().toZoneId());
       }

       /**
        * 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 ContextIndependentDateTimeTz(isUnknown(), this.longValue(), this.getTime(), offset);
       }

       /**
        * Initialized this object using the provided arguments.
        *
        * @param    d
        *           The date from which to copy.  This must be an instance of the {@code date} superclass and may
        *           NOT be an instance of the {@code datetime} or {@code datetimetz} since that is a 4GL error.
        * @param    time
        *           The milliseconds since midnight.
        * @param    offset
        *           The timezone offset from UTC in minutes. Must be between -840 and 840 inclusive.
        */
       private void initialize(date d, NumberType time, NumberType offset)
       {
          if (d.isUnknown() || time.isUnknown() || offset.isUnknown())
          {
             setUnknown();
             return;
          }

          if (d instanceof datetime)
          {
             ErrorManager.recordOrThrowError(223, "", "");
             // ** Incompatible data types in expression or assignment. (223)
             return;
          }

          setDayNumber(d.intValue());
          setTime(time.intValue());
          setTimeZoneOffset(offset.intValue());
       }

       /**
        * Initialized this object using the provided arguments.
        *
        * @param    d
        *           The date (or date subclass) from which to copy.
        * @param    timeOrOffset
        *           The milliseconds since midnight if the first parameter is a {@code date} or a timezone offset
        *           from UTC in minutes if the first parameter is a {@code datetime}. As an offset, the value must
        *           be between -840 and 840.
        */
       private void initialize(date d, NumberType timeOrOffset)
       {
          if (d.isUnknown() || timeOrOffset.isUnknown())
          {
             setUnknown();
             return;
          }

          if (d instanceof datetimetz)
          {
             assign(new datetimetz(d).toOffset(timeOrOffset.intValue()));
          }
          else if (d instanceof datetime)
          {
             assign(d);
             offset = timeOrOffset.intValue();
          }
          else
          {
             // this path is for the date superclass
             super.deepAssign(d);
             setTime(timeOrOffset.longValue());
             offset = datetimetz.getDefaultTimezoneOffset(this);
          }
       }

       /**
        * Reset the time and offset for this instance.
        */
       @Override
       protected void resetTime()
       {
          resetTime(null);
       }

       /**
        * Reset the time and offset for this instance.
        */
       protected void resetTime(Date d)
       {
          super.resetTime();

          this.offset = getDefaultOffset(d);
       }

       /**
        * Private utility method that returns a Calendar value as string in ISO format
        * (yyyy-MM-dd hh:mm:ss.ttt).
        *
        * @param   c
        *          The calendar.
        *
        * @return  The ISO string representing the calendar instance.
        */
       private static String toString(Calendar c)
       {
          if (c == null)
          {
             return "";
          }
          return isoFormat(
             c.get(Calendar.YEAR),
             c.get(Calendar.MONTH) + 1,
             c.get(Calendar.DAY_OF_MONTH),
             c.get(Calendar.HOUR_OF_DAY),
             c.get(Calendar.MINUTE),
             c.get(Calendar.SECOND),
             c.get(Calendar.MILLISECOND),
             (c.get(Calendar.DST_OFFSET) + c.get(Calendar.ZONE_OFFSET)) / (1000 * 60)
          );
       }

       /**
        * Private utility method that returns a date (by components) as string in ISO format
        * (yyyy-MM-dd hh:mm:ss.ttt).
        *
        * @param   y
        *          the year (4 digits)
        * @param   M
        *          the month (1-12)
        * @param   d
        *          the day (1-31)
        * @param   h
        *          the hour of the day (0-23)
        * @param   m
        *          the minute of the hour (0-59)
        * @param   s
        *          the second (0-59)
        * @param   t
        *          the milliseconds (0-999)
        * @param   z
        *          the time offset from UTC (in minutes, -840 - 840)
        *
        * @return  a string representation of the date in ISO format.
        */
       private static String isoFormat(int y, int M, int d, int h, int m, int s, int t, int z) {
          NumberFormat nf = NumberFormat.getInstance();
          nf.setGroupingUsed(false);

          StringBuilder sb = new StringBuilder();
          nf.setMinimumIntegerDigits(4);
          // date:
          sb.append(nf.format(y));
          sb.append('-');
          nf.setMinimumIntegerDigits(2);
          sb.append(nf.format(M));
          sb.append('-');
          sb.append(nf.format(d));
          sb.append('T');
          // time:
          sb.append(nf.format(h));
          sb.append(':');
          sb.append(nf.format(m));
          sb.append(':');
          sb.append(nf.format(s));
          sb.append('.');
          nf.setMinimumIntegerDigits(3);
          sb.append(nf.format(t));
          // tz offset
          nf.setMinimumIntegerDigits(2);
          sb.append(z >= 0 ? '+' : '-');
          z = Math.abs(z);
          sb.append(nf.format(z / 60));
          sb.append(':');
          sb.append(nf.format(z % 60));

          return sb.toString();
       }

       /**
        * Convenient class that wraps the low-level {@link #julian}, {@link #time} and {@link #offset}
        * parameters to avoid pinning the whole context-local of the datetime-tz.
        */
       public static class ContextIndependentDateTimeTz
       extends ContextIndependentDateTime
       {
          /** Time zone offset in minutes from Coordinated Universal Time (UTC). */
          int offset;

          /**
           * 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.
           * @param   offset
           *          Time zone offset in minutes from Coordinated Universal Time (UTC).
           */
          public ContextIndependentDateTimeTz(boolean unknown, long julian, int time, int offset)
          {
             super(unknown, julian, time);
             this.offset = offset;
          }

          /**
           * Implementation of equals.
           */
          @Override
          public boolean equals(Object other)
          {
             if (this == other)
             {
                return true;
             }

             if (other instanceof ContextIndependentDateTimeTz)
             {
                ContextIndependentDateTimeTz that = (ContextIndependentDateTimeTz) other;

                boolean u1 = this.isUnknown();
                boolean u2 = that.isUnknown();

                if (u1 && u2)
                {
                   return true;
                }

                if (u1 || u2)
                {
                   return false;
                }

                long thisAbsTime = this.julian * date.MILLIS_PER_DAY +
                      this.time - this.offset * 60 * 1000L;
                long thatAbsTime = that.julian * date.MILLIS_PER_DAY +
                      that.time - that.offset * 60 * 1000L;
                return thisAbsTime == thatAbsTime;
             }

             return super.equals(other);
          }

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

             return (int) (37 * result + this.julian * date.MILLIS_PER_DAY +
                   this.time - this.offset * 60 * 1000);
          }
       }

       /**
        * Sample testing.
        * <p>
        * Displays the current date/time from various sources (date OS command, java
        * {@link java.util.Date}, java {@link java.util.GregorianCalendar},
        * P2J {@link com.goldencode.p2j.util.date#today()},
        * P2J {@link com.goldencode.p2j.util.datetime#now()},
        * P2J {@link com.goldencode.p2j.util.datetimetz#now()} that must be compared by hand.
        * <p>
        * Builds a lots of {@code datetime-tz} instances (built with {@link #fromLiteral(String)})
        * from a small database of sensible data instances and compare the string representation of
        * different outputs:
        * <ol>
        *    <li>{@link #toStringExport()} - MESSAGE P4GL output
        *    <li>{@link #getIsoDate()} - output of ISO-DATE P4GL function
        *    <li>{@link #calendarValue()} - Calendar export of internal value of P2J datatype to Java
        * </ol>
        * The summary is displayed.
        *
        * @param   args
        *          ignored.
        */
       public static void main(String[] args)
       {
          date.setSessionTimeZone(+8 * 60);
          date.setDateOrder("ymd"); // try to match the ISO pattern

          Date java_date = new Date();
          date today = date.today();
          datetime dt_now = datetime.now();
          datetimetz dtz_now = datetimetz.now();

          System.out.print("OS date command:     ");
          try
          {
             Process proc = Runtime.getRuntime().exec("date -Ins"); // -Iseconds
             while (true) {
                int c = proc.getInputStream().read();
                if (c == -1)
                {
                   break;
                }
                System.out.write((char)c);
             }
             proc.waitFor();
          }
          catch (Exception e)
          {
             System.out.println("'date' OS command failed");
          }
          System.out.println("Java date:           " +java_date);
          System.out.println("Java date ISO:       " +
                                   new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ").format(java_date));
          System.out.println("GregorianCalendar is " + toString(new GregorianCalendar()));
          System.out.println("TODAY is             " + today.getIsoDate());
          System.out.println("NOW (datetime) is    " + dt_now.getIsoDate());
          System.out.println("NOW (datetime-tz) is " + dtz_now.getIsoDate());

    /*
    //      String strDtz = "2013-05-29T10:10:10.100+08:30";
          String strDtz = "1582-10-05T00:00:00.000-05:00";
          System.out.println(strDtz);

          datetimetz dtz1 = datetimetz.fromLiteral(strDtz);
          System.out.println(dtz1.toStringExport().replace('/', '-'));

          System.out.println(dtz1.getIsoDate());

          Calendar cal = dtz1.calendarValue();
          System.out.println(datetimetz.toString(cal));

          System.out.println();

          strDtz = "1582-10-15T00:00:00.000-05:00";
          System.out.println(strDtz);

          dtz1 = datetimetz.fromLiteral(strDtz);
          System.out.println(dtz1.toStringExport().replace('/', '-'));

          System.out.println(dtz1.getIsoDate());

          cal = dtz1.calendarValue();
          System.out.println(datetimetz.toString(cal));
    */

          final int[] years   = { /*-4716, -100, -99, -1,*/ 1, 1582, /*1918,*/ 1949, 1969, 1970, 1999, 2000, /*2001,*/ 2015, 2049, 2050, 4716 };
          final int[] months  = { 1, 3, 4, 10, 12};
          final int[] days    = { 1, 5, /*13, 14,*/ 15, 30, 31, 26, 27, 28, 30, 31 };

          final int[] hours   = { 0, 1, 3, 4, 11, 12, 13, 22, 23 };
          final int[] minutes = { 0, 1, 2, 30, 58, 59 };
          final int[] seconds = { 0, 1, 2, 30, 58, 59 };
          final int[] millis  = { 0, 1, 500, 999 };

          final int[] tzOff = { -14 * 60,
                           -5 * 60,
                           -3 * 60 - 30,
                           -1 * 60,
                            0,
                            1 * 60,
                            2 * 60,
                            5 * 60 + 45,
                           14 * 60};

          long t0 = System.currentTimeMillis();
          final int[] cnt = {0};
          final int[] errs = {0};
          final int[] threadCnt = {0};
          final Object lock = new Object();
          for (final int tz : tzOff) {
             synchronized (lock) {
                threadCnt[0]++;
             }
             new Thread(new Runnable()
             {
                @Override
                public void run()
                {
                   date.setDateOrder("ymd"); // try to match the ISO pattern

                   int lcnt = 0;
                   int lerrs = 0;

                   for (int hour : hours)
                   {
                      if (tz == tzOff[0]) {
                         System.out.print("*");
                      }
                      for (int milli : millis)
                      {
                         for (int sec : seconds)
                         {
                            for (int min : minutes)
                            {
                               for (int month : months)
                               {
                                  for (int day : days)
                                  {
                                     if (day == 31 && month == 4)
                                     {
                                        // date does not exist
                                        continue;
                                     }
                                     for (int year : years)
                                     {
                                        if (year == 1582 && month == 10 && (day > 4 || day < 15))
                                        {
                                           // date does not exist
                                           continue;
                                        }
                                        lcnt++;
                                        String strDtz = isoFormat(year, month, day,
                                                                  hour, min, sec, milli,
                                                                  tz);
                                        // "2013-05-29T10:10:10.100+08:30";

                                        datetimetz dtz1 = datetimetz.fromLiteral(strDtz);
                                        if (dtz1 == null)
                                        {
                                           lerrs++;
                                           System.out.println(lcnt + " 0 ");
                                           continue;
                                        }
                                        String t1 = dtz1.toStringExport()
                                              .replace('/', '-')
                                              .replace(' ', 'T');
                                        if (!t1.equals(strDtz))
                                        {
                                           if (!strDtz.equals("0" + t1))
                                           {
                                              // this is a workaround for three digits years (1-999)
                                              lerrs++;
                                              System.out.println(lcnt + " 1 " + t1 + " " + strDtz);
                                           }
                                        }

                                        String t2 = dtz1.getIsoDate();
                                        if (!t2.equals(strDtz))
                                        {
                                           lerrs++;
                                           System.out.println(lcnt + " 2 " + t2 + " " + strDtz);
                                        }

                                        Calendar cal = dtz1.calendarValue();
                                        String t3 = datetimetz.toString(cal);
                                        if (!t3.equals(strDtz))
                                        {
                                           lerrs++;
                                           System.out.println(lcnt + " 3 " + t3 + " " + strDtz);
                                        }
                                     }
                                  }
                               }
                            }
                         }
                      }
                   }
                   if (tz == tzOff[0])
                   {
                      System.out.println("*");
                   }

                   synchronized (lock) {
                      threadCnt[0]--;
                      cnt[0] += lcnt;
                      errs[0] += lerrs;
                      lock.notifyAll();
                   }
                }
             }).start();
          }

          for (;;) {
             synchronized (lock) {
                if (threadCnt[0] == 0) {
                   break;
                }
                try
                {
                   lock.wait();
                }
                catch (InterruptedException e)
                {
                   LOG.severe("", e);
                }
             }
          }

          System.out.println(errs[0] + " errors of " + cnt[0] +
                             " in tests " + (System.currentTimeMillis() - t0) / 1000.0 + "secs.");
       }
    }