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