datetimetz.java
/*
** Module : datetimetz.java
** Abstract : Progress 4GL compatible datetime-tz object
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ------------------------------------Description------------------------------------------
** 001 GES 20130128 Initial commit with basic functionality.
** 002 GES 20130322 Fixed the BDT c'tor and modified the assign(BDT,boolean) to be the
** common location for the _POLY morphing. This is safe because all current
** usage would have generated a ClassCastException if that method was ever used
** with an invalid type. Now, instead of the CCE, the code will morph the value
** as is possible. The morphing code still needs to be finished.
** 003 OM 20130529 Massive update:
** Added time-zone components constants for datetime-tz formatter.
** Added components validation function.
** Default c'tor builds a light/fast unknown instance.
** Created static function for NOW 4GL functionality.
** Added parsers for 4GL literal/SQL and ISO
** 004 MAG 20140602 Fixed the BDT c'tor.
** 005 OM 20150129 Group c'tors at top of file, fixed javadocs. Added support for parsing
** negative years. Rewrote calendarValue(). Added main() test method that
** will indirectly test date and datetime functionality as well.
** 006 OM 20150623 Fixed conversion from text to datetime in polymorphic cases.
** 007 ECF 20150715 Replace StringBuffer with StringBuilder.
** 008 CA 20160627 Reworked undoable support - the undoables register themselves with all blocks
** up the stack, until either 1. the tx block which created it is reached or
** 2. the last tx block where it was saved is reached.
** 009 CA 20160812 Use date.getZoneFromOffset to build TimeZone instances (as this will be use
** internal cache).
** 010 EVL 20171103 Fix for date copy for constructor with date as parameter.
** 011 GES 20171207 Removed explicit bypass on pending error (new silent error mode).
** 012 CA 20180503 Renamed/moved some SESSION attribute getters/setters to fix issues with using
** the SESSION via a handle.
** 013 OM 20190206 Added implementation of field size needed by record-length. Fixed c'tor form.
** 014 CA 20190615 Added fromIso() to parse a ISO-8601 date.
** 015 CA 20190628 Javadoc fix.
** 016 OM 20190626 Granted public access to getIsoDate().
** 017 CA 20191206 Fixed assign/deepAssign when the value is null or unknown.
** 018 CA 20200503 Added datetimetz(NumberType,NumberType) stub.
** 019 CA 20200519 ISO 8601 format allows single-digit fields in 4GL.
** 020 OM 20191101 Added OffsetDateTime based constructor.
** 021 GES 20200806 Reduced ErrorManager context-local usage.
** CA 20200914 Fixed c'tor with explicit time offset.
** 022 IAS 20201007 Added Type enum
** CA 20210304 Fixed date, datetime and datetimetz literal parsing.
** CA 20210628 Fixed DATETIME and DATETIME-TZ toStringExport() usage.
** VVT 20211027 Fixed: time zone spec duplicated in toStringMessage(). See #5734.
** VVT 20211029 Fixed: toStringMessage(): should match the description in javadoc.
** See #5734-7. Missing @Override annotations added.
** AL2 20220328 Added proxy checks for BDT.
** CA 20220329 'fromIso' method was changed to public.
** CA 20220513 timezone() builtin function must emit as a static method call if the argument is POLY.
** Added POLY versions of the DATETIMETZ() builtin function.
** OM 20220622 Improve date/interval operations. Added implementation for the new methods.
** 023 CA 20230215 'duplicate()' method returns the real type instead of BDT.
** 024 CA 20230322 Cache the pattern used by 'fromLiteral', for ISO 8601 literals.
** 025 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 026 AB 20230907 Overrided resetTime(Date). Modified resetTime() and assign(date).
** AB 20230907 Removed the overriding annotation for resetTime(Date). Modified assign(date).
** 027 CA 20240318 INITIAL value of a datetime(-tz) variable will always have the date 'mdy' format.
** 028 OM 20240509 Added auto detection for date format, depending on the context and a quick heuristic
** analysis of the text.
** 029 OM 20240524 Refined algorithm from H028. It also caused regressions.
** 030 SB 20240904 For constructors where time components are set individually, if a time zone offset is
** not provided, use the default time zone. Refs #9124.
** 031 CA 20241002 Fixed assignment to java types.
** 032 LS 20250127 Added datetimetz(Date,int).
** 033 ICP 20250129 Used int64.of to leverage caches instances.
** 034 AL2 20250530 Added getIndependentFromContext.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.util;
import java.io.*;
import java.text.*;
import java.time.*;
import java.time.chrono.*;
import java.time.format.*;
import java.time.temporal.*;
import java.util.*;
import java.util.regex.*;
import com.goldencode.p2j.util.ErrorManager.ErrorEntry;
import com.goldencode.p2j.util.date.ContextIndependentDate;
import com.goldencode.p2j.util.datetime.ContextIndependentDateTime;
import com.goldencode.p2j.util.logging.*;
/**
* A class that represents a Progress 4GL compatible datetime-tz object whose
* data is mutable. All Progress language features related to date data
* types are supported including all function that can be accessed via the
* date aware operators and built-in Progress date-related functions.
* <p>
* This class extends the <code>datetime</code> class so all methods should work
* directly. If they don't they will be overridden. Also should the following
* static implementations:
* <p>
* <pre>
* now function (as initializer for datetime-tz) {@link #now()}
* datetime-tz function (date, integer) {@link #datetimetz(date, NumberType)}
* datetime-tz function (datetime, integer) {@link #datetimetz(date, NumberType)}
* datetime-tz function (datetime-tz, integer) {@link #datetimetz(date, NumberType)}
* datetime-tz function (date, integer, integer) {@link #datetimetz(date, NumberType, NumberType)}
* datetime-tz function (integer m, d, y, h, mm, s, ms, tz) {@link #datetimetz(NumberType, NumberType, NumberType, NumberType, NumberType, NumberType, NumberType, NumberType)}
* datetime-tz function (string parameter) {@link #datetimetz(String)}
* timezone function (datetime-tz parameter) {@link #getTimeZoneOffset}
* [literal] {@link #fromLiteral(String)}
* </pre>
* <p>
* Default data type display format: 99/99/9999 HH:MM:SS.SSS+HH:MM
*/
public class datetimetz
extends datetime
{
/** The specific ISO-8601 datetime-tz format used by 4GL. */
protected static final DateTimeFormatter ISO_OFFSET_DATE_TIME =
new DateTimeFormatterBuilder().parseCaseInsensitive()
.append(ISO_LOCAL_DATE_TIME)
.appendOffsetId()
.toFormatter()
.withResolverStyle(ResolverStyle.LENIENT)
.withChronology(IsoChronology.INSTANCE);
/** The default format string. */
private static final String defaultFormat = "99/99/9999 HH:MM:SS.SSS+HH:MM";
/** The pattern format for ISO 8601 dates. */
private static final String DTZ_PATTERN_STRING =
// mandatory part
// year has exactly 4 digits left-filled with 0 if needed, negative not allowed
// month and day are 2 digits, prepended with a 0 if needed
// hour is in 24H format, also with 2 digits format always
// minutes it the only field from mandatory part that can use one or two digits
"((\\-)?\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[tT](\\d\\d):(\\d\\d)" +
// optional seconds and millisecond part
// seconds can be represented on one or two digits
// the fractions of the seconds can be represented on any number of digits, but
// only first 3 of them are taken into consideration
"(:(\\d\\d?)(\\.(\\d)+)?)?" +
// timezone part
// mandatory + / - sign
// the hour represented on one or two digits; 0 prefix is optional if hour < 10
// the minutes is optional and represented on max 2 digits.
"(([+\\-])(\\d\\d?)(:(\\d\\d?))?)";
/** The pattern to parse ISO 8601 formats. */
private static final Pattern DTZ_PATTERN = Pattern.compile(DTZ_PATTERN_STRING);
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(datetimetz.class);
/** Time zone offset in minutes from Coordinated Universal Time (UTC). */
private int offset = INVALID_TZ;
/**
* Default constructor builds an unknown object.
*/
public datetimetz()
{
super(); // the default constructor of date will set the object to unknown
}
/**
* Constructs an instance using the inputs.
* <p>
* This is an implementation of 1 form of the DATETIME-TZ() built-in function.
* <p>
* The time value (positive or negative) is added to the date and the
* resulting value will be used to determine the date and time based on
* the given timezone. In other words, the date value can change in
* either direction by an arbitrary magnitude. This is how it is done in
* the 4GL.
* <p>
* Any input that is <code>unknown</code> will cause this instance to be
* <code>unknown</code>.
* <p>
* All arguments for this constructor are POLY arguments.
*
* @param d
* The date from which to copy. This must be an instance of the
* <code>date</code> superclass and may NOT be an instance of the
* <code>datetime</code> or <code>datetimetz</code> since that is
* a 4GL error.
* @param time
* The milliseconds since midnight.
* @param offset
* The timezone offset from UTC in minutes. Must be between -840 and 840 inclusive.
*/
public datetimetz(BaseDataType d, BaseDataType time, BaseDataType offset)
{
if (!(d instanceof date) || !(time instanceof NumberType) || !(offset instanceof NumberType))
{
ErrorManager.recordOrShowError(12117, "DATETIME-TZ", "");
// Unacceptable datatype for DATETIME-TZ argument (12117)
return;
}
initialize((date) d, (NumberType) time, (NumberType) offset);
}
/**
* Constructs an instance using the inputs, with varying behavior depending
* on the type of the first parameter.
* <p>
* If the first parameter is a <code>date</code> instance, then the date is
* copied from that instance and the second parameter is treated as the
* time value (number of milliseconds since midnight).
* <p>
* If the first parameter is a <code>datetime</code> or
* <code>datetimetz</code> instance, then the date and time is copied from
* the instance and the second parameter is treated as a positive or
* negative timezone offset for the new instance.
* <p>
* This is an implementation of 3 forms of the DATETIME-TZ() built-in
* function.
* <p>
* The time value (positive or negative) is added to the date and the
* resulting value will be used to determine the date and time based on
* the default timezone. In other words, the date value can change in
* either direction by an arbitrary magnitude. This is how it is done in
* the 4GL.
* <p>
* Any input that is <code>unknown</code> will cause this instance to be
* <code>unknown</code>.
* <p>
* All arguments for this constructor are POLY arguments.
*
* @param d
* The date (or date subclass) from which to copy.
* @param timeOrOffset
* The milliseconds since midnight if the first parameter is a
* <code>date</code> or a timezone offset from UTC in minutes
* if the first parameter is a <code>datetime</code>. As an
* offset, the value must be between -840 and 840.
*/
public datetimetz(BaseDataType d, BaseDataType timeOrOffset)
{
if (!(d instanceof date) || !(timeOrOffset instanceof NumberType))
{
ErrorManager.recordOrShowError(12117, "DATETIME-TZ", "");
// Unacceptable datatype for DATETIME-TZ argument (12117)
return;
}
initialize((date) d, (NumberType) timeOrOffset);
}
/**
* Constructs an instance using the inputs, with varying behavior depending
* on the type of the first parameter.
* <p>
* If the first parameter is a <code>date</code> instance, then the date is
* copied from that instance and the second parameter is treated as the
* time value (number of milliseconds since midnight).
* <p>
* If the first parameter is a <code>datetime</code> or
* <code>datetimetz</code> instance, then the date and time is copied from
* the instance and the second parameter is treated as a positive or
* negative timezone offset for the new instance.
* <p>
* This is an implementation of 3 forms of the DATETIME-TZ() built-in
* function.
* <p>
* The time value (positive or negative) is added to the date and the
* resulting value will be used to determine the date and time based on
* the default timezone. In other words, the date value can change in
* either direction by an arbitrary magnitude. This is how it is done in
* the 4GL.
* <p>
* Any input that is <code>unknown</code> will cause this instance to be
* <code>unknown</code>.
* <p>
* The first parameter for this constructor is a POLY argument.
*
* @param d
* The date (or date subclass) from which to copy.
* @param timeOrOffset
* The milliseconds since midnight if the first parameter is a
* <code>date</code> or a timezone offset from UTC in minutes
* if the first parameter is a <code>datetime</code>. As an
* offset, the value must be between -840 and 840.
*/
public datetimetz(BaseDataType d, NumberType timeOrOffset)
{
if (!(d instanceof date))
{
ErrorManager.recordOrShowError(12117, "DATETIME-TZ", "");
// Unacceptable datatype for DATETIME-TZ argument (12117)
return;
}
initialize((date) d, timeOrOffset);
}
/**
* This is a special c'tor which should be used only when converting the
* value returned by a function or method with polymorphic return type into the
* expected type (i.e. DYNAMIC-FUNCTION()). In such cases, the 4GL does some
* automatic type conversion (see {@link #assign(BaseDataType)}).
*
* @param value
* The value to be used for this instance.
*/
public datetimetz(BaseDataType value)
{
if (value == null || value.isUnknown())
{
setUnknown();
}
else
{
if (value instanceof date)
{
assign(value);
}
else if (value instanceof Text)
{
String strVal = ((Text) value).getValue();
// make a first attempt to parse the string as ISO literal, save as backup
datetimetz isoDt = datetimetz.fromLiteral(strVal);
datetimetz[] tmp = new datetimetz[1];
Runnable code = () -> tmp[0] = new datetimetz(strVal);
// if ISO parsing was successful, ignore further errors otherwise we don't have the
// backup value so the ErrorManager will handle the bad syntax of date-format
try
{
if (isoDt != null)
{
ErrorManager.ignore(code);
}
else
{
code.run();
}
}
finally
{
if (tmp[0] != null && tmp[0].isUnknown() && isoDt == null)
{
int[] errCodes = { 5729, 5678 };
String[] errMsgs = {
"Incompatible datatypes found during runtime conversion",
"Unable to do run-time conversion of datatypes"
};
ErrorManager.recordOrShowError(errCodes, errMsgs, false, false);
}
}
// at this moment either the P4GL date-format parsing was successful or it failed but
// we have a isoDt as backup (that put the ErrorManager into ignore state)
if (!tmp[0].isUnknown())
{
// P4GL date-format parsing was successful use this value
assign(tmp[0]);
}
else
{
assign(isoDt);
}
}
else if (value instanceof jobject)
{
assign((jobject<?>) value);
}
else
{
incompatibleTypesOnConversion();
}
}
}
/**
* Constructs an instance with the exact same state as the passed
* instance. This can be used both as a copy constructor as well as an
* implementation of the DATETIME-TZ() built-in function when called with a
* <code>date</code> subclass.
*
* @param d
* The instance from which to copy state.
*/
public datetimetz(date d)
{
assign(d);
}
/**
* Constructs an instance using the inputs, with varying behavior depending
* on the type of the first parameter.
* <p>
* If the first parameter is a <code>date</code> instance, then the date is
* copied from that instance and the second parameter is treated as the
* time value (number of milliseconds since midnight).
* <p>
* If the first parameter is a <code>datetime</code> or
* <code>datetimetz</code> instance, then the date and time is copied from
* the instance and the second parameter is treated as a positive or
* negative timezone offset for the new instance.
* <p>
* This is an implementation of 3 forms of the DATETIME-TZ() built-in
* function.
* <p>
* The time value (positive or negative) is added to the date and the
* resulting value will be used to determine the date and time based on
* the default timezone. In other words, the date value can change in
* either direction by an arbitrary magnitude. This is how it is done in
* the 4GL.
* <p>
* Any input that is <code>unknown</code> will cause this instance to be
* <code>unknown</code>.
*
* @param d
* The date (or date subclass) from which to copy.
* @param timeOrOffset
* The milliseconds since midnight if the first parameter is a
* <code>date</code> or a timezone offset from UTC in minutes
* if the first parameter is a <code>datetime</code>. As an
* offset, the value must be between -840 and 840.
*/
public datetimetz(date d, NumberType timeOrOffset)
{
initialize(d, timeOrOffset);
}
/**
* Create an instance from the given date and time, via numeric values.
*
* @param d
* The date as a numeric value.
* @param timeOrOffset
* The time as a numeric value.
*/
public datetimetz(NumberType d, NumberType timeOrOffset)
{
UnimplementedFeature.missing("datetimetz(NumberType, NumberType)");
}
/**
* Constructs an instance using the inputs, with varying behavior depending
* on the type of the first parameter.
* <p>
* If the first parameter is a <code>date</code> instance, then the date is
* copied from that instance and the second parameter is treated as the
* time value (number of milliseconds since midnight).
* <p>
* If the first parameter is a <code>datetime</code> or
* <code>datetimetz</code> instance, then the date and time is copied from
* the instance and the second parameter is treated as a positive or
* negative timezone offset for the new instance.
* <p>
* This is an implementation of 3 forms of the DATETIME-TZ() built-in
* function.
* <p>
* The time value (positive or negative) is added to the date and the
* resulting value will be used to determine the date and time based on
* the default timezone. In other words, the date value can change in
* either direction by an arbitrary magnitude. This is how it is done in
* the 4GL.
* <p>
* Any input that is <code>unknown</code> will cause this instance to be
* <code>unknown</code>.
*
* @param d
* The date (or date subclass) from which to copy.
* @param timeOrOffset
* The milliseconds since midnight if the first parameter is a
* <code>date</code> or a timezone offset from UTC in minutes
* if the first parameter is a <code>datetime</code>. As an
* offset, the value must be between -840 and 840.
*/
public datetimetz(date d, double timeOrOffset)
{
initialize(d, new decimal(timeOrOffset));
}
/**
* Constructs an instance using the inputs.
* <p>
* This is an implementation of 1 form of the DATETIME-TZ() built-in function.
* <p>
* The time value (positive or negative) is added to the date and the
* resulting value will be used to determine the date and time based on
* the given timezone. In other words, the date value can change in
* either direction by an arbitrary magnitude. This is how it is done in
* the 4GL.
* <p>
* Any input that is <code>unknown</code> will cause this instance to be
* <code>unknown</code>.
*
* @param d
* The date from which to copy. This must be an instance of the
* <code>date</code> superclass and may NOT be an instance of the
* <code>datetime</code> or <code>datetimetz</code> since that is
* a 4GL error.
* @param time
* The milliseconds since midnight.
* @param offset
* The timezone offset from UTC in minutes. Must be between -840 and 840 inclusive.
*/
public datetimetz(date d, NumberType time, NumberType offset)
{
initialize(d, time, offset);
}
/**
* Constructs an instance using the inputs.
* <p>
* This is an implementation of 1 form of the DATETIME-TZ() built-in
* function.
* <p>
* The time value (positive or negative) is added to the date and the
* resulting value will be used to determine the date and time based on
* the given timezone. In other words, the date value can change in
* either direction by an arbitrary magnitude. This is how it is done in
* the 4GL.
* <p>
* Any input that is <code>unknown</code> will cause this instance to be
* <code>unknown</code>.
*
* @param d
* The date from which to copy. This must be an instance of the
* <code>date</code> superclass and may NOT be an instance of the
* <code>datetime</code> or <code>datetimetz</code> since that is
* a 4GL error.
* @param time
* The milliseconds since midnight.
* @param offset
* The timezone offset from UTC in minutes. Must be between -840 and 840 inclusive.
*/
public datetimetz(date d, NumberType time, double offset)
{
initialize(d, time, new decimal(offset));
}
/**
* Constructs an instance using the inputs.
* <p>
* This is an implementation of 1 form of the DATETIME-TZ() built-in
* function.
* <p>
* The time value (positive or negative) is added to the date and the
* resulting value will be used to determine the date and time based on
* the given timezone. In other words, the date value can change in
* either direction by an arbitrary magnitude. This is how it is done in
* the 4GL.
* <p>
* Any input that is <code>unknown</code> will cause this instance to be
* <code>unknown</code>.
*
* @param d
* The date from which to copy. This must be an instance of the
* <code>date</code> superclass and may NOT be an instance of the
* <code>datetime</code> or <code>datetimetz</code> since that is a 4GL error.
* @param time
* The milliseconds since midnight.
* @param offset
* The timezone offset from UTC in minutes. Must be between -840 and 840 inclusive.
*/
public datetimetz(date d, double time, NumberType offset)
{
initialize(d, new decimal(time), offset);
}
/**
* Constructs an instance using the inputs.
* <p>
* This is an implementation of 1 form of the DATETIME-TZ() built-in function.
* <p>
* The time value (positive or negative) is added to the date and the
* resulting value will be used to determine the date and time based on
* the given timezone. In other words, the date value can change in
* either direction by an arbitrary magnitude. This is how it is done in the 4GL.
* <p>
* Any input that is <code>unknown</code> will cause this instance to be
* <code>unknown</code>.
*
* @param d
* The date from which to copy. This must be an instance of the
* <code>date</code> superclass and may NOT be an instance of the
* <code>datetime</code> or <code>datetimetz</code> since that is a 4GL error.
* @param time
* The milliseconds since midnight.
* @param offset
* The timezone offset from UTC in minutes. Must be between -840 and 840 inclusive.
*/
public datetimetz(date d, double time, double offset)
{
initialize(d, new decimal(time), new decimal(offset));
}
/**
* Construct a datetime instance using explicit values for each discrete
* component. This is a variant of the DATETIME-TZ() built-in function.
*
* @param month
* The month number from 1 to 12 inclusive.
* @param day
* The day of the month from 1 to the maximum days in that month,
* inclusive
* @param year
* The Progress 4GL year which may be a negative number which
* represents the BC era or a positive AD era year. The value must
* be between -32768 and 32767 and it cannot be 0 since the year
* 0 is invalid in Progress. The value will be windowed into the
* right century if it is 2 digits. It will be used unchanged if
* it is 3 or more digits.
* @param hours
* The hour of the day from 0 to 23 inclusive.
* @param minutes
* The minute of the hour from 0 to 59 inclusive.
* @param seconds
* The seconds of the minute from 0 to 62 inclusive. This accounts
* for leap seconds. Please note that Progress documents 61 as
* the limit. It turns out that 62 is the real limit.
* @param millis
* The milliseconds of the second from 0 to 999 inclusive.
*/
public datetimetz(double month,
double day,
double year,
double hours,
double minutes,
double seconds,
double millis)
{
// the super class will take care of the date part
super(month, day, year);
setTime(new decimal(hours), new decimal(minutes), new decimal(seconds),
new decimal(millis), new int64(datetimetz.getDefaultTimezoneOffset(this)));
}
/**
* Construct a datetime instance using explicit values for each discrete
* component. This is a variant of the DATETIME-TZ() built-in function.
*
* @param month
* The month number from 1 to 12 inclusive.
* @param day
* The day of the month from 1 to the maximum days in that month,
* inclusive
* @param year
* The Progress 4GL year which may be a negative number which
* represents the BC era or a positive AD era year. The value must
* be between -32768 and 32767 and it cannot be 0 since the year
* 0 is invalid in Progress. The value will be windowed into the
* right century if it is 2 digits. It will be used unchanged if
* it is 3 or more digits.
* @param hours
* The hour of the day from 0 to 23 inclusive.
* @param minutes
* The minute of the hour from 0 to 59 inclusive.
* @param seconds
* The seconds of the minute from 0 to 62 inclusive. This accounts
* for leap seconds. Please note that Progress documents 61 as
* the limit. It turns out that 62 is the real limit.
* @param millis
* The milliseconds of the second from 0 to 999 inclusive.
* The offset in minutes from UTC. Must be between -840 and 840
* inclusive.
*/
public datetimetz(double month,
double day,
double year,
double hours,
double minutes,
double seconds,
double millis,
double offset)
{
// the super class will take care of the date part
super(month, day, year);
setTime(new decimal(hours),
new decimal(minutes),
new decimal(seconds),
new decimal(millis),
new decimal(offset));
}
/**
* Construct a datetime instance using explicit values for each discrete
* component. This is a variant of the DATETIME-TZ() built-in function.
*
* @param month
* The month number from 1 to 12 inclusive.
* @param day
* The day of the month from 1 to the maximum days in that month,
* inclusive
* @param year
* The Progress 4GL year which may be a negative number which
* represents the BC era or a positive AD era year. The value must
* be between -32768 and 32767 and it cannot be 0 since the year
* 0 is invalid in Progress. The value will be windowed into the
* right century if it is 2 digits. It will be used unchanged if
* it is 3 or more digits.
* @param hours
* The hour of the day from 0 to 23 inclusive.
* @param minutes
* The minute of the hour from 0 to 59 inclusive.
* @param seconds
* The seconds of the minute from 0 to 62 inclusive. This accounts
* for leap seconds. Please note that Progress documents 61 as
* the limit. It turns out that 62 is the real limit.
*/
public datetimetz(double month,
double day,
double year,
double hours,
double minutes,
double seconds)
{
// the super class will take care of the date part
super(month, day, year);
setTime(new decimal(hours), new decimal(minutes), new decimal(seconds),
int64.of(0L), int64.of((long)datetimetz.getDefaultTimezoneOffset(this)));
}
/**
* Construct a datetime instance using explicit values for each discrete
* component. This is a variant of the DATETIME-TZ() built-in function.
*
* @param month
* The month number from 1 to 12 inclusive.
* @param day
* The day of the month from 1 to the maximum days in that month,
* inclusive
* @param year
* The Progress 4GL year which may be a negative number which
* represents the BC era or a positive AD era year. The value must
* be between -32768 and 32767 and it cannot be 0 since the year
* 0 is invalid in Progress. The value will be windowed into the
* right century if it is 2 digits. It will be used unchanged if
* it is 3 or more digits.
* @param hours
* The hour of the day from 0 to 23 inclusive.
* @param minutes
* The minute of the hour from 0 to 59 inclusive.
*/
public datetimetz(double month, double day, double year, double hours, double minutes)
{
// the super class will take care of the date part
super(month, day, year);
setTime(new decimal(hours), new decimal(minutes), int64.of(0L), int64.of(0L),
int64.of((long)datetimetz.getDefaultTimezoneOffset(this)));
}
/**
* Construct a datetime instance using explicit values for each discrete
* component. This is a variant of the DATETIME-TZ() built-in function. Any
* component that is <code>unknown</code> will result in the instance being
* <code>unknown</code>.
*
* @param month
* The month number from 1 to 12 inclusive.
* @param day
* The day of the month from 1 to the maximum days in that month,
* inclusive
* @param year
* The Progress 4GL year which may be a negative number which
* represents the BC era or a positive AD era year. The value must
* be between -32768 and 32767 and it cannot be 0 since the year
* 0 is invalid in Progress. The value will be windowed into the
* right century if it is 2 digits. It will be used unchanged if
* it is 3 or more digits.
* @param hours
* The hour of the day from 0 to 23 inclusive.
* @param minutes
* The minute of the hour from 0 to 59 inclusive.
* @param seconds
* The seconds of the minute from 0 to 62 inclusive. This accounts
* for leap seconds. Please note that Progress documents 61 as
* the limit. It turns out that 62 is the real limit.
* @param millis
* The milliseconds of the second from 0 to 999 inclusive.
*/
public datetimetz(NumberType month,
NumberType day,
NumberType year,
NumberType hours,
NumberType minutes,
NumberType seconds,
NumberType millis)
{
// the super class will take care of the date part
super(month, day, year);
// use the version of setTime that handles unknown properly
setTime(hours, minutes, seconds, millis, int64.of(0L));
}
/**
* Construct a datetime instance using explicit values for each discrete
* component. This is a variant of the DATETIME-TZ() built-in function. Any
* component that is <code>unknown</code> will result in the instance being
* <code>unknown</code>.
*
* @param month
* The month number from 1 to 12 inclusive.
* @param day
* The day of the month from 1 to the maximum days in that month,
* inclusive
* @param year
* The Progress 4GL year which may be a negative number which
* represents the BC era or a positive AD era year. The value must
* be between -32768 and 32767 and it cannot be 0 since the year
* 0 is invalid in Progress. The value will be windowed into the
* right century if it is 2 digits. It will be used unchanged if
* it is 3 or more digits.
* @param hours
* The hour of the day from 0 to 23 inclusive.
* @param minutes
* The minute of the hour from 0 to 59 inclusive.
* @param seconds
* The seconds of the minute from 0 to 62 inclusive. This accounts
* for leap seconds. Please note that Progress documents 61 as
* the limit. It turns out that 62 is the real limit.
* @param millis
* The milliseconds of the second from 0 to 999 inclusive.
* @param offset
* The offset in minutes from UTC. Must be between -840 and 840
* inclusive.
*/
public datetimetz(NumberType month,
NumberType day,
NumberType year,
NumberType hours,
NumberType minutes,
NumberType seconds,
NumberType millis,
NumberType offset)
{
// the super class will take care of the date part
super(month, day, year);
// use the version of setTime that handles unknown properly
setTime(hours, minutes, seconds, millis, offset);
}
/**
* Construct a datetime instance using explicit values for each discrete
* component. This is a variant of the DATETIME-TZ() built-in function. Any
* component that is <code>unknown</code> will result in the instance being
* <code>unknown</code>.
*
* @param month
* The month number from 1 to 12 inclusive.
* @param day
* The day of the month from 1 to the maximum days in that month,
* inclusive
* @param year
* The Progress 4GL year which may be a negative number which
* represents the BC era or a positive AD era year. The value must
* be between -32768 and 32767 and it cannot be 0 since the year
* 0 is invalid in Progress. The value will be windowed into the
* right century if it is 2 digits. It will be used unchanged if
* it is 3 or more digits.
* @param hours
* The hour of the day from 0 to 23 inclusive.
* @param minutes
* The minute of the hour from 0 to 59 inclusive.
* @param seconds
* The seconds of the minute from 0 to 62 inclusive. This accounts
* for leap seconds. Please note that Progress documents 61 as
* the limit. It turns out that 62 is the real limit.
*/
public datetimetz(NumberType month,
NumberType day,
NumberType year,
NumberType hours,
NumberType minutes,
NumberType seconds)
{
// the super class will take care of the date part
super(month, day, year);
// use the version of setTime that handles unknown properly
setTime(hours, minutes, seconds, int64.of(0L),
int64.of((long)datetimetz.getDefaultTimezoneOffset(this)));
}
/**
* Construct a datetime instance using explicit values for each discrete
* component. This is a variant of the DATETIME-TZ() built-in function. Any
* component that is <code>unknown</code> will result in the instance being
* <code>unknown</code>.
*
* @param month
* The month number from 1 to 12 inclusive.
* @param day
* The day of the month from 1 to the maximum days in that month,
* inclusive
* @param year
* The Progress 4GL year which may be a negative number which
* represents the BC era or a positive AD era year. The value must
* be between -32768 and 32767 and it cannot be 0 since the year
* 0 is invalid in Progress. The value will be windowed into the
* right century if it is 2 digits. It will be used unchanged if
* it is 3 or more digits.
* @param hours
* The hour of the day from 0 to 23 inclusive.
* @param minutes
* The minute of the hour from 0 to 59 inclusive.
*/
public datetimetz(NumberType month,
NumberType day,
NumberType year,
NumberType hours,
NumberType minutes)
{
// the super class will take care of the date part
super(month, day, year);
// use the version of setTime that handles unknown properly
setTime(hours, minutes, new int64(0), new int64(0), new int64(0));
}
/**
* Creates a new instance from the given date and time specification. This
* is one form of the DATETIME-TZ built-in function. This is also the form of
* the constructor that is used to handle initializer expressions (literals).
* <p>
* The specification must match the following format rules:
* <p>
* <ol>
* <li> Date value (this is not optional). The string cannot start with
* whitespace. The first input is a date value that has at least a
* month and day specified or a month, day and year. The values
* are parsed in order as specified in SESSION:DATE-FORMAT. Zero
* padding is allowed but not required.
* <li> Date and time delimiter (not optional). Whitespace or a letter T
* (upper or lowercase) which is followed by whitespace. If the letter
* T is used, it cannot be preceded by whitespace. No other characters
* are allowed.
* <li> Time value (optional). If not specified, it is set to 0. It is
* specified in HH:MM:SS.sss format, however right side components
* can be left off (so HH, HH:MM, HH:MM:SS and HH:MM:SS.sss are all
* valid forms). Zero padding is allowed but not required, so hours
* can be H or HH, minutes M or MM, seconds S or SS and milliseconds
* can be s, ss or sss. Delimiters can only be there if the following
* component is included.
* <li> Whitespace (optional).
* <li> Timezone specification (optional). This is in the form of a sign
* (+ or -) then the hours (1 or 2 digits) a colon delimiter and then
* minites (1 or 2 digits). Zero padding is allowed but not required.
* The minutes can be left off but if so, then the delimiter must
* also be left off. If specified, the instance will be set using
* the timezone specified EVEN THOUGH THE TIMEZONE OFFSET IS NOT
* STORED (since this is not a datetimetz).
* <li> Whitespace (optional).
* </ol>
*
* @param spec
* The date and time specification.
*/
public datetimetz(String spec)
throws ErrorConditionException
{
instantiateFromStringWorker(spec, null/*auto*/, -1);
if (!isUnknown() && offset == INVALID_TZ)
{
offset = datetimetz.getDefaultTimezoneOffset(this);
}
}
/**
* Converts a string to a datetime-tz using a specified format and an optional windowing year
* for date part. The purpose of this constructor is to be used with PL/Java as there is no
* way to access P2J SESSION's attributes on SQL server side.
*
* @see datetimetz#datetimetz(String)
*
* @param spec
* The date and time specification.
* @param format
* The format string used for date conversion.
* @param windowingYear
* The start of the 100 year window used for conversion when year is in 2-digits.
*/
public datetimetz(String spec, String format, int windowingYear)
throws ErrorConditionException
{
instantiateFromStringWorker(spec, format, windowingYear);
if (!isUnknown() && offset == INVALID_TZ)
{
offset = datetimetz.getDefaultTimezoneOffset(this);
}
}
/**
* Creates a new instance from the given date and time specification. This
* is one form of the DATETIME-TZ built-in function. If the input is
* <code>unknown</code> the instance will be <code>unknown</code>.
* <p>
* The specification must match the following format rules:
* <p>
* <ol>
* <li> Date value (this is not optional). The string cannot start with
* whitespace. The first input is a date value that has at least a
* month and day specified or a month, day and year. The values
* are parsed in order as specified in SESSION:DATE-FORMAT. Zero
* padding is allowed but not required.
* <li> Date and time delimiter (not optional). Whitespace or a letter T
* (upper or lowercase) which is followed by whitespace. If the letter
* T is used, it cannot be preceded by whitespace. No other characters
* are allowed.
* <li> Time value (optional). If not specified, it is set to 0. It is
* specified in HH:MM:SS.sss format, however right side components
* can be left off (so HH, HH:MM, HH:MM:SS and HH:MM:SS.sss are all
* valid forms). Zero padding is allowed but not required, so hours
* can be H or HH, minutes M or MM, seconds S or SS and milliseconds
* can be s, ss or sss. Delimiters can only be there if the following
* component is included.
* <li> Whitespace (optional).
* <li> Timezone specification (optional). This is in the form of a sign
* (+ or -) then the hours (1 or 2 digits) a colon delimiter and then
* minites (1 or 2 digits). Zero padding is allowed but not required.
* The minutes can be left off but if so, then the delimiter must
* also be left off. If specified, the instance will be set using
* the timezone specified EVEN THOUGH THE TIMEZONE OFFSET IS NOT
* STORED (since this is not a datetimetz).
* <li> Whitespace (optional).
* </ol>
*
* @param spec
* The date and time specification.
*/
public datetimetz(character spec)
throws ErrorConditionException
{
if (spec.isUnknown())
{
setUnknown();
}
else
{
instantiateFromStringWorker(spec.toStringMessage(), null/*auto*/, -1);
// set the correct timezone in case it is not part of the string:
if (!isUnknown() && offset == INVALID_TZ)
{
offset = datetimetz.getDefaultTimezoneOffset(this);
}
}
}
/**
* Creates a new instance from the given calendar value.
*
* @param calendar
* The Calendar to be used for creating new instance.
*/
public datetimetz(Calendar calendar)
{
if (calendar == null)
{
setUnknown();
return;
}
super.deepAssign(new datetime(calendar.getTime(), calendar.getTimeZone()));
TimeZone tz = calendar.getTimeZone();
offset = (tz.getRawOffset() + tz.getDSTSavings()) / (60 * 1000);
}
/**
* Creates a new instance from the given {@code OffsetDateTime} value.
*
* @param odt
* The {code OffsetDateTime} to be used for creating new instance.
*/
public datetimetz(OffsetDateTime odt)
{
this(odt.getMonthValue(), odt.getDayOfMonth(), odt.getYear(),
odt.getHour(), odt.getMinute(), odt.getSecond(), odt.getNano() / 1000_000D,
odt.getOffset().getTotalSeconds() / 60D);
}
/**
* Creates an instance from a <code>java.util.Date</code> instance.
* <p>
* <b>WARNING: this will only work if the calendar in which this datetimetz
* was constructed is the <code>GregorianCalendar</code> AND if the
* timezone in which this object was created is the same as the current
* default for this JVM instance.</b> If this is not correct then you
* must use the {@link com.goldencode.p2j.util.datetimetz#datetimetz(Date,TimeZone)} version.
*
* @param d
* The <code>Date</code> instance from which to pattern the instance.
*/
public datetimetz(Date d)
{
this(d, TimeZone.getDefault());
}
/**
* Creates an instance from a <code>java.util.Date</code> instance.
*
* @param d
* The <code>Date</code> instance from which to pattern the instance.
* @param offset
* A timezone offset from UTC in minutes.
*/
public datetimetz(Date d, int offset)
{
this(d, TimeZone.getTimeZone(ZoneOffset.ofTotalSeconds(offset * 60)));
}
/**
* Creates an instance from a <code>java.util.Date</code> instance.
*
* @param d
* The <code>Date</code> instance from which to pattern the instance.
* @param zone
* The <code>TimeZone</code> for the calendar to be used for <code>d</code>.
*/
public datetimetz(Date d, TimeZone zone)
{
super(d, zone);
offset = (zone.getRawOffset() + zone.getDSTSavings()) / (60 * 1000);
}
/**
* Parses this ISO 8601 value as a datetime-tz.
* <p>
* This API relies on {@link #instantiateFromStringWorker} to parse the literal.
*
* @param val
* The date to parse.
*
* @return The date value.
*/
public static date parseLiteral(String val)
{
datetimetz d = new datetimetz();
d.instantiateFromStringWorker(val, DEFAULT_DATETIME_FORMAT, -1);
return d;
}
/**
* Creates an instance that represents current date and time using the default timezone.
* This is the equivalent of the NOW built-in function.
*
* @return an instance of datetimetz representing the current moment.
*/
public static datetimetz now()
{
// NOW uses the SESSION:TIME-ZONE if set or local-time otherwise
integer sessionOffset = date.getTimeZone();
int sessOffset = sessionOffset.isUnknown() ?
datetimetz.getDefaultTimezoneOffset(null) :
sessionOffset.intValue();
return new datetimetz(datetime.now(), sessOffset);
}
/**
* Obtains the timezone offset in minutes from UTC for this instance. This is the equivalent of the
* TIMEZONE(datetime-tz) built-in function.
*
* @return The timezone offset for this instance.
*/
public static int64 getTimeZoneOffset(BaseDataType value)
{
if (value instanceof datetimetz)
{
return ((datetimetz) value).getTimeZoneOffset();
}
else if (value instanceof Text)
{
return date.getOffsetForSpec(((Text) value).toJavaType());
}
ErrorManager.recordOrShowError(11872, "", "");
// Invalid timezone expression (11872)
return new int64();
}
/**
* Moves the current datetime-tz to another timezone offset. It is guaranteed to return another
* object instance and this object remains unchanged.
*
* @param offsetInMinutes
* The new timezone offset to move this object.
*
* @return a new instance of the datetimetz shifted to requested timezone offset.
*/
public datetimetz toOffset(int offsetInMinutes)
{
if (isUnknown())
{
return new datetimetz();
}
if (offsetInMinutes == this.offset)
{
return (datetimetz) this.duplicate();
}
datetimetz ret = datetimetz.plusMillis(this, (offsetInMinutes - offset) * 60 * 1000L);
ret.offset = offsetInMinutes;
return ret;
}
/**
* Parse the INITIAL string of a datetime-tz var definition.
*
* @param literal
* The datetime-tz literal, which will always have the date part in 'mdy' format.
*
* @return The datetime-tz instance initialized with this literal.
*/
public static datetimetz init(String literal)
{
return datetimetz.parseInitial(literal);
}
/**
* Check if the <code>str</code> is a Progress datetime-tz literal (in ISO8601 format string
* representation), parse it and return the <code>datetimetz</code> object.
* The recognized formats are:
* <pre>
* YYYY-MM-ddTHH:mm[:S[S].s[s[s[...]]]+H[[H]:M[M]]]
* </pre>
* where the date-separator can be either 't' or 'T' and timezone can be both positive '+'
* or negative '-'. The hours and minutes are mandatory in 2 digits and seconds, tz-hours and
* tz-minutes can be represented as 1 or 2 digits. The seconds parts can be tenths of seconds
* (one digit), hundredths (2 digits) and milliseconds (3 digits after '.'). If more digits
* appear after '.'they are ignored.
*
* Normally the timezone should be present, otherwise P2J parser wouldn't classify it as
* a datetime-tz literal.
*
* @param str
* The text to be parsed.
*
* @return a datetime-tz object whose string literal representation was given as argument on
* success or null if the passed argument does not fit the known pattern.
*/
public static datetimetz fromLiteral(String str)
{
Matcher matcher = DTZ_PATTERN.matcher(str);
if (!matcher.matches())
{
// failed, spec is not in ISO8601 format
return null;
}
int year = 0;
int month = 0;
int day = 0;
int hour = 0;
int minutes = 0;
int seconds = 0;
int millis = 0;
int tzHours = 0;
int tzMinutes = 0;
boolean negativeTz = false;
// parse captured tokens
try
{
// mandatory parts:
year = Integer.parseInt(matcher.group(1));
month = Integer.parseInt(matcher.group(3));
day = Integer.parseInt(matcher.group(4));
hour = Integer.parseInt(matcher.group(5));
minutes = Integer.parseInt(matcher.group(6));
// optional components:
if (matcher.group(8) != null)
{
seconds = Integer.parseInt(matcher.group(8));
}
if (matcher.group(9) != null)
{
String milliDigits = matcher.group(9).substring(1); // skip the .;
if (milliDigits.length() > 3)
{
// more than 3 digits are allowed for milliseconds,
// but only the first 3 digits are used, the other are dropped
milliDigits = milliDigits.substring(0, 3);
}
millis = Integer.parseInt(milliDigits);
if (milliDigits.length() == 1)
{
// if only one digit is used for millis they are tenths instead
millis *= 100;
}
else if (milliDigits.length() == 2)
{
// if only two digits are used for millis they are hundredths instead
millis *= 10;
}
}
if (matcher.group(13) != null)
{
tzHours = Integer.parseInt(matcher.group(13));
}
if (matcher.group(15) != null)
{
tzMinutes = Integer.parseInt(matcher.group(15));
}
negativeTz = "-".equals(matcher.group(12));
}
catch (NumberFormatException e)
{
// this should not happen, anyway since groups should be made of digits only
// if cannot parse than it's not in the correct format
return null;
}
datetimetz ret = new datetimetz();
long julian = ret.instantiateDate(year, month, day);
if (julian == INVALID_DATE)
{
return null;
}
ret.setDayNumber(julian);
ret.setTime(hour, minutes, seconds, millis);
// honoring the timezone stuff when calculating the milliseconds
int tzOffset = tzHours * 60 + tzMinutes;
if (negativeTz)
{
tzOffset = -tzOffset;
}
ret.setTimeZoneOffset(tzOffset);
// set parsed groups to internal data of the object
return ret;
}
/**
* Creates a new <code>datetimetz</code> instance that represents the
* <code>unknown value</code>.
* <p>
* This method is deprecated. Use the default constructor instead.
*
* @return An instance that represents the <code>unknown value</code>.
*/
@Deprecated
public static datetimetz instantiateUnknownDatetimeTz()
{
return new datetimetz();
}
/**
* Creates a new instance of the same type that represents the <code>unknown value</code>.
*
* @return An instance that represents the <code>unknown value</code>.
*/
@Override
public BaseDataType instantiateUnknown()
{
return new datetimetz();
}
/**
* Creates a new instance of the same type that represents the default initialized value.
*
* @return An instance that represents the default value.
*/
@Override
public BaseDataType instantiateDefault()
{
return new datetimetz(); // unknown value
}
/**
* Return the default display format string for this type.
*
* @return The default format string.
*/
@Override
public String defaultFormatString()
{
return defaultFormat;
}
/**
* Obtain the length (in bytes) of this BDT will use when serialized.
*
* @return the length of this BDT will use when serialized.
*/
@Override
public int getSize()
{
if (isUnknown())
{
return 1;
}
if (offset == 0 && intValue() == RAW_EPOCH && getTime() == 0)
{
return 1;
}
if (offset != 0 || intValue() != RAW_EPOCH )
{
return 13;
}
return 9 + new integer(getTime()).getSize();
}
/**
* Get the type
* @return type
*/
@Override
public Type getType()
{
return Type.DATETIMETZ;
}
/**
* Sets the state (data and unknown value) of this instance based on the
* state of the passed instance.
* <p>
* If the value is not of type <code>datetimetz</code>, the following automatic type
* conversion will occur:
* <p>
* <ol>
* <li>if the value is a date/datetime/datetimetz value, the current value is used
* <li>if the value is unknown, sets the instance to unknown
* <li>if the value is of type logical, the value is converted to integer (1 / 0) and then
* evaluate for the new value
* <li>if the value is a non-unknown numeric value, including recid,
* this uses the number as the julian day
* <li>if the value is a non-unknown character value, it attempts to parse this as a
* properly formatted date
* <li>otherwise it will recordOrThrowError number 5729.
* </ol>
* <p>
* This conversion is meant to handle the cases of built-in functions and methods
* in the 4GL which have polymorphic return types (e.g. DYNAMIC-FUNCTION()).
*
* @param value
* The instance from which to copy state.
*/
@Override
public void assign(BaseDataType value)
{
super.assign(value);
}
/**
* Sets the state of this instance based on the state of the passed instance.
* If time information is not available (<code>value</code> is a <code>date</code> or
* <code>datetime</code>) time and timezome, 00:00 is assumed.
*
* @param value
* The instance from which to copy state.
*/
@Override
public void assign(date value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
assign(value.val());
return;
}
if (value == null || value.isUnknown())
{
setUnknown();
return;
}
if (value instanceof datetimetz)
{
deepAssign((datetimetz)value);
}
else
{
if (value instanceof datetime)
{
deepAssign((datetime)value);
}
else
{
deepAssign(value);
setTime(0);
}
int[] dateParts = getYearMonthDay();
int newOffset = date.getDefaultOffset(date.createDate(dateParts[0], dateParts[1], dateParts[2]));
if (this.offset != newOffset)
{
checkUndoable(true);
}
this.offset = newOffset;
}
}
/**
* Sets the state (data and unknown value) and configuration (time-zone)
* of this instance based on the state of the passed instance.
*
* @param value
* The instance from which to copy state.
*/
public void deepAssign(datetimetz value)
{
if (value == null || value.isUnknown())
{
setUnknown();
return;
}
checkUndoable(value);
super.deepAssign(value);
offset = value.offset;
}
/**
* Compares this instance with the specified instance and returns a -1 if this instance is
* less than the specified, 0 if the two instances are equal and 1 if this instance is greater
* than the specified instance. This is the implementation of the <code>Comparable</code>
* interface.
* <p>
* The algorithm will fail to give meaningful results in the case where one tries to sort
* against other objects that do not represent compatible values.
* <p>
* Before comparing, both datetime-tz values are translated to UTC. There are no unique
* objects: the following are equals:
* 2001-06-30T22:59:59.999+05:00 EQ 2001-06-30T23:59:59.999+06:00
* and
* 2001-06-30T23:59:59.999+05:00 GT 2001-06-30T23:59:59.999+06:00
* is positive.
*
* @param obj
* The date instance to compare against.
*
* @return -1, 0 or 1 depending on if this instance is less than, equal to or greater
* (respectively) than the specified instance <code>obj</code>.
*/
@Override
public int compareTo(Object obj)
{
boolean compatible = obj instanceof date;
if ((obj instanceof datetime) && !(obj instanceof datetimetz))
{
// datetime-tz values are NOT comparable with datetime.
// in special conditions date values are compatible, see below
compatible = false;
}
if (!compatible)
{
ErrorManager.recordOrThrowError(
223, "Incompatible data types in expression or assignment");
return -1;
}
// The documentation say a date can be compared ONLY WITH other instances of date.
// This is true for compile-time values, the static checking will print error 223
// "Incompatible data types in expression or assignment" directly;
// However, if the compare operator receives different kind of date-related only the
// "common parts" are compared - ie. ignore additional fields from derived classes.
date thatDate = (date) obj;
if (!(thatDate instanceof datetime))
{
// must be a date, not datetime let it handle the comparison
// and return the mirrored result
return -thatDate.compareTo(this);
}
datetimetz that = (datetimetz) obj;
boolean u1 = this.isUnknown();
boolean u2 = that.isUnknown();
if (u1 && u2)
{
// two unknown are equals
return 0;
}
if (u1 || u2)
{
// only of is unknown, the other is not
return -1;
}
// compare the actual milliseconds from epoch translated to UTC
long thisAbsTime = this.longValue() * date.MILLIS_PER_DAY +
this.getTime() - this.offset * 60 * 1000L;
long thatAbsTime = that.longValue() * date.MILLIS_PER_DAY +
that.getTime() - that.offset * 60 * 1000L;
if (thisAbsTime > thatAbsTime)
{
return 1;
}
else if (thisAbsTime < thatAbsTime)
{
return -1;
}
// else they are equals
return 0;
}
/**
* An equality test which handles the most common cases with the least amount of overhead.
* The superclass implementation is invoked otherwise.
* <p>
* This implementation is consistent with {@link #hashCode()}.
*
* @see com.goldencode.p2j.util.BaseDataType#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (o instanceof datetimetz)
{
datetimetz that = (datetimetz) o;
boolean u1 = this.isUnknown();
boolean u2 = that.isUnknown();
if (u1 && u2)
{
return true;
}
if (u1 || u2)
{
return false;
}
long thisAbsTime = this.longValue() * date.MILLIS_PER_DAY +
this.getTime() - this.offset * 60 * 1000L;
long thatAbsTime = that.longValue() * date.MILLIS_PER_DAY +
that.getTime() - that.offset * 60 * 1000L;
return thisAbsTime == thatAbsTime;
}
return super.equals(o);
}
/**
* Hash code implementation which is consistent with {@link BaseDataType#equals}.
*
* @return Hash code value for this object instance.
*/
@Override
public int hashCode()
{
int result = 17;
if (isUnknown())
{
return result;
}
return (int) (37 * result + this.longValue() * date.MILLIS_PER_DAY +
this.getTime() - this.offset * 60 * 1000);
}
/**
* Does the same as standard <code>clone()</code> method but returns an instance of
* <code>BaseDataType</code> and doesn't throw the <code>CloneNotSupportedException</code>.
*
* @return A clone of this instance.
*/
@Override
public datetimetz duplicate()
{
return new datetimetz(this);
}
/**
* Obtains the timezone offset in minutes from UTC for this instance. This
* is the equivalent of the TIMEZONE(datetime-tz) built-in function.
*
* @return The timezone offset for this instance.
*/
public int64 getTimeZoneOffset()
{
return isUnknown() ? new int64() : new int64(offset);
}
/**
* Returns the legacy type name of the this class.
*
* @return Always "DATETIME-TZ".
*/
@Override
public String getTypeName()
{
return "DATETIME-TZ";
}
/**
* Get the offset in minutes from UTC for this instance.
*
* @return The offset. If valid it must be between -840 and 840 inclusive.
* It should not be invalid.
*/
public int getOffset()
{
return offset;
}
/**
* Set the offset in minutes from UTC for this instance.
*
* @param offset
* The offset, must be between -840 and 840 inclusive.
*/
@Override
public void setTimeZoneOffset(long offset)
{
if (offset < MIN_VALID_TZ || offset > MAX_VALID_TZ)
{
String err = "Invalid minute offset in datetime-tz value";
ErrorManager.recordOrThrowError(11254, err);
return;
}
if (this.offset != offset)
{
checkUndoable(true);
}
this.offset = (int) offset;
}
/**
* Sets the internal time and offset. This method will validate the
* parameters and raise an error if they are incorrect. Any input that is
* <code>unknown</code> will cause this instance to be <code>unknown</code>.
*
* @param hours
* The hour of the day from 0 to 23 inclusive.
* @param mins
* The minute of the hour from 0 to 59 inclusive.
* @param secs
* The seconds of the minute from 0 to 62 inclusive. This accounts
* for leap seconds. Please note that Progress documents 61 as
* the limit. It turns out that 62 is the real limit.
* @param millis
* The milliseconds of the second from 0 to 999 inclusive.
* @param offset
* The timezone offset in minutes from UTC. Must be between
* -840 and 840 inclusive.
*/
public void setTime(NumberType hours,
NumberType mins,
NumberType secs,
NumberType millis,
NumberType offset)
{
if (hours.isUnknown() ||
mins.isUnknown() ||
secs.isUnknown() ||
millis.isUnknown() ||
offset.isUnknown())
{
setUnknown();
}
else
{
setTime(hours.longValue(),
mins.longValue(),
secs.longValue(),
millis.longValue(),
offset.longValue());
}
}
/**
* Sets the internal time and offset. This method will validate the
* parameters and raise an error if they are incorrect.
*
* @param hours
* The hour of the day from 0 to 23 inclusive.
* @param mins
* The minute of the hour from 0 to 59 inclusive.
* @param secs
* The seconds of the minute from 0 to 62 inclusive. This accounts
* for leap seconds. Please note that Progress documents 61 as
* the limit. It turns out that 62 is the real limit.
* @param millis
* The milliseconds of the second from 0 to 999 inclusive.
* @param offset
* The timezone offset in minutes from UTC. Must be between
* -840 and 840 inclusive.
*/
public void setTime(long hours, long mins, long secs, long millis, long offset)
{
// let the superclass set the time value, do this before anything else to
// allow the error processing to occur first
super.setTime(hours, mins, secs, millis);
setTimeZoneOffset(offset);
}
/**
* Creates a SQL compatible string representation of the instance data
* (the format is "YYYY-MM-DD HH:MM:SS.zzz+HH:MM").
* <p>
* The year component is treated differently since it can have a sign (negative indicates
* BC era) and if the date is within the Y2K window it is truncated to fit.
*
* @return The formatted string or the 'failure' string if the date is
* too large to fit in the specified format.
*/
@Override
public String toStringSQL()
{
NumberFormat nf = NumberFormat.getInstance();
nf.setMinimumIntegerDigits(2);
nf.setGroupingUsed(false);
StringBuilder sb = new StringBuilder();
sb.append(super.toStringSQL()); // let the super class do its job
sb.append(' '); // this is the time-offset separator
// add the timezone components
sb.append((offset < 0) ? '-' : '+');
sb.append(nf.format(Math.abs(getTzHours()))); // the sign has already been appended
sb.append(':');
sb.append(nf.format(Math.abs(getTzMinutes())));
return sb.toString();
}
/**
* Creates a string representation of the instance data using the Progress 4GL 'export' format.
* This format is unusual in that it does not have a fixed width for the year but in fact it
* ensures that all years are formatted to at least 3 digits (with leading zeros) up to a
* maximum of 6 digits (for the year -32768 which is the widest possible Progress year) as
* necessary. The months and days are always formatted as '99'. Another 'quirk' of this
* format is that if the year is within the Y2K window, it is formatted as a 2 digit year.
* The separators are always '/' characters.
* The time part uses the standard format of 2 digits per components separated by ":", except
* for milliseconds which have three digits and are separated by ".".
* The timezone is also build using standard +/-HH:MM format.
*
* <p>
* This implementation creates the given string based on the order of
* date components (e.g. MDY, YMD...) defined for this context.
*
* @return The 'message' formatted string.
*/
@Override
public String toStringMessage()
{
// return toString(defaultFormat);
if (isUnknown())
{
return "?";
}
final StringBuilder sb = new StringBuilder();
// let the super class handle date and time components
sb.append(super.toStringMessage());
// add the timezone components
sb.append((offset < 0) ? '-' : '+');
final NumberFormat nf = NumberFormat.getInstance();
nf.setGroupingUsed(false);
nf.setMinimumIntegerDigits(2);
sb.append(nf.format(Math.abs(getTzHours()))); // the sign has already been appended
sb.append(':');
sb.append(nf.format(Math.abs(getTzMinutes())));
return sb.toString();
}
/**
* Obtains the text form of this date instance in ISO 8601 standard format.
*
* @return A string that is the ISO representation of this date
* ("YYYY-MM-DDTHH:MM:SS.SSS+HH:MM") or "?" if <code>unknown</code>.
*/
@Override
public String toStringExport()
{
if (isUnknown())
{
return "?";
}
return getIsoDate();
}
/**
* Replacement for the default object reading method. The latest state is read from the
* input source.
*
* @param in
* The input source from which fields will be restored.
*
* @throws IOException
* In case of I/O errors.
* @throws ClassNotFoundException
* If payload can't be instantiated.
*/
@Override
public void readExternal(ObjectInput in)
throws IOException,
ClassNotFoundException
{
// let the datetime class handle date and time component
super.readExternal(in);
// read the timezone offset component:
this.offset = in.readInt();
}
/**
* Replacement for the default object writing method. The latest state is written to the
* output destination.
*
* @param out
* The output destination to which fields will be saved.
*
* @throws IOException
* In case of I/O errors.
*/
@Override
public void writeExternal(ObjectOutput out)
throws IOException
{
// let the datetime class handle date and time component
super.writeExternal(out);
// write the timezone offset component:
out.writeInt(this.offset);
}
/**
* Return an object equal to this expressed in local time (current timezone offset).
*
* @return a datetime that is the representation of this object relative to local time.
*/
@Override
public datetime toLocalDatetime()
{
if (this.isUnknown())
{
return new datetime();
}
datetime ret = new datetime();
ret.deepAssign(this);
int localOffset = datetimetz.getDefaultTimezoneOffset(ret);
return plusMillis(ret, (localOffset - offset) * 60 * 1000L);
}
/**
* Obtains the text form of this date instance in ISO 8601 standard format.
*
* @return A string that is the ISO representation of this date
* ("YYYY-MM-DDTHH:MM:SS.SSS+HH:MM") or "?" if <code>unknown</code>.
*/
@Override
public String getIsoDate()
{
if (isUnknown())
{
return "?";
}
String isoDatetime = super.getIsoDate();
if ("?".equals(isoDatetime))
{
return "?";
}
StringBuilder sb = new StringBuilder(isoDatetime);
// add the timezone components
sb.append((offset < 0) ? '-' : '+');
NumberFormat nf = NumberFormat.getInstance();
nf.setGroupingUsed(false);
nf.setMinimumIntegerDigits(2);
sb.append(nf.format(Math.abs(getTzHours()))); // the sign has already been appended
sb.append(':');
sb.append(nf.format(Math.abs(getTzMinutes())));
return sb.toString();
}
/**
* Parses this ISO 8601 value as a datetime-tz.
*
* @param val
* The date to parse.
*
* @throws DateTimeParseException
* If the format is not ISO 8601 compliant.
*/
@Override
public void fromIso(String val)
{
OffsetDateTime odtz = OffsetDateTime.parse(val, ISO_OFFSET_DATE_TIME);
Date date = Date.from(Instant.from(odtz));
date d = new date(date);
int millis = odtz.get(ChronoField.MILLI_OF_DAY);
int offset = odtz.get(ChronoField.OFFSET_SECONDS) / 60;
assign(new datetimetz(d, millis, offset));
}
/**
* Parses the literal encountered in a INITIAL option. Does the best effort to parse the text of the
* literal taking into account the current session settings (like the date format and the windowing year
* and the moment when the method is invoked. The main purpose of this method is to select the proper
* date format and then create the proper {@code datetimetz} object with respective values.
* <p>
* {@code now} is also supported, in which case the current timestamp is returned.
*
* @param text
* The text of the literal to be parsed.
*
* @return A {@code datetime-tz} representing the respective value, taking into account the current
* session settings (like the date format and the windowing year).
*/
public static datetimetz parseInitial(String text)
{
if ("now".equalsIgnoreCase(text))
{
return now();
}
String df = getDynamicFormat();
if (df != null)
{
return new datetimetz(text, df, getWindowingYear());
}
if (text.length() >= 10)
{
// parsing a string literal is more lenient
if ((text.charAt(2) == '/' || text.charAt(2) == '-') &&
(text.charAt(5) == '/' || text.charAt(5) == '-'))
{
// identified dd/dd/dddd, must be MDY
return new datetimetz(text, DEFAULT_DATE_FORMAT, DEFAULT_DATE_WINDOWING_YEAR);
}
// assumed dddd/dd/dd, always in YMD
return new datetimetz(text, DEFAULT_DATETIME_FORMAT, DEFAULT_DATE_WINDOWING_YEAR);
}
// probably an incorrectly formed literal
return new datetimetz(text);
}
/**
* Computes the local offset (raw + DST) from UTC at a specified date.
* TODO: remove 'Default' token from name ?
*
* @param d
* The date for which the offset is computed. If null, the current JVM date is used.
*
* @return The local offset from UTC (in minutes) at the specified date taking into consideration the
* DST at that moment.
*/
public static int getDefaultTimezoneOffset(date d)
{
TimeZone tz = GregorianCalendar.getInstance().getTimeZone();
// TimeZone tz = date.getDefaultTimeZone();
Date time = (d == null) ? new Date() : d.dateValue();
return tz.getOffset(time.getTime()) / (60 * 1000);
}
/**
* Obtains the minutes of timezone offset.
*
* @return The minutes of timezone offset.
*/
@Override
public int getTzMinutes()
{
return offset % 60;
}
/**
* Obtains the hours of timezone offset.
*
* @return The hours of timezone offset.
*/
@Override
public int getTzHours()
{
return offset / 60;
}
/**
* Validate specified values and return error information if they don't consist of correct
* datetime components.
*
* @param month
* The month.
* @param day
* The day.
* @param year
* The year.
* @param forceFourDigits
* Flags that the year should be in 4 digits format.
* @param hours
* The hours.
* @param minutes
* The minutes.
* @param seconds
* The seconds.
* @param millis
* The milliseconds.
* @param ampm
* AM/PM indicator, or null/empty for 24H representation
* @param tz_hours
* Timezone hours (can be negative).
* @param tz_minutes
* Timezone minutes.
*
* @return <code>null</code> if components are valid and error information otherwise.
*/
public static ErrorEntry validate(int month,
int day,
int year,
boolean forceFourDigits,
int hours,
int minutes,
int seconds,
int millis,
String ampm,
int tz_hours,
int tz_minutes)
{
// check the date part using corresponding static function from date:
ErrorEntry err = date.validate(month, day, year, forceFourDigits);
if (err != null)
{
return err;
}
// now all that remained is to check the time parts:
if (hours < 0 || hours > 23)
{
return new ErrorEntry(11260, "Invalid hours in datetime value");
}
if (minutes < 0 || minutes > 59)
{
return new ErrorEntry(11256, "Invalid minutes in datetime value");
}
if (seconds < 0 || seconds > 61) // NOTE: leap seconds are allowed here!
{
return new ErrorEntry(11253, "Invalid seconds in datetime value");
}
if (millis < 0 || millis > 999)
{
return new ErrorEntry(11252, "Invalid milliseconds in datetime value");
}
if (ampm != null && !ampm.isEmpty())
{
ampm = ampm.toUpperCase();
if (!"AM".equals(ampm) && !"PM".equals(ampm) && !"A".equals(ampm) && !"P".equals(ampm))
{
// TODO: check this out
return new ErrorEntry(0, "Invalid AM/PM specifier");
}
if (hours < 1 || hours > 12)
{
return new ErrorEntry(11260, "Invalid hours in datetime value");
}
}
// now all that remained is to check the time parts:
if (tz_hours < -14 || tz_hours > 14)
{
return new ErrorEntry(11259,
"Invalid hour offset in the timezone portion of a datetime-tz value");
}
if (tz_minutes < 0 || tz_minutes > 59)
{
return new ErrorEntry(11254, "Invalid minute offset in datetime-tz value");
}
// declare it valid
return null;
}
/**
* Reinitialize date and time components of this object from the date parameter. The timezone
* offset remains unchanged.
* <p>
* This method is only used from <code>DatetimeTzUserType</code> when restoring an object form
* database. As it is stored as a compound type, the initialization happens in two atomic
* steps.
*
* @param date
* The date to be used for restoring date and time subcomponents.
*/
public void setTimestamp(Date date)
{
if (date == null)
{
setUnknown();
return;
}
// this may not be the best approach but it's fast to implement
super.deepAssign(new datetime(date));
}
/**
* Converts this object to a Calendar value to be used by Hibernate persistence.
*
* @return Calendar value of this object.
*/
public Calendar calendarValue()
{
if (isUnknown())
{
return null;
}
TimeZone tz = date.getZoneFromOffset(offset);
GregorianCalendar ret = new GregorianCalendar(tz);
ret.setTime(dateValue(tz));
return ret;
}
/**
* Converts this object to a {@code OffsetDateTime} value to be used by Hibernate persistence.
*
* @return {@code OffsetDateTime} value of this object.
*/
public OffsetDateTime offsetDateTimeValue()
{
Calendar cal = calendarValue();
if (cal == null)
{
return null;
}
return OffsetDateTime.ofInstant(cal.toInstant(), cal.getTimeZone().toZoneId());
}
/**
* Identify if this data is dependent upon the context local. This identifies
* if the data is safe to be cached in cross-session places or the data should
* not leak from the context. If is is not safe to be shared cross-session, then
* this should return {@code null}. If other representation is suitable for caching,
* then this can return another object that wraps internal values that are not
* dependent upon the context (e.g. date, date-time, date-time-tz).
*
* @return Always. {@code null}.
*/
@Override
public Object getIndependentFromContext()
{
return new ContextIndependentDateTimeTz(isUnknown(), this.longValue(), this.getTime(), offset);
}
/**
* Initialized this object using the provided arguments.
*
* @param d
* The date from which to copy. This must be an instance of the {@code date} superclass and may
* NOT be an instance of the {@code datetime} or {@code datetimetz} since that is a 4GL error.
* @param time
* The milliseconds since midnight.
* @param offset
* The timezone offset from UTC in minutes. Must be between -840 and 840 inclusive.
*/
private void initialize(date d, NumberType time, NumberType offset)
{
if (d.isUnknown() || time.isUnknown() || offset.isUnknown())
{
setUnknown();
return;
}
if (d instanceof datetime)
{
ErrorManager.recordOrThrowError(223, "", "");
// ** Incompatible data types in expression or assignment. (223)
return;
}
setDayNumber(d.intValue());
setTime(time.intValue());
setTimeZoneOffset(offset.intValue());
}
/**
* Initialized this object using the provided arguments.
*
* @param d
* The date (or date subclass) from which to copy.
* @param timeOrOffset
* The milliseconds since midnight if the first parameter is a {@code date} or a timezone offset
* from UTC in minutes if the first parameter is a {@code datetime}. As an offset, the value must
* be between -840 and 840.
*/
private void initialize(date d, NumberType timeOrOffset)
{
if (d.isUnknown() || timeOrOffset.isUnknown())
{
setUnknown();
return;
}
if (d instanceof datetimetz)
{
assign(new datetimetz(d).toOffset(timeOrOffset.intValue()));
}
else if (d instanceof datetime)
{
assign(d);
offset = timeOrOffset.intValue();
}
else
{
// this path is for the date superclass
super.deepAssign(d);
setTime(timeOrOffset.longValue());
offset = datetimetz.getDefaultTimezoneOffset(this);
}
}
/**
* Reset the time and offset for this instance.
*/
@Override
protected void resetTime()
{
resetTime(null);
}
/**
* Reset the time and offset for this instance.
*/
protected void resetTime(Date d)
{
super.resetTime();
this.offset = getDefaultOffset(d);
}
/**
* Private utility method that returns a Calendar value as string in ISO format
* (yyyy-MM-dd hh:mm:ss.ttt).
*
* @param c
* The calendar.
*
* @return The ISO string representing the calendar instance.
*/
private static String toString(Calendar c)
{
if (c == null)
{
return "";
}
return isoFormat(
c.get(Calendar.YEAR),
c.get(Calendar.MONTH) + 1,
c.get(Calendar.DAY_OF_MONTH),
c.get(Calendar.HOUR_OF_DAY),
c.get(Calendar.MINUTE),
c.get(Calendar.SECOND),
c.get(Calendar.MILLISECOND),
(c.get(Calendar.DST_OFFSET) + c.get(Calendar.ZONE_OFFSET)) / (1000 * 60)
);
}
/**
* Private utility method that returns a date (by components) as string in ISO format
* (yyyy-MM-dd hh:mm:ss.ttt).
*
* @param y
* the year (4 digits)
* @param M
* the month (1-12)
* @param d
* the day (1-31)
* @param h
* the hour of the day (0-23)
* @param m
* the minute of the hour (0-59)
* @param s
* the second (0-59)
* @param t
* the milliseconds (0-999)
* @param z
* the time offset from UTC (in minutes, -840 - 840)
*
* @return a string representation of the date in ISO format.
*/
private static String isoFormat(int y, int M, int d, int h, int m, int s, int t, int z) {
NumberFormat nf = NumberFormat.getInstance();
nf.setGroupingUsed(false);
StringBuilder sb = new StringBuilder();
nf.setMinimumIntegerDigits(4);
// date:
sb.append(nf.format(y));
sb.append('-');
nf.setMinimumIntegerDigits(2);
sb.append(nf.format(M));
sb.append('-');
sb.append(nf.format(d));
sb.append('T');
// time:
sb.append(nf.format(h));
sb.append(':');
sb.append(nf.format(m));
sb.append(':');
sb.append(nf.format(s));
sb.append('.');
nf.setMinimumIntegerDigits(3);
sb.append(nf.format(t));
// tz offset
nf.setMinimumIntegerDigits(2);
sb.append(z >= 0 ? '+' : '-');
z = Math.abs(z);
sb.append(nf.format(z / 60));
sb.append(':');
sb.append(nf.format(z % 60));
return sb.toString();
}
/**
* Convenient class that wraps the low-level {@link #julian}, {@link #time} and {@link #offset}
* parameters to avoid pinning the whole context-local of the datetime-tz.
*/
public static class ContextIndependentDateTimeTz
extends ContextIndependentDateTime
{
/** Time zone offset in minutes from Coordinated Universal Time (UTC). */
int offset;
/**
* Basic c'tor.
*
* @param unknown
* {@code true} if the value is unknown
* @param julian
* The date for this instance in Progress 4GL format.
* @param time
* The milliseconds since midnight.
* @param offset
* Time zone offset in minutes from Coordinated Universal Time (UTC).
*/
public ContextIndependentDateTimeTz(boolean unknown, long julian, int time, int offset)
{
super(unknown, julian, time);
this.offset = offset;
}
/**
* Implementation of equals.
*/
@Override
public boolean equals(Object other)
{
if (this == other)
{
return true;
}
if (other instanceof ContextIndependentDateTimeTz)
{
ContextIndependentDateTimeTz that = (ContextIndependentDateTimeTz) other;
boolean u1 = this.isUnknown();
boolean u2 = that.isUnknown();
if (u1 && u2)
{
return true;
}
if (u1 || u2)
{
return false;
}
long thisAbsTime = this.julian * date.MILLIS_PER_DAY +
this.time - this.offset * 60 * 1000L;
long thatAbsTime = that.julian * date.MILLIS_PER_DAY +
that.time - that.offset * 60 * 1000L;
return thisAbsTime == thatAbsTime;
}
return super.equals(other);
}
/**
* Implementation of hash code.
*/
@Override
public int hashCode()
{
int result = 17;
if (isUnknown())
{
return result;
}
return (int) (37 * result + this.julian * date.MILLIS_PER_DAY +
this.time - this.offset * 60 * 1000);
}
}
/**
* Sample testing.
* <p>
* Displays the current date/time from various sources (date OS command, java
* {@link java.util.Date}, java {@link java.util.GregorianCalendar},
* P2J {@link com.goldencode.p2j.util.date#today()},
* P2J {@link com.goldencode.p2j.util.datetime#now()},
* P2J {@link com.goldencode.p2j.util.datetimetz#now()} that must be compared by hand.
* <p>
* Builds a lots of {@code datetime-tz} instances (built with {@link #fromLiteral(String)})
* from a small database of sensible data instances and compare the string representation of
* different outputs:
* <ol>
* <li>{@link #toStringExport()} - MESSAGE P4GL output
* <li>{@link #getIsoDate()} - output of ISO-DATE P4GL function
* <li>{@link #calendarValue()} - Calendar export of internal value of P2J datatype to Java
* </ol>
* The summary is displayed.
*
* @param args
* ignored.
*/
public static void main(String[] args)
{
date.setSessionTimeZone(+8 * 60);
date.setDateOrder("ymd"); // try to match the ISO pattern
Date java_date = new Date();
date today = date.today();
datetime dt_now = datetime.now();
datetimetz dtz_now = datetimetz.now();
System.out.print("OS date command: ");
try
{
Process proc = Runtime.getRuntime().exec("date -Ins"); // -Iseconds
while (true) {
int c = proc.getInputStream().read();
if (c == -1)
{
break;
}
System.out.write((char)c);
}
proc.waitFor();
}
catch (Exception e)
{
System.out.println("'date' OS command failed");
}
System.out.println("Java date: " +java_date);
System.out.println("Java date ISO: " +
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSZ").format(java_date));
System.out.println("GregorianCalendar is " + toString(new GregorianCalendar()));
System.out.println("TODAY is " + today.getIsoDate());
System.out.println("NOW (datetime) is " + dt_now.getIsoDate());
System.out.println("NOW (datetime-tz) is " + dtz_now.getIsoDate());
/*
// String strDtz = "2013-05-29T10:10:10.100+08:30";
String strDtz = "1582-10-05T00:00:00.000-05:00";
System.out.println(strDtz);
datetimetz dtz1 = datetimetz.fromLiteral(strDtz);
System.out.println(dtz1.toStringExport().replace('/', '-'));
System.out.println(dtz1.getIsoDate());
Calendar cal = dtz1.calendarValue();
System.out.println(datetimetz.toString(cal));
System.out.println();
strDtz = "1582-10-15T00:00:00.000-05:00";
System.out.println(strDtz);
dtz1 = datetimetz.fromLiteral(strDtz);
System.out.println(dtz1.toStringExport().replace('/', '-'));
System.out.println(dtz1.getIsoDate());
cal = dtz1.calendarValue();
System.out.println(datetimetz.toString(cal));
*/
final int[] years = { /*-4716, -100, -99, -1,*/ 1, 1582, /*1918,*/ 1949, 1969, 1970, 1999, 2000, /*2001,*/ 2015, 2049, 2050, 4716 };
final int[] months = { 1, 3, 4, 10, 12};
final int[] days = { 1, 5, /*13, 14,*/ 15, 30, 31, 26, 27, 28, 30, 31 };
final int[] hours = { 0, 1, 3, 4, 11, 12, 13, 22, 23 };
final int[] minutes = { 0, 1, 2, 30, 58, 59 };
final int[] seconds = { 0, 1, 2, 30, 58, 59 };
final int[] millis = { 0, 1, 500, 999 };
final int[] tzOff = { -14 * 60,
-5 * 60,
-3 * 60 - 30,
-1 * 60,
0,
1 * 60,
2 * 60,
5 * 60 + 45,
14 * 60};
long t0 = System.currentTimeMillis();
final int[] cnt = {0};
final int[] errs = {0};
final int[] threadCnt = {0};
final Object lock = new Object();
for (final int tz : tzOff) {
synchronized (lock) {
threadCnt[0]++;
}
new Thread(new Runnable()
{
@Override
public void run()
{
date.setDateOrder("ymd"); // try to match the ISO pattern
int lcnt = 0;
int lerrs = 0;
for (int hour : hours)
{
if (tz == tzOff[0]) {
System.out.print("*");
}
for (int milli : millis)
{
for (int sec : seconds)
{
for (int min : minutes)
{
for (int month : months)
{
for (int day : days)
{
if (day == 31 && month == 4)
{
// date does not exist
continue;
}
for (int year : years)
{
if (year == 1582 && month == 10 && (day > 4 || day < 15))
{
// date does not exist
continue;
}
lcnt++;
String strDtz = isoFormat(year, month, day,
hour, min, sec, milli,
tz);
// "2013-05-29T10:10:10.100+08:30";
datetimetz dtz1 = datetimetz.fromLiteral(strDtz);
if (dtz1 == null)
{
lerrs++;
System.out.println(lcnt + " 0 ");
continue;
}
String t1 = dtz1.toStringExport()
.replace('/', '-')
.replace(' ', 'T');
if (!t1.equals(strDtz))
{
if (!strDtz.equals("0" + t1))
{
// this is a workaround for three digits years (1-999)
lerrs++;
System.out.println(lcnt + " 1 " + t1 + " " + strDtz);
}
}
String t2 = dtz1.getIsoDate();
if (!t2.equals(strDtz))
{
lerrs++;
System.out.println(lcnt + " 2 " + t2 + " " + strDtz);
}
Calendar cal = dtz1.calendarValue();
String t3 = datetimetz.toString(cal);
if (!t3.equals(strDtz))
{
lerrs++;
System.out.println(lcnt + " 3 " + t3 + " " + strDtz);
}
}
}
}
}
}
}
}
if (tz == tzOff[0])
{
System.out.println("*");
}
synchronized (lock) {
threadCnt[0]--;
cnt[0] += lcnt;
errs[0] += lerrs;
lock.notifyAll();
}
}
}).start();
}
for (;;) {
synchronized (lock) {
if (threadCnt[0] == 0) {
break;
}
try
{
lock.wait();
}
catch (InterruptedException e)
{
LOG.severe("", e);
}
}
}
System.out.println(errs[0] + " errors of " + cnt[0] +
" in tests " + (System.currentTimeMillis() - t0) / 1000.0 + "secs.");
}
}