datetime.java

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

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

package com.goldencode.p2j.util;

import java.io.*;
import java.text.*;
import java.time.*;
import java.time.chrono.*;
import java.time.format.*;
import java.time.temporal.*;
import java.util.*;
import java.util.regex.*;

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

/**
 * A class that represents a Progress 4GL compatible datetime object whose
 * data is mutable.  All Progress language features related to date data
 * types are supported including all function that can be accessed via the
 * date aware operators and built-in Progress date-related functions.
 * <p>
 * This class extends the <code>date</code> class so all methods should work
 * directly. If they don't they will be overridden. Also should the following
 * static implementations:
 * <p>
 * <pre>
 * +        operator (datetime, integer)              {@link #plusMillis(datetime, NumberType)} 
 * -        operator (datetime, integer)              {@link #minusMillis(datetime, NumberType)}
 * -        operator (datetime, datetime)             {@link #differenceNum(datetime, datetime)}
 * string   function (default format)                 {@link #toString()}
 *                   (user-defined format)            {@link #toString(String)} 
 *                   (export format)                  {@link #toStringExport()}
 * now      function                                  {@link #now()}
 * datetime function (date)                           {@link #datetime(date)}
 * datetime function (date, integer)                  {@link #datetime(date, NumberType)}
 * datetime function (integer m, d, y, h, mm, s, ms)  {@link #datetime(NumberType, NumberType, NumberType, NumberType, NumberType, NumberType, NumberType)}
 * datetime function (string parameter)               {@link #datetime(String)}
 *                   [literal]                        {@link #fromLiteral(String)}
 * </pre>
 * <p>
 * Default data type display formats: DATETIME 99/99/9999 HH:MM:SS.SSS
 *
 * @author    OM
 */
public class datetime
extends date
{
   /** Represents the hours component of the string formatted datetime(-tz). */
   public static final byte HOURS = 3;

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

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

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

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

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

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

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

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

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

   /**
    * Default constructor which creates an unknown instance.
    */
   public datetime()
   {
      super(); // the default constructor of date will set the object to unknown  
   }
   
   /**
    * This is a special c'tor which should be used only when converting the
    * value returned by a function or method with polymorphic return type into the
    * expected type (i.e. DYNAMIC-FUNCTION()).  In such cases, the 4GL does some
    * automatic type conversion (see {@link #assign(BaseDataType)}).
    * 
    * @param    value
    *           The value to be used for this instance.
    */
   public datetime(BaseDataType value)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      value = value == null ? null : value.val();
      if (value == null || value.isUnknown())
      {
         setUnknown();
      }
      else
      {
         if (value instanceof date)
         {
            assign(value);
         } 
         else if (value instanceof Text)
         {
            String strVal = ((Text) value).getValue();
            
            // make a first attempt to parse the string as ISO literal, save as backup
            datetime   isoDt = datetime.fromLiteral(strVal);
            datetime[] tmp   = new datetime[1];
            
            Runnable code = () -> { tmp[0] = new datetime(strVal); };
            
            // if ISO parsing was successful, ignore further errors otherwise we don't have the
            // backup value so the ErrorManager will handle the bad syntax of date-format
            try
            {
               if (isoDt != null)
               {
                  ErrorManager.ignore(code);
               }
               else
               {
                  code.run();
               }
            }
            finally
            {
               if (tmp[0] != null && tmp[0].isUnknown() && isoDt == null)
               {
                  int[] errCodes = { 5729, 5678 };
                  String[] errMsgs = {
                     "Incompatible datatypes found during runtime conversion", 
                     "Unable to do run-time conversion of datatypes"
                  };
                  ErrorManager.recordOrShowError(errCodes, errMsgs, false, false);
               }
            }
            
            // at this moment either the  P4GL date-format parsing was successful or it failed but
            // we have a isoDt as backup (that put the ErrorManager into ignore state)
            if (!tmp[0].isUnknown())
            {
               // P4GL date-format parsing was successful use this value
               assign(tmp[0]);
            }
            else
            {
               assign(isoDt);
            }
         }
         else if (value instanceof jobject)
         {
            assign((jobject<?>) value);
         }
         else
         {
            incompatibleTypesOnConversion();
         }
      }
   }
   
   /**
    * Creates an instance from a <code>java.util.Date</code> instance.
    * <p>
    * <b>WARNING: this will only work if the calendar in which this datetime
    * was constructed is the <code>GregorianCalendar</code> AND if the
    * timezone in which this object was created is the same as the current
    * default for this JVM instance.</b>  If this is not correct then you
    * must use the {@link com.goldencode.p2j.util.datetime#datetime(Date,TimeZone)} version.
    *
    * @param    d
    *           The <code>Date</code> instance from which to pattern the instance.
    */
   public datetime(Date d)
   {
       this(d, TimeZone.getDefault());
   }
   
   /**
    * Creates an instance from a <code>java.util.Date</code> instance.
    *
    * @param    d
    *           The <code>Date</code> instance from which to pattern the instance.
    * @param    zone
    *           The <code>TimeZone</code> for the calendar to be used for <code>d</code>.
    */
   public datetime(Date d, TimeZone zone)
   {
       long millis = dateToLocalMillis(d, zone);
       long julian = millisToJulianDay(millis);
       long remain = millis - julianDayToMillis(julian);
       
       instantiateFromDateTime(new date(julian), (int) remain);
   }
   
   /**
    * Constructs an instance with the exact same state as the passed
    * instance.  This can be used both as a copy constructor as well as an
    * implementation of the DATETIME() built-in function when called with a
    * <code>date</code> subclass.
    *
    * @param    d
    *           The instance from which to copy state.
    */
   public datetime(date d)
   {
      assign(d);
   }

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

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

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

   /**
    * Construct a datetime instance using explicit values for each discrete
    * component.  This is a variant of the DATETIME() built-in function.
    * 
    * @param    month
    *           The month number from 1 to 12 inclusive.
    * @param    day
    *           The day of the month from 1 to the maximum days in that month,
    *           inclusive
    * @param    year
    *           The Progress 4GL year which may be a negative number which
    *           represents the BC era or a positive AD era year. The value must
    *           be between -32768 and 32767 and it cannot be 0 since the year
    *           0 is invalid in Progress. The value will be windowed into the
    *           right century if it is 2 digits.  It will be used unchanged if
    *           it is 3 or more digits.
    * @param    hours
    *           The hour of the day from 0 to 23 inclusive.
    * @param    minutes
    *           The minute of the hour from 0 to 59 inclusive.
    * @param    seconds
    *           The seconds of the minute from 0 to 62 inclusive. This accounts
    *           for leap seconds.  Please note that Progress documents 61 as
    *           the limit. It turns out that 62 is the real limit.
    * @param    millis
    *           The milliseconds of the second from 0 to 999 inclusive.
    */
   public datetime(double month,
                   double day,
                   double year,
                   double hours,
                   double minutes,
                   double seconds,
                   double millis)
   {
      // the super class will take care of the date part
      super(month, day, year);

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

   /**
    * Construct a datetime instance using explicit values for each discrete
    * component.  This is a variant of the DATETIME() built-in function.
    * 
    * @param    month
    *           The month number from 1 to 12 inclusive.
    * @param    day
    *           The day of the month from 1 to the maximum days in that month,
    *           inclusive
    * @param    year
    *           The Progress 4GL year which may be a negative number which
    *           represents the BC era or a positive AD era year. The value must
    *           be between -32768 and 32767 and it cannot be 0 since the year
    *           0 is invalid in Progress. The value will be windowed into the
    *           right century if it is 2 digits.  It will be used unchanged if
    *           it is 3 or more digits.
    * @param    hours
    *           The hour of the day from 0 to 23 inclusive.
    * @param    minutes
    *           The minute of the hour from 0 to 59 inclusive.
    * @param    seconds
    *           The seconds of the minute from 0 to 62 inclusive. This accounts
    *           for leap seconds.  Please note that Progress documents 61 as
    *           the limit. It turns out that 62 is the real limit.
    */
   public datetime(double month,
                   double day,
                   double year,
                   double hours,
                   double minutes,
                   double seconds)
   {
      // the super class will take care of the date part
      super(month, day, year);

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

   /**
    * Construct a datetime instance using explicit values for each discrete
    * component.  This is a variant of the DATETIME() built-in function.
    * 
    * @param    month
    *           The month number from 1 to 12 inclusive.
    * @param    day
    *           The day of the month from 1 to the maximum days in that month,
    *           inclusive
    * @param    year
    *           The Progress 4GL year which may be a negative number which
    *           represents the BC era or a positive AD era year. The value must
    *           be between -32768 and 32767 and it cannot be 0 since the year
    *           0 is invalid in Progress. The value will be windowed into the
    *           right century if it is 2 digits.  It will be used unchanged if
    *           it is 3 or more digits.
    * @param    hours
    *           The hour of the day from 0 to 23 inclusive.
    * @param    minutes
    *           The minute of the hour from 0 to 59 inclusive.
    */
   public datetime(double month,
                   double day,
                   double year,
                   double hours,
                   double minutes)
   {
      // the super class will take care of the date part
      super(month, day, year);

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

   /**
    * Construct a datetime instance using explicit values for each discrete
    * component of the date and with a time value of 0.
    * 
    * @param    month
    *           The month number from 1 to 12 inclusive.
    * @param    day
    *           The day of the month from 1 to the maximum days in that month,
    *           inclusive
    * @param    year
    *           The Progress 4GL year which may be a negative number which
    *           represents the BC era or a positive AD era year. The value must
    *           be between -32768 and 32767 and it cannot be 0 since the year
    *           0 is invalid in Progress. The value will be windowed into the
    *           right century if it is 2 digits.  It will be used unchanged if
    *           it is 3 or more digits.
    */
   public datetime(double month, double day, double year)
   {
      // the super class will take care of the date part
      super(month, day, year);

      setTime(0, 0, 0, 0);
   }
   
   /**
    * Construct a datetime instance using explicit values for each discrete
    * component.  This is a variant of the DATETIME() built-in function. Any
    * component that is <code>unknown</code> will result in the instance being
    * <code>unknown</code>.
    * 
    * @param    month
    *           The month number from 1 to 12 inclusive.
    * @param    day
    *           The day of the month from 1 to the maximum days in that month,
    *           inclusive
    * @param    year
    *           The Progress 4GL year which may be a negative number which
    *           represents the BC era or a positive AD era year. The value must
    *           be between -32768 and 32767 and it cannot be 0 since the year
    *           0 is invalid in Progress. The value will be windowed into the
    *           right century if it is 2 digits.  It will be used unchanged if
    *           it is 3 or more digits.
    * @param    hours
    *           The hour of the day from 0 to 23 inclusive.
    * @param    minutes
    *           The minute of the hour from 0 to 59 inclusive.
    * @param    seconds
    *           The seconds of the minute from 0 to 62 inclusive. This accounts
    *           for leap seconds.  Please note that Progress documents 61 as
    *           the limit. It turns out that 62 is the real limit.
    * @param    millis
    *           The milliseconds of the second from 0 to 999 inclusive.
    */
   public datetime(NumberType month,
                   NumberType day,
                   NumberType year,
                   NumberType hours,
                   NumberType minutes,
                   NumberType seconds,
                   NumberType millis)
   {
      // the super class will take care of the date part
      super(month, day, year);

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

   /**
    * Construct a datetime instance using explicit values for each discrete
    * component.  This is a variant of the DATETIME() built-in function. Any
    * component that is <code>unknown</code> will result in the instance being
    * <code>unknown</code>.
    * 
    * @param    month
    *           The month number from 1 to 12 inclusive.
    * @param    day
    *           The day of the month from 1 to the maximum days in that month,
    *           inclusive
    * @param    year
    *           The Progress 4GL year which may be a negative number which
    *           represents the BC era or a positive AD era year. The value must
    *           be between -32768 and 32767 and it cannot be 0 since the year
    *           0 is invalid in Progress. The value will be windowed into the
    *           right century if it is 2 digits.  It will be used unchanged if
    *           it is 3 or more digits.
    * @param    hours
    *           The hour of the day from 0 to 23 inclusive.
    * @param    minutes
    *           The minute of the hour from 0 to 59 inclusive.
    * @param    seconds
    *           The seconds of the minute from 0 to 62 inclusive. This accounts
    *           for leap seconds.  Please note that Progress documents 61 as
    *           the limit. It turns out that 62 is the real limit.
    */
   public datetime(NumberType month,
                   NumberType day,
                   NumberType year,
                   NumberType hours,
                   NumberType minutes,
                   NumberType seconds)
   {
      // the super class will take care of the date part
      super(month, day, year);
      
      // use the version of setTime that handles unknown properly
      setTime(hours, minutes, seconds, int64.of(0L));
   }

   /**
    * Construct a datetime instance using explicit values for each discrete
    * component.  This is a variant of the DATETIME() built-in function. Any
    * component that is <code>unknown</code> will result in the instance being
    * <code>unknown</code>.
    * 
    * @param    month
    *           The month number from 1 to 12 inclusive.
    * @param    day
    *           The day of the month from 1 to the maximum days in that month,
    *           inclusive
    * @param    year
    *           The Progress 4GL year which may be a negative number which
    *           represents the BC era or a positive AD era year. The value must
    *           be between -32768 and 32767 and it cannot be 0 since the year
    *           0 is invalid in Progress. The value will be windowed into the
    *           right century if it is 2 digits.  It will be used unchanged if
    *           it is 3 or more digits.
    * @param    hours
    *           The hour of the day from 0 to 23 inclusive.
    * @param    minutes
    *           The minute of the hour from 0 to 59 inclusive.
    */
   public datetime(NumberType month,
                   NumberType day,
                   NumberType year,
                   NumberType hours,
                   NumberType minutes)
   {
      // the super class will take care of the date part
      super(month, day, year);

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

   /**
    * Construct a datetime instance using explicit values for each discrete
    * component of the date and with a time value of 0. Any <code>unknown</code>
    * input will result in an instance that is <code>unknown</code>.
    * 
    * @param    month
    *           The month number from 1 to 12 inclusive.
    * @param    day
    *           The day of the month from 1 to the maximum days in that month,
    *           inclusive
    * @param    year
    *           The Progress 4GL year which may be a negative number which
    *           represents the BC era or a positive AD era year. The value must
    *           be between -32768 and 32767 and it cannot be 0 since the year
    *           0 is invalid in Progress. The value will be windowed into the
    *           right century if it is 2 digits.  It will be used unchanged if
    *           it is 3 or more digits.
    */
   public datetime(NumberType month, NumberType day, NumberType year)
   {
      // the super class will take care of the date part
      super(month, day, year);

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

   /**
    * Creates a new instance from the given date and time specification. This
    * is one form of the DATETIME built-in function. If the input is
    * <code>unknown</code> the instance will be <code>unknown</code>.
    * <p>
    * The specification must match the following format rules:
    * <p>
    * <ol>
    *   <li> Date value (this is not optional).  The string cannot start with
    *        whitespace. The first input is a date value that has at least a
    *        month and day specified or a month, day and year. The values
    *        are parsed in order as specified in SESSION:DATE-FORMAT. Zero
    *        padding is allowed but not required.
    *   <li> Date and time delimiter (not optional). Whitespace or a letter T
    *        (upper or lowercase) which is followed by whitespace. If the letter
    *        T is used, it cannot be preceded by whitespace. No other characters
    *        are allowed.
    *   <li> Time value (optional). If not specified, it is set to 0. It is
    *        specified in HH:MM:SS.sss format, however right side components
    *        can be left off (so HH, HH:MM, HH:MM:SS and HH:MM:SS.sss are all
    *        valid forms). Zero padding is allowed but not required, so hours
    *        can be H or HH, minutes M or MM, seconds S or SS and milliseconds
    *        can be s, ss or sss. Delimiters can only be there if the following
    *        component is included.
    *   <li> Whitespace (optional).
    *   <li> Timezone specification (optional). This is in the form of a sign
    *        (+ or -) then the hours (1 or 2 digits) a colon delimiter and then
    *        minutes (1 or 2 digits). Zero padding is allowed but not required.
    *        The minutes can be left off but if so, then the delimiter must
    *        also be left off.  If specified, the instance will be set using
    *        the timezone specified EVEN THOUGH THE TIMEZONE OFFSET IS NOT
    *        STORED (since this is not a datetime-tz).
    *   <li> Whitespace (optional).
    * </ol>
    *
    * @param    spec
    *           The date and time specification.
    */
   public datetime(character spec)
   throws ErrorConditionException
   {
      if (spec.val().isUnknown())
      {
         setUnknown();
      }
      else
      {
         instantiateFromStringWorker(spec.val().toStringMessage(), null/*auto*/, -1);
      }
   }
   
   /**
    * Parses this ISO 8601 value as a datetime.
    * <p>
    * This API relies on {@link #instantiateFromStringWorker} to parse the literal.
    *  
    * @param    val
    *           The date to parse.
    *           
    * @return   The date value.
    */
   public static date parseLiteral(String val)
   {
      datetime d = new datetime();
      d.instantiateFromStringWorker(val, DEFAULT_DATETIME_FORMAT, -1);
      return d;
   }
   
   /**
    * Creates an instance that represents current date and time using the default timezone.
    * This is the equivalent of the NOW built-in function.
    */
   public static datetime now()
   {
      TimeZone sessionTz = date.getDefaultTimeZone();
      GregorianCalendar now = getZoneCalendar(sessionTz);
      
      return new datetime(date.today(), datetime.millisSinceMidnight(now));
   }
   
   /**
    * Parse the INITIAL string of a datetime var definition.
    * 
    * @param    literal
    *           The datetime literal, which will always have the date part in 'mdy' format.
    * 
    * @return   The datetime instance initialized with this literal.
    */
   public static datetime init(String literal)
   {
      return datetime.parseInitial(literal);
   }
   
   /**
    * Check if the <code>str</code> is a Progress datetime-tz literal (in ISO8601 format string 
    * representation), parse it and return the <code>datetimetz</code> object.
    * The recognized formats are:
    * <pre>
    *    YYYY-MM-ddTHH:mm[:S[S].s[s[s[...]]]]
    * </pre>
    * where the date-separator can be either 't' or 'T'. The hours and minutes are mandatory in
    * 2 digits and seconds can be represented as 1 or 2 digits. The seconds parts can be tenths 
    * of seconds (one digit), hundredths (2 digits) and milliseconds (3 digits after '.'). 
    * If more digits appear after '.' they are ignored.   
    * 
    * Normally the timezone should be present, otherwise P2J parser wouldn't classified it as
    * a datetime-tz literal. 
    *  
    * @param   str
    *          The text to be parsed.
    * 
    * @return  a datetime-tz object whose string literal representation was given as argument on 
    *          success or null if the passed argument does not fit the known pattern. 
    */
   public static datetime fromLiteral(String str)
   {
      Matcher matcher = DT_PATTERN.matcher(str);
      if (!matcher.matches())
      {
         // failed, spec is not in ISO8601 format
         return null;
      }
      
      int year = 0;
      int month = 0;
      int day = 0;
      int hour = 0;
      int minutes = 0;
      int seconds = 0;
      int millis = 0;

      // parse captured tokens
      try 
      {
         // mandatory parts:
         year = Integer.parseInt(matcher.group(1));
         month = Integer.parseInt(matcher.group(2));
         day = Integer.parseInt(matcher.group(3));
         hour = Integer.parseInt(matcher.group(4));
         minutes = Integer.parseInt(matcher.group(5));
         
         // optional components:
         if (matcher.group(7) != null)
         {
            seconds = Integer.parseInt(matcher.group(7));
         }
         if (matcher.group(8) != null)
         {
            String milliDigits = matcher.group(8).substring(1); // skip the .;
            if (milliDigits.length() > 3)
            {
               // more than 3 digits are allowed for milliseconds, 
               // but only the first 3 digits are used, the other are dropped 
               milliDigits = milliDigits.substring(0, 3);
            }
            millis = Integer.parseInt(milliDigits);
            if (milliDigits.length() == 1)
            {
               // if only one digit is used for millis they are tenths instead 
               millis *= 100;
            }
            else if (milliDigits.length() == 2)
            {
               // if only two digits are used for millis they are hundredths instead 
               millis *= 10;
            }
         }
      }
      catch (NumberFormatException e)
      {
         // this should not happen, anyway since groups should be made of digits only
         // if cannot parse than it's not in the correct format
         return null;
      }
      
      datetime ret = new datetime(); 
      long julian = ret.instantiateDate(year, month, day);
      if (julian == INVALID_DATE)
      {
         return null;
      }
      ret.setDayNumber(julian);
      ret.setTime(hour, minutes, seconds, millis);
      
      // set parsed groups to internal data of the object
      return ret;
   }
   
   /**
    * Get the type
    * @return type
    */
   public Type getType()
   {
      return Type.DATETIME;
   }

   /**
    * Returns the number of milliseconds difference between the datetimes of two instances.
    *
    * @param    op1
    *           The left operand.
    * @param    op2
    *           The right operand (instance to subtract from the left operand).
    *
    * @return   The difference in days between the two datetimes or 
    *           <code>unknown value</code> if any input is <code>unknown value</code>.
    */
   public static int64 differenceNum(datetime op1, datetime op2)
   {
      if (op1.isUnknown() || op2.isUnknown())
      {
         return new int64();
      }
      
      return new int64(op1.getAbsoluteTimeOffset() - op2.getAbsoluteTimeOffset());
   }
   
   /**
    * Obtain the millis since midnight for this instance. This is an 
    * implementation of the <code>TIME()</code> built-in function.
    * 
    * @return   The milliseconds since midnight.
    */
   public int64 getTimeNum()
   {
      return isUnknown() ? new int64() : new int64(time);
   }

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

   /**
    * Sets the millis from midnight.
    * 
    * @param   millis
    *          Milliseconds passed from midnight.
    */
   public void setTime(NumberType millis)
   {
      if (millis.isUnknown())
      {
         setUnknown();
      }
      else 
      {
         setTime(millis.longValue());
      }
   }
   
   /**
    * Sets the milliseconds since midnight. If millis is negative a previous day is computed.
    * If millis &gt; date.MILLIS_PER_DAY, the extra milliseconds are combined in days in the future. 
    * 
    * @param    millis
    *           Milliseconds since midnight.
    */
   public void setTime(long millis)
   {
      // Progress warps the amount of time in 32bit: 
      int newMillis = (int) millis;
      long days = longValue();
      
      if (newMillis < 0)
      {
         // must go back in time!
         long daysOff = newMillis / date.MILLIS_PER_DAY;
         newMillis   %= date.MILLIS_PER_DAY;
         if (newMillis != 0)
         {
            newMillis += date.MILLIS_PER_DAY;
            daysOff--;
         }
         
         days += daysOff;
      }
      else if (newMillis >= date.MILLIS_PER_DAY)
      {
         // go forward in time
         days += newMillis / date.MILLIS_PER_DAY;
         newMillis %= date.MILLIS_PER_DAY;
      }
      else 
      {
         if (time != newMillis)
         {
            checkUndoable(true);
         }

         // this is in fact the basic case when the day does not need adjustments
         this.time = newMillis;
         return;
      }
      
      if (time != newMillis)
      {
         checkUndoable(true);
      }

      // put-back the obtained values
      setDayNumber(days);
      this.time = newMillis;
   }
   
   /**
    * Extract the hours time-element from this datetime.
    * 
    * @return   The hour of this datetime object.
    */
   public int getHours()
   {
      return time / (1000 * 60 * 60);
   }
   
   /**
    * Extract the hours time-element from this datetime.
    * 
    * @return  the hour of this datetime object or unknown value if this
    *          is an unknown object.
    */
   public int64 getHoursNum()
   {
      return isUnknown() ? new int64() : new int64(getHours());
   }
   
   /**
    * Extract the minutes time-element from this datetime.
    * 
    * @return the minute of this datetime object
    */
   public int getMinutes()
   {
      int timeInMinutes = time / (1000 * 60);
      return timeInMinutes % 60;
   }

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

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

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

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

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

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

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

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

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

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

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

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

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

      int mtime = (int) (millis + 1000 * (secs + 60 * (mins + 60 * hours)));
      
      if (time != mtime)
      {
         checkUndoable(true);
      }
      
      time = mtime;
   }

   /**
    * TODO: perhaps this should be moved to DateOps
    * Adds <code>milliseconds</code> to the datetime of the left operand and 
    * returns a new instance representing that datetime.
    *
    * @param   dt
    *          The left operand.
    * @param   millis
    *          The right operand (the number of milliseconds to add. 
    *          Note that this can be a negative number to effectively 
    *          subtract instead of add.
    *
    * @return  The date which is <code>millis</code> different from the
    *          left operand or <code>unknown value</code> if <code>dt</code> 
    *          is <code>unknown value</code>.
    */
   @SuppressWarnings("unchecked")
   public static <T extends datetime> T plusMillis(T dt, long millis)
   {
      if (dt.isUnknown())
      {
         return (T) dt.instantiateUnknown();
      }
      
      // Apparently in java 1.7, private field dt.time is not accessible from static context.
      // The cause is dt is of type T not datetime, so it does make sense a little
      long newMillis = dt.getTime() + millis;
      long days = dt.longValue();
      
      if (newMillis < 0)
      {
         // must go back in time!
         long daysOff = newMillis / date.MILLIS_PER_DAY;
         newMillis    = newMillis % date.MILLIS_PER_DAY;
         
         if (newMillis != 0)
         {
            newMillis += date.MILLIS_PER_DAY;
            daysOff--;
         }

         days += daysOff;
      }
      else if (newMillis >= date.MILLIS_PER_DAY)
      {
         // go forward in time
         days += newMillis / date.MILLIS_PER_DAY;
         newMillis = newMillis % date.MILLIS_PER_DAY;
      }
      
      T ret = (T) dt.duplicate();
      ret.setDayNumber(days);
      ret.setTime(newMillis);
      
      return ret;
   }

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

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

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

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

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

   /**
    * Returns the time in milliseconds (MTIME, similar to TIME, which 
    * returns seconds since midnight).
    * 
    * @param   dt
    *          the datetime or datetime-tz to evaluate.
    *
    * @return  the time in milliseconds.
    */
   public static integer millisecondsSinceMidnight(character dt)
   {
      if (dt.isUnknown())
      {
         return new integer();
      }
      
      ErrorManager.recordOrThrowError(12117, "Unacceptable datatype for MTIME argument.", false);
      
      return new integer();
   }
   
   /**
    * Creates a new <code>datetime</code> instance that represents the 
    * <code>unknown value</code>.
    * <p>
    * This method is deprecated. Use the default constructor instead.
    * 
    * @return   An instance that represents the <code>unknown value</code>.
    */
   @Deprecated
   public static datetime instantiateUnknownDatetime()
   {
      return new datetime();
   }

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

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

   /**
    * Return the default display format string for this type.
    *
    * @return   The default format string.
    */
   public String defaultFormatString()
   {
      return defaultFormat;
   }
   
   /**
    * Obtain the length (in bytes) of this BDT will use when serialized. 
    *
    * @return  the length of this BDT will use when serialized.
    */
   @Override
   public int getSize()
   {
      if (isUnknown())
      {
         return 1;
      }
      
      if (super.intValue() == RAW_EPOCH)
      {
         // during the RAW_EPOCH, the time is the only serialized data, luckily, this is relative
         // to midnight and its size follows integer size
         return 1 + new integer(time).getSize();
      }
      
      // for all other dates, the time has a 4 bytes reserved and the space needed by the day is
      // given by the super class 
      return super.getSize() + 5;
   }
   
   /**
    * Sets the state (data and unknown value) of this instance based on the
    * state of the passed instance.
    * <p>
    * If the value is not of type <code>datetime</code>, the following automatic type
    * conversion will occur:
    * <p>
    * <ol>
    *    <li>if the value is a date/datetime/datetimetz value, the current value is used
    *    <li>if the value is unknown, sets the instance to unknown
    *    <li>if the value is of type logical, the value is converted to integer (1 / 0) and then 
    *             evaluate for the new value
    *    <li>if the value is a non-unknown numeric value, including recid, 
    *             this uses the number as the julian day
    *    <li>if the value is a non-unknown character value, it attempts to parse this as a
    *        properly formatted date
    *    <li>otherwise it will recordOrThrowError number 5729.
    * </ol>
    * <p>
    * This conversion is meant to handle the cases of built-in functions and methods
    * in the 4GL which have polymorphic return types (e.g. DYNAMIC-FUNCTION()).
    *
    * @param    value
    *           The instance from which to copy state.
    */
   public void assign(BaseDataType value)
   {
      if (value == null)
      {
         super.assign(value);
         return;
      }

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

         incompatibleTypesOnConversion();
      }
   }
   
   /**
    * Sets the state of this instance based on the state of the passed instance.
    * If time information is not available (<code>value</code> is a <code>date</code>) 00:00 is 
    * assumed. Extra information available in <code>datetimetz</code> (time-zone offset) is 
    * ignored. 
    *
    * @param    value
    *           The instance from which to copy state.
    */
   @Override
   public void assign(date value)
   {
      // the value may be a proxy; make sure to unwrap and work with the real value
      if (BaseDataType.isProxy(value))
      {
         assign(value.val());
         return;
      }
      
      if (value == null || value.isUnknown())
      {
         setUnknown();
         return;
      }
      
      if (value instanceof datetimetz)
      {
         // convert value to local time before assigning
         deepAssign(value.toLocalDatetime());
      }
      else if (value instanceof datetime)
      {
         deepAssign((datetime) value);
      }
      else 
      {
         checkUndoable(value);
         
         super.deepAssign(value);
         resetTime();
      }
   }
   
   /**
    * Sets the state (data and unknown value) and configuration (time-zone)
    * of this instance based on the state of the passed instance.
    *
    * @param    value
    *           The instance from which to copy state.
    */
   public void deepAssign(datetime value)
   {
      if (value == null || value.isUnknown())
      {
         setUnknown();
         return;
      }

      checkUndoable(value);
      
      super.deepAssign(value);
      time = value.time;
   }
   
   /**
    * Compares this instance with the specified instance and returns a -1
    * if this instance is less than the specified, 0 if the two instances
    * are equal and 1 if this instance is greater than the specified
    * instance.  This is the implementation of the <code>Comparable</code>
    * interface.
    * <p>
    * The algorithm will fail to give meaningful results in the case where
    * one tries to sort against other objects that do not represent compatible
    * values.
    *
    * @param   obj
    *          The date instance to compare against.
    *
    * @return  -1, 0 or 1 depending on if this instance is less than, equal to or greater 
    *          (respectively) than the specified instance <code>obj</code>.
    */
   public int compareTo(Object obj)
   {
      if (!(obj instanceof date) || obj instanceof datetimetz)
      {
         // datetime-tz values are NOT comparable with datetime.
         // in special conditions date values are compatible, see below 
         ErrorManager.recordOrThrowError(
               223, "Incompatible data types in expression or assignment");
         return -1;
      }

      // The documentation say a date can be compared ONLY WITH other instances of date.
      // This is true for compile-time values, the static checking will print error 223
      // "Incompatible data types in expression or assignment" directly;
      // However, if the compare operator receives different kind of date-related only the 
      // "common parts" are compared - ie. ignore additional fields from derived classes.
      date thatDate = (date) obj; 
      
      if (!(thatDate instanceof datetime))
      {
         // must be a date, not datetime let it handle the comparison
         // and return the mirrored result
         return -thatDate.compareTo(this);
      }
      
      datetime that = (datetime) thatDate;

      boolean u1 = this.isUnknown();
      boolean u2 = that.isUnknown();
      
      if (u1 && u2)
      {
         // two unknown are equals
         return 0;
      }
      if (u1 || u2)
      {
         // only of is unknown, the other is not
         return -1;
      }
      
      // compare the actual days of both values
      long thisDays = this.longValue();
      long thatDays = that.longValue();
      if (thisDays > thatDays)
      {
         return 1;
      }
      else if (thisDays < thatDays)
      {
         return -1;
      }
      
      // in case of equality, compare the time
      if (this.time < that.time)
      {
         return -1;
      }
      else if (this.time > that.time)
      {
         return 1;
      }

      // otherwise they are equals
      return 0;
   }

   /**
    * An equality test which handles the most common cases with the least
    * amount of overhead.  The superclass implementation is invoked otherwise.
    * <p>
    * This implementation is consistent with {@link #hashCode()}.
    * 
    * @see com.goldencode.p2j.util.BaseDataType#equals(java.lang.Object)
    */
   public boolean equals(Object o)
   {
      if (this == o)
      {
         // common case
         return true;
      }
      if (o instanceof unknown && this.isUnknown())
      {
         // if this is unknown then it is equal to typeless unknown 
         return true;
      }
      if (!(o instanceof datetime) || o instanceof datetimetz)
      {
         // a datetime can be compared ONLY WITH other instances of datetime
         return false;
      }
      
      datetime that = (datetime) o;
      boolean u1 = this.isUnknown();
      boolean u2 = that.isUnknown();
      if (u1 && u2)
      {
         // two unknown are equals
         return true;
      }
      if (u1 || u2)
      {
         // only of is unknown, the other is not
         return false;
      }

      return (this.longValue() == that.longValue()) && (this.time == that.time);
   }

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

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

   /**
    * Returns the current instance as an instance of the <code>Date</code>
    * class which will accurately represent the UTC time that corresponds
    * to the current date in the timezone stored in this instance or
    * in the default timezone if the stored timezone is <code>null</code>.
    * <p>
    * <strong>WARNING:</strong> since the J2SE <code>Date</code> class
    * implicitly encodes locale-specific time information along with date
    * information, the <code>Date</code> object returned by this method is
    * sensitive to the environment in which it is <em>interpreted</em>
    * downstream.  It is imperative therefore, when using the returned
    * <code>Date</code> in downstream code, that the timezone of such code
    * (e.g., a date formatter or <code>Calendar</code> instance) matches the
    * timezone stored in this <code>date</code> instance (if the stored
    * timezone is <code>null</code>, the JVM's default timezone should be used
    * in downstream code).  Otherwise, the discrepancy in timezones (including
    * any Daylight Savings offset) may cause downstream code to interpret the
    * date information in the returned object incorrectly, typically as one
    * day earlier than expected.
    *
    * @return   The date converted to be specific to the stored timezone or
    *           to the default timezone if the stored timezone is 
    *           <code>null</code>.
    */
   public Date dateValue()
   {
      return dateValue(TimeZone.getDefault());
   }
   
   /**
    * Returns the current instance as an instance of the <code>Date</code>
    * class which will accurately represent the UTC time that corresponds
    * to the current date in the timezone passed as a parameter or in the
    * default timezone if the passed timezone is <code>null</code>.
    * <p>
    * <strong>WARNING:</strong> since the J2SE <code>Date</code> class
    * implicitly encodes locale-specific time information along with date
    * information, the <code>Date</code> object returned by this method is
    * sensitive to the environment in which it is <em>interpreted</em>
    * downstream.  It is imperative therefore, when using the returned
    * <code>Date</code> in downstream code, that the timezone of such code
    * (e.g., a date formatter or <code>Calendar</code> instance) matches the
    * the <code>override</code> parameter to this method (if
    * <code>override</code> is <code>null</code>, the JVM's default timezone
    * should be used in downstream code).  Otherwise, the discrepancy in
    * timezones (including any Daylight Savings offset) may cause downstream
    * code to interpret the date information in the returned object
    * incorrectly, typically as one day earlier than expected.
    *
    * @param    override
    *           The timezone to use for interpreting this date or 
    *           <code>null</code> if the default JVM timezone should be used.
    *
    * @return   The date converted to be specific to the passed timezone.
    */
   public Date dateValue(TimeZone override)
   {
      if (isUnknown())
      {
         return null;
      }
      
      if (override == null)
      {
         override = TimeZone.getDefault();
      }
      
      return neutralMillisToDate(julianDayToMillis(longValue()) + time, override);
   }

   /**
    * Creates a SQL compatible string representation of the instance data
    * (the format is "YYYY-MM-DD HH:MM:SS.zzz"). 
    * <p>
    * The year component is treated differently since it can have a sign (negative indicates 
    * BC era) and if the date is within the Y2K window it is truncated to fit.
    *
    * @return   The formatted string or the 'failure' string if the date is
    *           too large to fit in the specified format.
    */
   @Override
   public String toStringSQL()
   {
      NumberFormat nf = NumberFormat.getInstance();
      nf.setMinimumIntegerDigits(2);
      nf.setGroupingUsed(false);
      
      StringBuilder sb = new StringBuilder();
      
      sb.append(super.toStringSQL()); // let the super class do its job
      
      sb.append(' ');   // this is the date-time separator
      
      sb.append(nf.format(getHours()));
      sb.append(':');
      sb.append(nf.format(getMinutes()));
      sb.append(':');
      sb.append(nf.format(getSeconds()));
      sb.append('.');
      nf.setMinimumIntegerDigits(3);
      sb.append(nf.format(getMilliseconds()));
      
      return sb.toString();
   }
   
   /**
    * Creates a string representation of the instance data using the Progress 4GL 'export' format.
    * This format is unusual in that it does not have a fixed width for the year but in fact it 
    * ensures that all years are formatted to at least 3 digits (with leading zeros) up to a
    * maximum of 6 digits (for the year -32768 which is the widest possible Progress year) as 
    * necessary.  The months and days are always formatted as '99'. Another 'quirk' of this 
    * format is that if the year is within the Y2K window, it is formatted as a 2 digit year.
    * The separators are always '/' characters. 
    * The time part uses the standard format of 2 digits per components separated by ":", except
    * for milliseconds which have three digits and are separated by ".".
    * <p>
    * This implementation creates the given string based on the order of
    * date components (e.g. MDY, YMD...) defined for this context.
    *
    * @return   The 'message' formatted string.
    */
   @Override
   public String toStringMessage()
   {
      if (isUnknown())
      {
         return "?";
      }
      
      byte[] dateOrder = date.getDateComponentOrder();
      NumberFormat nf = NumberFormat.getInstance();
      nf.setGroupingUsed(false);
      nf.setMinimumIntegerDigits(2);
      
      StringBuilder sb = new StringBuilder();
      
      for (int i = 0; i < 3; i++)
      {
         if (dateOrder[i] == MONTH)
         {
            sb.append(nf.format(getMonth()));
         }
         else if (dateOrder[i] == DAY)
         {
            sb.append(nf.format(getDay()));
         }
         else
         {
            sb.append(toYearString(3, 6, false));
         }
         
         // add separators between the 1st/2nd and 2nd/3rd components
         if (i < 2)
         {
            sb.append('/');
         }
      }
      
      sb.append(' '); // date-time separator
      
      sb.append(nf.format(getHours()));
      sb.append(':');
      sb.append(nf.format(getMinutes()));
      sb.append(':');
      sb.append(nf.format(getSeconds()));
      sb.append(NumberType.getDecimalSeparator()); // default is '.';);
      nf.setMinimumIntegerDigits(3);
      sb.append(nf.format(getMilliseconds()));
      
      return sb.toString();
   }
   
   /**
    * Obtains the text form of this date instance in ISO 8601 standard format.
    * 
    * @return   A string that is the ISO representation of this date 
    *           ("YYYY-MM-DDTHH:MM:SS.SSS+HH:MM") or "?" if <code>unknown</code>.
    */
   @Override
   public String toStringExport()
   {
      if (isUnknown())
      {
         return "?";
      }
      
      return getIsoDate();
   }

   /**
    * Create a string representation of this date using a Progress 4GL style
    * date format string such as 99/99/99 HH:MM:SS.SSS or 99-99-9999 HH:MM:SS.SSS. 
    * The '/', '-', '.' and ':' are valid separators.  All 7 components of the date must be
    * represented in the format string and the number of digits in each
    * component is used to pad on the left with zeros as well as to detect 
    * when values are too large to fit in the requested format.  In such
    * cases the resulting string has all '9' characters of the format string
    * replaced with the '?'.  For example, a failure with a format string of
    * '99/99/99' will result in a '??/??/??'. 
    * <p>
    * There are 2 (and only 2) cases where separator characters are not
    * needed: '999999' and '99999999'.  Any other format string without
    * separator characters will cause an error to occur. 
    * <p>
    * The year component is treated differently since it can have a sign
    * (negative indicates BC era) and if the date is within the Y2K window it
    * is truncated to fit inside a 9 (if possible without losing data) or 99
    * format (but not a 999 format).
    * <p>
    * This implementation creates the given string based on the order of
    * date components (e.g. MDY, YMD...) defined for this context.
    *
    * @param    fmt
    *           The Progress style format string to use or <code>null</code>
    *           to use the default obtained using <code>defaultFormatString()</code>.
    *
    * @return   The formatted string or the 'failure' string if the date is
    *           too large to fit in the specified format.
    *
    */
   @Override
   public String toString(String fmt)
   {
      String saveFmt = fmt; 
      // set our default format as needed
      if (fmt == null || fmt.trim().length() == 0)
      {
         fmt = defaultFormatString();
      }
      
      if (isUnknown())
      {
         char[] blanks = new char[fmt.length()];
         Arrays.fill(blanks, ' ');
         return new String(blanks);
      }
      
      char timedateSep = ' ';
      if (fmt.contains("t"))
      {
         fmt = fmt.replace('t', ' ');
         timedateSep = 't';
      }
      else if (fmt.contains("T"))
      {
         fmt = fmt.replace('T', ' ');
         timedateSep = 'T';
      }
      
      int k = fmt.indexOf(' '); // look for date / time separator
      
      if (k == -1)
      {
         // if not found assume format contain only date part
         return super.toString(fmt);
      }
      
      StringBuilder sb = new StringBuilder();
      
      NumberFormat nf = NumberFormat.getInstance();
      nf.setGroupingUsed(false);
      nf.setMinimumIntegerDigits(2);
      
      String datePartFormat = fmt.substring(0, k);    // first k digits
      String timePartFormat = fmt.substring(k + 1).toUpperCase();   // everything after digit k
      
      if (datePartFormat.isEmpty() || timePartFormat.isEmpty()) 
      {
         throw new ErrorConditionException(-1, "Invalid datetime format: " + saveFmt);
//         ErrorManager.recordOrThrowError(-1, "Invalid datetime format: " + saveFmt);
      }
      String datePart = super.toString(datePartFormat); // let date class format its part
      if (datePart.startsWith("??"))
      {
         // date failed to be represented on designed format; fail all:
         return fmt.replaceAll(".", "?");
      }
      sb.append(datePart);
      sb.append(timedateSep);
      
      boolean uppercaseAM = timePartFormat.contains("A");
      boolean lowercaseAM = timePartFormat.contains("a");
      timePartFormat = timePartFormat.toUpperCase();
      
      if (timePartFormat.startsWith("HH"))
      {
         // hours
         int hours = getHours();
         // if am/pm is specified adjust hours in 0..12 interval
         if (uppercaseAM || lowercaseAM)
         {
            if (hours >= 12)
            {
               hours -= 12;
            }
            if (hours == 0)
            {
               hours = 12;
            }
         }
         sb.append(nf.format(hours));
         timePartFormat = timePartFormat.substring(2);
         
         if (timePartFormat.startsWith(":MM"))
         {
            // minutes
            sb.append(':').append(nf.format(getMinutes()));
            timePartFormat = timePartFormat.substring(3);
            
            if (timePartFormat.startsWith(":SS"))
            {
               // seconds
               sb.append(':').append(nf.format(getSeconds()));
               timePartFormat = timePartFormat.substring(3);
               
               char decimalSep = NumberType.getDecimalSeparator(); // default is '.';
               if (timePartFormat.startsWith(".SSS"))
               {
                  // milli - seconds
                  nf.setMinimumIntegerDigits(3);
                  sb.append(decimalSep).append(nf.format(getMilliseconds()));
                  timePartFormat = timePartFormat.substring(4);
                  nf.setMinimumIntegerDigits(2);
               }
               else if (timePartFormat.startsWith(".SS"))
               {
                  // hundreds of seconds
                  int millis = getMilliseconds();
                  sb.append(decimalSep).append(nf.format(millis / 10));
                  timePartFormat = timePartFormat.substring(3);
               }
               else if (timePartFormat.startsWith(".S"))
               {
                  // tenths of seconds
                  int millis = getMilliseconds();
                  nf.setMinimumIntegerDigits(1);
                  sb.append(decimalSep).append(nf.format(millis / 100));
                  timePartFormat = timePartFormat.substring(2);
                  nf.setMinimumIntegerDigits(2);
               }
            }
         }
      }
      
      // am/pm and timezone seem to behave exclusively, however, AM is always before TZ
      String ampm = null;
      if (timePartFormat.startsWith(" AP"))
      {
         // two letter am/pm
         ampm = getHours() < 12 ? " AM" : " PM";
         timePartFormat = timePartFormat.substring(3);
      }
      else if (timePartFormat.startsWith(" A"))
      {
         // one letter am/pm
         ampm = getHours() < 12 ? " A" : " P";
         timePartFormat = timePartFormat.substring(2);
      }
      if (ampm != null)
      {
         
         if (lowercaseAM)
         {
            ampm = ampm.toLowerCase();
         }
         sb.append(ampm);
      }
      
      // check the timezone
      if (timePartFormat.startsWith("+HH") || timePartFormat.startsWith("-HH"))
      {
         // tz hours
         sb.append(getTzHours() < 0 ? '-' : '+');
         sb.append(nf.format(Math.abs(getTzHours())));
         timePartFormat = timePartFormat.substring(3);

         if (timePartFormat.startsWith(":MM"))
         {
            // tz minutes
            sb.append(':').append(nf.format(Math.abs(getTzMinutes())));
            timePartFormat = timePartFormat.substring(3);
         }
      }
      
      // now timePartFormat should be an empty string
      return sb.toString();
   }
   
   /**
    * Replacement for the default object reading method. The latest state is read from the 
    * input source.
    * 
    * @param    in
    *           The input source from which fields will be restored.
    *
    * @throws   IOException
    *           In case of I/O errors.
    * @throws   ClassNotFoundException
    *           If payload can't be instantiated.
    */
   public void readExternal(ObjectInput in)
   throws IOException,
          ClassNotFoundException
   {
      // let the date class handle date component
      super.readExternal(in);
      
      // read the time component:
      this.time = in.readInt();
   }

   /**
    * Replacement for the default object writing method. The latest state is written to the
    * output destination.
    * 
    * @param    out
    *           The output destination to which fields will be saved.
    *
    * @throws   IOException
    *           In case of I/O errors.
    */
   public void writeExternal(ObjectOutput out)
   throws IOException
   {
      // let the date class handle date component
      super.writeExternal(out);
      
      // write the time component:
      out.writeInt(this.time);
   }
   
   /**
    * Return an object equal to this expressed in local time (current timezone offset).
    *
    * @return  a copy of this object (it's already in local time)
    */
   public datetime toLocalDatetime()
   {
      return new datetime(this);
   }
   
   /**
    * Obtains the text form of this date instance in ISO 8601 standard format.
    * 
    * @return   A string that is the ISO representation of this date
    *           ("YYYY-MM-DDTHH:MM:SS.SSS") or "?" if <code>unknown</code>.
    */
   @Override
   public String getIsoDate()
   {
      if (isUnknown())
      {
         return "?";
      }
      
      String isoDate = super.getIsoDate();
      if ("?".equals(isoDate)) 
      {
         return "?";
      }
      
      int hours = getHours();
      int minutes = getMinutes();
      int seconds = getSeconds();
      int millis = getMilliseconds();
      
      StringBuilder sb = new StringBuilder(isoDate);
      sb.append("T");
      if (hours < 10)
      {
         sb.append('0');
      }
      sb.append(hours);
      sb.append(":");
      if (minutes < 10)
      {
         sb.append('0');
      }
      sb.append(minutes);
      sb.append(":");
      if (seconds < 10)
      {
         sb.append('0');
      }
      sb.append(seconds);
      sb.append(".");
      if (millis < 100)
      {
         if (millis < 10)
         {
            sb.append('0');
         }
         sb.append('0');
      }
      sb.append(millis);

      return sb.toString();
   }

   /**
    * Reset the time for this instance.
    */
   protected void resetTime()
   {
      this.time = 0;
   }
 
   /**
    * Parses this ISO 8601 value as a datetime.
    *  
    * @param    val
    *           The date to parse.
    * 
    * @throws   DateTimeParseException
    *           If the format is not ISO 8601 compliant.
    */
   @Override
   public void fromIso(String val)
   {
      LocalDateTime odt = LocalDateTime.parse(val, ISO_LOCAL_DATE_TIME);
      Date date = Date.from(odt.atZone(ZoneId.systemDefault()).toInstant());
      date d = new date(date);
      int millis = odt.get(ChronoField.MILLI_OF_DAY);
      assign(new datetime(d, millis));
   }
   
   /**
    * Parses the literal encountered in a INITIAL option. Does the best effort to parse the text of the
    * literal taking into account the current session settings (like the date format and the windowing year
    * and the moment when the method is invoked. The main purpose of this method is to select the proper
    * date format and then create the proper {@code datetime} object with respective values. 
    * <p>
    * {@code now} is also supported, in which case the current timestamp is returned. 
    *
    * @param   text
    *          The text of the literal to be parsed. 
    *
    * @return  A {@code datetime} representing the respective value, taking into account the current session
    *          settings (like the date format and the windowing year). 
    */
   public static datetime parseInitial(String text)
   {
      if ("now".equalsIgnoreCase(text))
      {
         return now();
      }
      
      String df = getDynamicFormat();
      if (df != null)
      {
         // when dynamic format is not null, the date may be in any format the user selected in
         // SESSION:DATE-FORMAT
         return new datetime(text, df, getWindowingYear());
      }
      
      if (text.length() >= 10)
      {
         // parsing a string literal is more lenient
         if ((text.charAt(2) == '/' || text.charAt(2) == '-') &&
             (text.charAt(5) == '/' || text.charAt(5) == '-'))
         {
            // identified dd/dd/dddd, must be MDY 
            return new datetime(text, DEFAULT_DATE_FORMAT, DEFAULT_DATE_WINDOWING_YEAR);
         }
         
         // assumed dddd/dd/dd, always in YMD
         return new datetime(text, DEFAULT_DATETIME_FORMAT, DEFAULT_DATE_WINDOWING_YEAR);
      }
      
      // probably an incorrectly formed literal
      return new datetime(text);
   }
   
   /**
    * Identify if this data is dependent upon the context local. This identifies
    * if the data is safe to be cached in cross-session places or the data should
    * not leak from the context. If is is not safe to be shared cross-session, then
    * this should return {@code null}. If other representation is suitable for caching,
    * then this can return another object that wraps internal values that are not
    * dependent upon the context (e.g. date, date-time, date-time-tz).
    * 
    * @return   Always. {@code null}.
    */
   @Override
   public Object getIndependentFromContext()
   {
      return new ContextIndependentDateTime(isUnknown(), this.longValue(), this.time);
   }
   
   /**
    * Returns the absolute time in milliseconds since Java 'Epoch' or 0
    * if this is unknown.
    *
    * @return   The absolute time in milliseconds since the Java epoch.
    */
   @Override
   protected long getAbsoluteTimeOffset()
   {
      return isUnknown() ? 0 : (super.getAbsoluteTimeOffset() + time);
   }
   
   /**
    * Worker to set the date (in the superclass) and the time (in this class)
    * given all the basic inputs.
    *
    * @param    d
    *           The date instance to copy from.
    * @param    time
    *           Milliseconds since midnight.
    */
   protected void instantiateFromDateTime(date d, long time)
   {
      checkUndoable(() -> new datetime(d,  time));
      
      super.deepAssign(d);
      if (!isUnknown())
      {
         this.time = (int) time;
      }
   }
   
   /**
    * Generate a parsing error. The object is left in unknown state after setting the error code
    * and associated message.
    * The routine automatically generates the error message based on the error code provided.
    *  
    * @param   errCode
    *          The code of error to be raised.
    */
   protected void generateParsingError(int errCode)
   {
      String msg = "";
      switch (errCode)
      {
         case 11841:
            msg = "Invalid characters in datetime/datetime-tz value";
            break;
         case 11256:
            msg = "Invalid minutes in datetime value";
            break;
         case 11254:
            msg = "Invalid minute offset in the timezone portion of a datetime-tz value";
            break;
         case 11257:
            msg = "Missing colon between hour and minute in datetime value";
            break;
         case 11259:
            msg = "Invalid hour offset in the timezone portion of a datetime-tz value";
            break;
         case 11260:
            msg = "Invalid hours in datetime value";
            break;
         case 11253:
            msg = "Invalid seconds in datetime value";
            break;
         case 11252:
            msg = "Invalid milliseconds in datetime value";
            break;
         case 0:
            return; //do nothing
      }
      
      ErrorManager.recordOrThrowError(errCode, msg);
      setUnknown();
   }
   
   /**
    * Parses the given date and time specification and assigns the values into the instance.  The result will
    * be a date and time value that is "local" to the default timezone.  If a different timezone is specified
    * in the input, then the date and time values that are specified will be translated into values "local"
    * to the default timezone, before assignment.
    * <p>
    * The special string "NOW" is honored as the specification.  This is equivalent to the built-in function
    * {@code now}.
    * <p>
    * When the specification is of a datetime literal, it must match the following format rules:
    * <p>
    * <ol>
    *   <li> Date value (this is not optional).  The string cannot start with whitespace. The first input is
    *        a date value that has at least a month and day specified or a month, day and year. The values
    *        are parsed in order as specified in SESSION:DATE-FORMAT. Zero padding is allowed but not
    *        required.
    *   <li> Date and time delimiter (not optional). Whitespace or a letter T (upper or lowercase) which is
    *        followed by whitespace. If the letter T is used, it cannot be preceded by whitespace. No other
    *        characters are allowed.
    *   <li> Time value (optional). If not specified, it is set to 0. It is specified in HH:MM:SS.sss format,
    *        however right side components can be left off (so HH, HH:MM, HH:MM:SS and HH:MM:SS.sss are all
    *        valid forms). Zero padding is allowed but not required, so hours can be H or HH, minutes M or
    *        MM, seconds S or SS and milliseconds can be s, ss or sss. Delimiters can only be there if the
    *        following component is included.
    *   <li> Whitespace (optional).
    *   <li> Timezone specification (optional). This is in the form of a sign (+ or -) then the hours (1 or
    *        2 digits) a colon delimiter and then minutes (1 or 2 digits). Zero padding is allowed but not
    *        required.  The minutes can be left off but if so, then the delimiter must also be left off.  If
    *        specified, the instance will be set using the timezone specified EVEN THOUGH THE TIMEZONE OFFSET
    *        IS NOT STORED (since this is not a datetimetz).
    *   <li> Whitespace (optional).
    * </ol>
    * If the {@code spec} string is empty or if it matches '?' (with no leading or trailing spaces), an
    * unknown datetime object is instantiated.
    *
    * @param    spec
    *           The date and time specification.
    * @param    dateFormat
    *           The date format used for parsing.  If it is null the default is used.
    * @param    windowingYear
    *           The windowing year used for 2 digits years. If it is negative the default is used.
    */
   @Override
   protected void instantiateFromStringWorker(String spec, String dateFormat, int windowingYear)
   {
      if (spec == null || spec.trim().isEmpty() || spec.equals("?"))
      {
         setUnknown();
         return;
      }
      
      // special cases where the input is not a dt literal
      if ("now".equalsIgnoreCase(spec))
      {
         assign(datetimetz.now());
         return;
      }
      else if ("today".equalsIgnoreCase(spec))
      {
         invalidInitializer("TODAY");
         setUnknown();
         return;
      }
      
      // at this point, if the input includes ? character but had other characters also
      // included (e.g. leading or trailing spaces like " ?" or "? "), then it is improperly
      // formed
      if (spec.indexOf('?') >= 0)
      {
         ErrorManager.recordOrThrowError(85, "Invalid date input");
         return;
      }
      
      // auto detect date format
      if (dateFormat == null/*auto*/)
      {
         dateFormat = date.getDateOrderNative();
      }
      
      spec = spec.trim();
      boolean hasZuluTime = spec.endsWith("Z");
      if (hasZuluTime)
      {
         spec = spec.substring(0, spec.length() - 1);
      }
      
      final String decSep = String.valueOf(NumberType.getDecimalSeparator());
      final String regexpDecimalSep = (".".equals(decSep)) ? "\\." : decSep;
      
      // global sanity check:
      String copy = spec.toUpperCase()
                        .replaceAll("[TAPM:,\\." + regexpDecimalSep + "\\-\\+0123456789/]", " ").trim();
      if (!copy.isEmpty()) 
      {
         generateParsingError(11841);
         return;
      }
      
      // Switching to uppercase will assure date-time separator is 'T' or space and AM/PM are 
      // also uppercased. We replace the (first) occurrence of 'T' with space to simplify date and 
      // time separation.
      // Also, spaces at start/end seem to be ignored, except if the date portion includes a ?
      // which is not allowed and is handled above.
      spec = spec.toUpperCase().trim();
      
      int k = spec.indexOf('T'); // look for date / time separator
      if (k == -1)
      {
         // if 'T' is not the separator, then it's probably space:
         k = spec.indexOf(' ');
      }
      else if (k > 0 && spec.charAt(k - 1) == ' ')
      {
         // if space appears just before 'T' separator we have an invalid format
         generateParsingError(11841);
         return;
      }
      if (k == -1)
      {
         String fspec = spec;
         String fDateFormat = dateFormat;
         checkUndoable(() -> new datetime(fspec, fDateFormat, windowingYear));
         
         // if no separators have been found assume format contain only date part
         super.instantiateFromStringWorker(spec, dateFormat, windowingYear);
         // so time and tz are null:
         if (getType() == Type.DATETIMETZ)
         {
            int[] dateParts = getYearMonthDay();
            ((datetimetz) this).resetTime(date.createDate(dateParts[0], dateParts[1], dateParts[2]));
         }
         else if (getType() == Type.DATETIME)
         {
            resetTime();
         }
         
         return;
      }

      String datePartSpec = spec.substring(0, k);    // first k digits is the date
      // date part can use date.instantiateFromStringWorker() 
      super.instantiateFromStringWorker(datePartSpec, dateFormat, windowingYear);
      if (isUnknown())
      {
         // date parsing failed, do not continue
         return;
      }
      
      String timePartSpec = spec.substring(k + 1).trim();
      // acceptable formats: h[:m[:s]] [am] [+h[:m]]
      
      String timeStr = "";
      String zoneStr = "";
      
      // locate am/pm (if exists)
      char ampmSpec = '0';
      int ampmOffset = -1;
      ampmOffset = timePartSpec.indexOf("PM");
      
      if (ampmOffset != -1)
      {
         ampmSpec = 'P';
      }
      else
      {
         ampmOffset = timePartSpec.indexOf("P");
         if (ampmOffset != -1)
         {
            ampmSpec = 'P';
         }
         else
         {
            ampmOffset = timePartSpec.indexOf("AM");
            if (ampmOffset != -1)
            {
               ampmSpec = 'A';
            }
            else
            {
               ampmOffset = timePartSpec.indexOf("A");
               if (ampmOffset != -1)
               {
                  ampmSpec = 'A';
               }
            }
         }
      }

      //locate timezone (if exists)
      int tzOffset = timePartSpec.indexOf("+");
      if (tzOffset == -1)
      {
         tzOffset = timePartSpec.indexOf("-");
      }
      boolean hasTimeZone = (tzOffset != -1); 
      
      if (hasTimeZone && hasZuluTime)
      {
         generateParsingError(11841);
         return;
      }
      
      if (ampmSpec != '0')
      {
         timeStr = timePartSpec.substring(0, ampmOffset);
         
         if (hasTimeZone)
         {
            if (ampmOffset > tzOffset)
            {
               generateParsingError(11841);
               return;
            }
            
            // has all three components
            zoneStr = timePartSpec.substring(tzOffset);
         }
      }
      else if (hasTimeZone)
      {
         // time and timezone
         timeStr = timePartSpec.substring(0,  tzOffset);
         zoneStr = timePartSpec.substring(tzOffset);
      }
      else
      {
         // no ampm and no timezone
         timeStr = timePartSpec;
      }
      
      // sanity check, round 2: timeStr must contain only : . and digits
      // additionally + and - are ignored at this stage
      copy = timeStr.toUpperCase().replaceAll("[,\\.\\+\\-" + regexpDecimalSep + ":0123456789/]", " ").trim();
      if (!copy.isEmpty()) 
      {
         generateParsingError(11841);
         return;
      }
      
      // sanity check, round 3: zoneStr must contain only  + - : and digits
      copy = zoneStr.toUpperCase().replaceAll("[\\+\\-:0123456789/]", " ").trim();
      if (!copy.isEmpty()) 
      {
         generateParsingError(11841);
         return;
      }
      
      long hours = 0;
      long minutes = 0;
      long seconds = 0;
      long millis = 0;
      long zoneOffset = datetimetz.INVALID_TZ;
      
      int errCode = 0;
      try
      { 
         // catching only number parsing exceptions 
         String decSeps = ",." + decSep;
         StringTokenizer st = new StringTokenizer(timeStr.trim(), (decSep + ",.: "), true);
         if (st.hasMoreTokens())
         {
            // prepare hours parsing exceptions:
            errCode = 11841;
            hours = Long.parseLong(st.nextToken());
            if (st.hasMoreTokens())
            {
               if (!":".equals(st.nextToken())) {
                  generateParsingError(11841);
                  return;
               }
               if (!st.hasMoreTokens())
               {
                  generateParsingError(11256);
                  return;
               }
               
               // prepare minutes parsing exceptions:
               errCode = 11256;
               minutes = Long.parseLong(st.nextToken());
               if (st.hasMoreTokens())
               {
                  if (!":".equals(st.nextToken())) {
                     generateParsingError(11841);
                     return;
                  }
                  if (!st.hasMoreTokens())
                  {
                     generateParsingError(11253);
                     return;
                  }
                  // prepare seconds parsing exceptions:
                  errCode = 11253;
                  seconds = Long.parseLong(st.nextToken());
                  if (st.hasMoreTokens())
                  {
                     if (!decSeps.contains(st.nextToken())) {
                        generateParsingError(11841);
                        return;
                     }
                     if (!st.hasMoreTokens())
                     {
                        generateParsingError(11252);
                        return;
                     }
                     String millisStr = st.nextToken();
                     if (millisStr.length() > 3)
                     {
                        // if there are more than 3 digits, only the first 3 are processed,
                        // the others are discarded
                        millisStr = millisStr.substring(0, 3);
                     }
                     errCode = 11252;
                     millis = Long.parseLong(millisStr);
                     
                     if (millisStr.length() == 2)
                     {
                        millis *= 10;
                     }
                     if (millisStr.length() == 1)
                     {
                        millis *= 100;
                     }
                     
                     if (st.hasMoreTokens())
                     {
                        // that should have been the last token
                        generateParsingError(11841);
                        return;
                     }
                  }
               }
            }
         }
         
         // adjust AM/PM hours format
         if (ampmSpec != '0')
         {
            if (hours == 12) //in AM/PM there is no 0 hour
            {
               hours = 0;
            }
            else if (hours > 12)
            {
               // hour 12+ in AM/PM mode ?
               generateParsingError(11260);
               return;
            }
            if (ampmSpec == 'P') // PM
            {
               hours += 12;
            }
         }
         
         // set up read time (do this first, to validate the values)
         setTime(hours, minutes, seconds, millis);
         
         // read timezone and validate (only if the time is valid)
         if (!zoneStr.isEmpty())
         {
            // timezone spec can use date.specToOffset()
            st = new StringTokenizer(zoneStr.trim(), ":+-", true);
            String sign = st.nextToken();
            
            if (!st.hasMoreTokens())
            {
               generateParsingError(11259);
               return;
            }
            
            // prepare hours parsing exceptions:
            errCode = 11259;
            long tzHours = Long.parseLong(st.nextToken());
            long tzMinutes = 0;
            
            if (st.hasMoreElements())
            {
               if (!":".equals(st.nextToken()))
               {
                  generateParsingError(11257);
                  return;
               }
               if (!st.hasMoreTokens())
               {
                  generateParsingError(11254);
                  return;
               }
               // prepare hours parsing exceptions:
               errCode = 11254;
               tzMinutes = Long.parseLong(st.nextToken());
               
               if (st.hasMoreTokens())
               {
                  // that should have been the last token
                  generateParsingError(11841);
                  return;
               }
            }
            
            if (tzMinutes < 0 || tzMinutes > 59)
            {
               generateParsingError(11254);
               return;
            }
            if (tzHours < -14 || tzHours > 14)
            {
               generateParsingError(11259);
               return;
            }
            zoneOffset = tzHours * 60 + tzMinutes;
            if ("-".equals(sign))
            {
               zoneOffset = -zoneOffset;
            }
            //adjust time with zoneOffset 
         }
      }
      catch (NumberFormatException e)
      {
         generateParsingError(errCode);
         return;
      }
      
      if (hasTimeZone)
      {
         setTimeZoneOffset(zoneOffset);
      }
      else if (hasZuluTime)
      {
         setTimeZoneOffset(0);
      }
      else
      {
         setTimeZoneOffset(datetimetz.getDefaultTimezoneOffset(this));
      }
   }
   
   /**
    * Set the offset in minutes from UTC for this instance and adjust to localtime.
    * This should only called from <code>instantiateFromStringWorker()</code>.
    *
    * @param    offset
    *           The offset, must be between -840 and 840 inclusive.
    */
   protected void setTimeZoneOffset(long offset)
   {
      if (offset < MIN_VALID_TZ || offset > MAX_VALID_TZ)
      {
         String err = "Invalid minute offset in datetime-tz value";
         ErrorManager.recordOrThrowError(11254, err);
         return;
      }
      
      // adjust to local time
      int localOffset = datetimetz.getDefaultTimezoneOffset(this);
      assign(plusMillis(this, (localOffset - offset) * 60 * 1000));
   }
   
   /**
    * Extract the portion of the calendar representing the number of
    * milliseconds since midnight.
    *
    * @param    cal
    *           The calendar instance to query.
    *
    * @return   The milliseconds since midnight.
    */
   private static int millisSinceMidnight(Calendar cal)
   {
      int hour   = cal.get(Calendar.HOUR_OF_DAY);
      int mins   = cal.get(Calendar.MINUTE);
      int secs   = cal.get(Calendar.SECOND);
      int millis = cal.get(Calendar.MILLISECOND);
      
      return (((((hour * 60) + mins) * 60) + secs) * 1000) + millis;
   }
   
   /**
    * Convenient class that wraps the low-level {@link #julian} and {@link #time} parameters
    * to avoid pinning the whole context-local of the datetime.
    */
   public static class ContextIndependentDateTime
   extends ContextIndependentDate
   {      
      /** The milliseconds since midnight. */
      int time;
      
      /**
       * Basic c'tor.
       * 
       * @param   unknown
       *          {@code true} if the value is unknown
       * @param   julian
       *          The date for this instance in Progress 4GL format.
       * @param   time
       *          The milliseconds since midnight.
       */
      public ContextIndependentDateTime(boolean unknown, long julian, int time)
      {
         super(unknown, julian);
         this.time = time;
      }

      /**
       * Implementation of equals.
       */
      @Override
      public boolean equals(Object other)
      {
         if (this == other)
         {
            return true;
         }
         
         if (other instanceof unknown && this.isUnknown())
         {
            // if this is unknown then it is equal to typeless unknown 
            return true;
         }
         if (!(other instanceof ContextIndependentDateTime) || 
              (other instanceof datetimetz.ContextIndependentDateTimeTz))
         {
            // a date can be compared ONLY WITH other instances of date
            return false;
         }
         
         ContextIndependentDateTime that = (ContextIndependentDateTime) other; 
         boolean u1 = this.isUnknown();
         boolean u2 = that.isUnknown();
         
         if (u1 && u2)
         {
            // two unknown are equals
            return true;
         }
         if (u1 || u2)
         {
            // only of is unknown, the other is not
            return false;
         }

         return this.julian == that.julian && (this.time == that.time);
      }
      
      /**
       * Implementation of hash code.
       */
      @Override
      public int hashCode()
      {
         int result = 17;
         if (isUnknown())
         {
            return result;
         }
         
         result = 37 * result + (int) (this.julian ^ (this.julian >>> 32));
         result = 37 * result + time;
         
         return result;
      }
   }
}