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