date.java
/*
** Module : date.java
** Abstract : Progress 4GL compatible date object
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 GES 20050413 @20788 Created initial version with full support for all Progress date
** processing including all functions and operators. Also includes
** string conversion (to/from) using Progress style format strings. Date
** processing has been made compatible including the implementation of
** controllable Y2K windowing (start year defaults to 1950 just like in
** Progress using the -yy startup parm or the session:year-offset
** attribute). At this time, the order of the date fields in a format
** string is hard coded to 'mdy' format, which is something that can be
** changed per-process in Progress via the -d startup option or the
** session:date-format attribute.
** 002 GES 20050421 @20814 Major updates to fix processing based on the default constructor and
** the constructor that accepts Date instances. In these cases the
** timezone processing was wrong (on input and when the resulting
** instance was used for accessing the day, month, weekday and year).
** Also added strict checks on (yy, mm, dd) and string constructors so
** that months and days are checked for validity.
** 003 GES 20050506 @21118 Implemented the comparable interface and also overrode the equals()
** method of Object to make this object easier to use.
** 004 GES 20050517 @21220 Rewrite the class to be mutable and to support an internal
** representation of the unknown value (as opposed to using null
** references). Awareness and integration with the Progress compatible
** primative wrappers was added. Comparison operations were
** refactored into a parent class.
** 005 GES 20050606 @21429 Added static versions of the getters to make conversion easier.
** 007 GES 20050607 @21439 Final versions of static functions and some cleanup.
** 008 GES 20050621 @21531 Added an assign() method to modify this instance's state based on
** the result of an expression of compatible type.
** 009 SVG 20050720 @21763 Cloning support for BaseDataType added.
** 010 ECF 20050729 @21969 Added neutralMillis convenience method.
** Delegates work to dateValue().
** 011 GES 20050815 @22119 Added Undoable interface support.
** 012 GES 20050829 @22285 Split assignment of configuration and state from normal assign().
** deepAssign() is now provided to set both configuration and state
** at the same time.
** 013 GES 20050829 @22365 Added processing to replace all '9' chars with '?' when there is any
** portion of the date which cannot be displayed using the
** given format.
** 014 GES 20050829 @22382 Added error condition processing.
** 015 ECF 20051004 @22967 Implement hashCode() method. Required to be consistent with
** BaseDataType's implementation of equals() method.
** 016 GES 20051024 @23123 Added toStringSQL() method.
** 017 GES 20060113 @23898 Added new constructor signatures.
** 018 GES 20060123 @24022 Move to directory for default windowingYear and context-local for
** storage of both windowingYear and baseMillis.
** 019 GES 20060206 @24298 Made compareTo() safe to process any object
** type (avoids ClassCastException).
** 020 GES 20060208 @24388 Added a default format string getter.
** 021 GES 20060405 @25371 Added increment/decrement helpers to simplify usage of dates
** as a loop control var.
** 022 GES 20060417 @25553 Added default value generation.
** 023 ECF 20060428 @25870 Fixed NPE in hashCode(). Timezone may be
** null and must be conditionally included in hashcode calculation.
** 024 GES 20060508 @26016 Minor documentation fix.
** 025 GES 20060531 @26760 Fix for plus and minus operators to properly honor unknown value
** (they now return unknown when any input is unknown).
** 026 GES 20060724 @28159 Added a forced assign() abstract method.
** 027 GES 20060730 @28226 Cache and reuse calendar instances since the cost of construction
** is high. This cache is timezone specific and kept in thread
** local vars.
** 028 GES 20060807 @28472 Moved to externalizable interface to optimize
** network performance.
** 029 ECF 20060908 @29385 Made constructor which accepts java.util.Date null-safe. A null date
** argument results in the instance being set to unknown value.
** 030 ECF 20060908 @29388 Fixed regression of default constructor.
** 031 GES 20070219 @32173 Added support for "999999" and "99999999" format strings
** (normally separators are required).
** 032 SVL 20080226 @37158 Additional constructors and yymmddWorker have been added to support
** years that have 2 or 1 digit(s).
** 033 GES 20080403 @37913 Added support for arbitrary ordering of the 3 date components,
** when reading from or writing to a string.
** 034 ECF 20090130 @41251 Minor performance improvement. Removed CachedCalendar and replaced it
** with ThreadLocal<GregorianCalendar>. Only set
** embedded calendar's time to current time when necessary.
** 035 ECF 20090212 @41297 Implemented a faster equals(). Handles most common cases without
** having to invoke the superclass' implementation.
** 036 SVL 20090213 @41271 Return "?" for the unknown value into toStringExport().
** 037 ECF 20090330 @41713 Fixed neutralMillisToCalendar(). In case of a non-null TimeZone,
** Daylight Savings offset must be queried after the Calendar has been
** updated with the base millisecond value. This allows the calendar
** to compute an appropriate DST offset for the target date. Removed
** unused method neutralMillis(). Reimplemented dateValue() to call
** dateValue(TimeZone) with internal zone value. Removed static modifier
** from WorkArea.dateFormat variable.
** 038 GES 20090422 @41907 Converted to standard string formatting.
** 039 GES 20090424 @41969 Import change.
** 040 GES 20090427 @41991 Rolled back the addition of some error message display code that
** was thought to be needed based on a TODO comment that was previously
** left behind. At a minimum, unknown value processing should not
** go through that path, so that addition was removed
** to eliminate regressions.
** 041 SIY 20090701 @43011 Added date components validation method.
** 042 GES 20090814 @43620 MONTH, DAY, YEAR and WEEKDAY must return unknown when the instance
** is unknown.
** 043 ECF 20090816 @43662 Removed final modifier from class.
** 044 SIY 20091105 @44317 Fixed handling of long (> 2 digits) years in parseWorker().
** 045 SIY 20091106 @44318 Return INVALID_DATE from parseWorker() if date
** can't be handled and silent mode is active.
** 046 GES 20100312 @44707 secondsSinceMidnight() had a flaw on the day of a DST change, it
** always was comparing the current millis (which was already DST aware)
** with the "neutral" number of millis at midnight. On that day (twice
** a year), the neutral value will always be either DST adjusted
** (in fall) or DST unadjusted (in spring). This means that after 2am
** on that day, the seconds since midnight will always be calculated
** wrong (1 hour later in fall and 1 hour earlier in spring). Changed
** the algorithm to directly use the fields of the Gregorian calendar
** and calculate the seconds off that. This has the advantage of being
** much simpler and is probably much faster too.
** 047 GES 20100617 @44909 Added a version of secondsSinceMidnight() which takes a calendar as
** a parameter. This allows the method to be used to calculate for any
** arbitrary time value instead of just for the current time
** since midnight.
** 048 GES 20110303 Moved maximum/minimum to varargs.
** 049 GES 20111013 Resolve deadlock that occurs on the client when context-local
** initialization calls to the server to read the directory. The
** solution is to separate the server communication from the
** context-local get() method. Once the get() is complete, it is
** safe to call the server.
** 050 CA 20130123 Added c'tor which builds a new instance from a BaseDataType value,
** if possible. This should be used only when constructing an instance
** using the value returned by a DYNAMIC-FUNCTION call; 4GL
** does some special casting in this case, which is not implemented yet.
** 051 GES 20130115 Added support for datetime(tz) data types.
** 052 GES 20130307 Added leading zero removal from date strings.
** 053 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 checked for validity.
** 054 OM 20130329 Added int64 support. Cosmetic changes.
** 055 OM 20130513 Added static function for parsing date literals.
** 056 OM 20130604 Massive update to support datetime/-tz types: added
** SESSION:DISPLAY-TIMEZONE attribute, static from/to SQL/literal
** conversion functions and static today function. Default c'tor now
** creates unknown values. Removed timezone private field (only used by
** TODAY function)
** 057 CA 20130822 set/getYearOffset and set/getDateOrder are made package private, so
** that the static proxy for the SESSION resource can be created (the
** names collide with their SessionUtils counterpart).
** 058 EVL 20131014 Change the methods access type to package private for the following
** methods: getSessionTimeZone(), getTimeSource(), getDisplayTimeZone()
** to avoid runtime ambiguous exception when creating proxy for session
** resource.
** 059 MAG 20140602 Fixed the BDT c'tor and modified the assign(BDT,boolean) on _POLY
** morphing.
** 060 VIG 20140914 Changed ErrorManager.displayError calls to equivalent method, that
** also adds error numbers to _MSG stack.
** 061 OM 20141016 Fixed equality of unknown date to typeless unknown literal.
** Made setDateOrder() public (accessible to TRPL).
** 062 OM 20141105 Rolled back setDateOrder() to package level because of proxy creation
** conflict. Functionality accessed indirectly via SessionUtils.
** 063 OM 20141111 Rolled back H062. Proxy conflict eliminated using refactoring the
** name of the method in the SessionUtils class.
** 064 GES 20150115 Refactored to create a separate class ParsedDateFormat, where shared
** format string parsing resides.
** 065 OM 20150129 Fixed getDefaultOffset() to add DST only when needed.
** Fixed millisToJulianDay to return correct day when the date is before
** java epoch.
** 066 OM 20150312 Fixed special conversions to/from empty string.
** 067 GES 20160617 Added unknown literal processing for the polymorphic assign case.
** 068 IAS 20160628 Set to default value of the SESSION:TIME-SOURCE attribute to
** 'LOCAL' (uppercase).
** 069 GES 20160707 Type conversion from a '?' string should result in unknown value.
** 070 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.
** 071 CA 20160812 getZoneFromOffset will cache TimeZone instances, keyed by offset.
** 072 OM 20160831 Optimized context-local accesses.
** 073 EVL 20161003 Adding more case to handle unknown values. 4GL developers can use
** "" empty string as another form of unknown value.
** 074 GES 20171206 Removed explicit bypass on pending error (new silent error mode).
** 075 CA 20180503 Renamed/moved some SESSION attribute getters/setters to fix issues
** with using the SESSION via a handle.
** 076 ECF 20180918 Performance improvement in fromLiteral.
** 077 CA 20181029 Fixed a bug in assign(BDT) where the unknown flag was not being
** cleared. Fixed export formatting in case where year is null.
** 078 OM 20190206 Added implementation of field size needed by record-length.
** HC 20190315 When converting dynamic queries the date format must be restored
** after the conversion. The conversion date format(p2j.cfg.xml) may
** differ from the runtime format (directory.xml).
** 079 CA 20190615 Added parseIsoDate() and fromIso() to parse a ISO-8601 date.
** 080 CA 20190628 Javadoc fix.
** 081 OM 20190626 Granted public access to getIsoDate().
** 082 EVL 20190729 Extended invalid format criteria to empty cleaned string.
** 083 CA 20190927 Added support for direct Java access from 4GL code.
** 084 CA 20191206 Fixed assign/deepAssign when the value is null or unknown.
** 085 CA 20200519 ISO 8601 format allows single-digit fields in 4GL.
** 086 OM 20200616 ParsedDateFormat API changes.
** 087 HC 20200806 Added OCX_EPOCH_OFFSET.
** 088 IAS 20201007 Added Type enum
** 089 ME 20210114 Throw invalid data type conversion when assigned longchar/clob/blob even if unknown.
** CA 20210304 Fixed date, datetime and datetimetz literal parsing.
** VVT 20210317 Updated for #4880, genBadCharInFormatError() removed.
** VVT 20210322 Fixed after the review. See #4880-107.
** CA 20211227 ETIME rerepsents the millis since the FWD session started, and not epoch.
** AL2 20220319 Added value proxy check in assign.
** CA 20220329 'fromIso' method was changed to public.
** OM 20220622 Improved date interval operations.
** CA 20230116 Added more POLY ctor variants.
** VVT 20230224 The public windowing(year) method added to use in customer projects. See #6953.
** 090 CA 20230215 'duplicate()' method returns the real type instead of BDT.
** 091 SBI 20230308 Chained decrement() and increment().
** 092 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 093 VVT 20230710 Wrong error fixed when parsing valid string into date. See 7407.
** Also minor non-functional fixes.
** 094 AB 20230906 Convert the variable to the required type in assign().
** RAA 20230906 Added default case to switch case in assign().
** AB 20230907 Modified access modifier for createDate from private to protected.
** Added getDefaultOffset(Date d).
** AB 20230907 Added getYearMonthDay() method.
** 095 VVT 20230918 Multiple non-functional minot refactorings/optimizations.
** 096 CA 20240214 Added 'date' constructor overload with POLY year argument.
** 097 AL2 20240222 Make new 'date' POLY constructor convert to the wider int64.
** 098 CA 20240320 Cache the NumberFormat instance.
** 099 CA 20240323 Removed the NumberFormat usage (and the cache) from 'getIsoDate', as this is
** expensive.
** 100 OM 20240508 Runtime parsing of date literal takes into account the current date-format.
** Avoid wrapping String literals, when possible.
** 20240510 Added auto detection for date format, depending on the context and a quick
** heuristic analysis of the text.
** 101 OM 20240524 Refined algorithm from H100. It also caused regressions.
** 102 VVT 20241202 Session startup parameters now also used when getting SESSION:DATA-FORMAT.
** See #9398.
** 103 CA 20250128 DATE(m, d, y) must not use the SESSION:YEAR-OFFSET or the '-yy' offset, the year
** is interpreted 'as is'.
** Fixed the standalone tests to have the same expected results as in OE - tests
** 70 and 71 fail at this time.
** 104 ICP 20250123 Used integer, int64 and character constants to leverage caches instances.
** 105 AI 20250220 Change param of withDynamicFormat into Supplier.
** 106 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.concurrent.*;
import java.util.function.*;
import java.util.regex.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.ErrorManager.ErrorEntry;
/**
* A class that represents a Progress 4GL compatible date 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>
* The Progress date is similar to the "Chronological Julian Day
* Number" (which has a base date of 01/01/4712 BC or 01/01/-4713 which
* is the 0 day) however it is 1 day off (its base date is 12/31/-4714 for
* day 0).
* <p>
* The following is the mapping of Progress language features to the
* corresponding feature in this class:
* <p>
* <pre>
* + operator (date + integer days) {@link #plusDays(date,long)}
* + operator (date + decimal days) {@link #plusDays(date,NumberType)}
* - operator (date - integer days) {@link #minusDays(date,long)}
* - operator (date - decimal days) {@link #minusDays(date,NumberType)}
* - operator (date - date) {@link #differenceNum}
* = or EQ operator {@link CompareOps#isEqual}
* <> or NE operator {@link CompareOps#isNotEqual}
* > or GT operator {@link CompareOps#isGreaterThan}
* >= or GE operator {@link CompareOps#isGreaterThanOrEqual}
* < or LT operator {@link CompareOps#isLessThan}
* <= or LE operator {@link CompareOps#isLessThanOrEqual}
* day function {@link #day}
* weekday function {@link #weekday}
* month function {@link #month}
* year function {@link #year}
* maximum function {@link CompareOps#maximum(BaseDataType[])}
* minimum function {@link CompareOps#minimum(BaseDataType[])}
* decimal function (to convert dates) {@link #decimalValue}
* integer function (to convert dates) {@link #integerValue}
* string function (default format) {@link #toString()}
* (user-defined format) {@link #toString(String)}
* (export format) {@link #toStringExport()}
* time function {@link #secondsSinceMidnight}
* timezone function (no parameter) {@link #getDefaultTimeZoneOffset}
* timezone function (character parameter) {@link #getOffsetForSpec}
* etime function {@link #elapsed}
* today function {@link #today()}
* date function (integer parameter) {@link #date(NumberType)}
* date function (integer m, d, y) {@link #date(NumberType,NumberType,NumberType)}
* date function (string parameter) {@link #date(String)}
* date function (date subclass parameter) {@link #date(date)}
* date-format SESSION attribute {@link #getDateOrder} or {@link #setDateOrder(character)}
* timezone SESSION attribute {@link #getTimeZone} or {@link #setTimeZone(NumberType)}
* time-source SESSION attribute {@link #getTimeSource} or {@link #setTimeSource(character)}
* year-offset SESSION attribute {@link #getYearOffset} or {@link #setYearOffset(NumberType)}
* [literal] {@link #fromLiteral(String)}
* </pre>
* <p>
* Date processing has been made compatible with Progress including the
* implementation of controllable Y2K windowing (the start year defaults to
* 1950). The replacement for using the -yy startup parameter or the
* session:year-offset attribute is to use the {@link #setWindowingYear}.
* 2-digit to 4-digit year conversion can be accessed manually via the
* {@link #windowingImpl}.
* <p>
* The order of the date fields when reading from a string or when writing to
* a string defaults to 'mdy' format, but this can be changed using a
* directory setting. That is equivalent to the -d startup option or the
* session:date-format attribute in Progress. To set this value, one must
* store the order of the 3 date sub-components as a 3 character string with
* one character each for "D" (day), "M" (month) and "Y" (year). This value
* is context-specific to the current user's session. The default value is
* "MDY".
* <p>
* The value returned may have been found via a search algorithm that
* is account (user or process) specific or group specific
* within the current server or a global default for all servers.
* <p>
* The implementation iteratively looks up the directory node under:
* /server/<serverID>/runtime/<account_or_group>/dateFormat
* <p>
* If no user/process or group nodes are present, then this is checked:
* /server/<serverID>/runtime/default/dateFormat
* <p>
* If no /server/<serverID>/runtime node exists, this is checked
* (it is the global default area for all servers):
* /server/default/runtime/<account_or_group>/dateFormat
* <p>
* Finally, if no user/process or group nodes are present in the global
* default area, then this is checked:
* /server/default/runtime/default/dateFormat
* <p>
* This class is very dependent upon the <code>GregorianCalendar</code> and
* Progress itself seems to have no concept of different types of calendars
* (e.g. lunar...) so this is OK. The two <code>GregorianCalendar</code>
* implementations (Progress and J2SE) appear to be quite compatible including
* how they handle the missing days from 10-05-1582 through 10-14-1582. Both
* implementations allow these dates to be instantiated and both generate
* a date that is equivalent to 10-15-1582 through 10-24-1582 respectively.
* This leads to apparent logical contradictions such as 10-14-1582 comparing
* as greater than 10-15-1582.
* <p>
* The Progress date has no notion of a time of day and the Java
* <code>java.util.Date</code> is inherently built with this notion (and
* the notion of a timezone). When being created from any J2SE generated
* date source (e.g. an instance of the Date class or the default constructor
* which accesses the current system time), this timezone specific data
* must be taken into account and neutered in the resulting stored date.
* Likewise, whenever using this data, no timezone is used unless one is
* converting this instance into an instance of <code>java.util.Date</code>,
* in which case the timezone can be overridden as in
* {@link #dateValue(TimeZone)} or in the {@link #date(Date,TimeZone)} constructor.
* <p>
* This class also features a built-in regression testing harness that is
* invoked via {@link #main}.
*
* @author GES
*/
public class date
extends BaseDataType
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(date.class);
/**
* The number of days between the Progress base date (12/31/-4714) and the
* Java epoch start (01/01/1970).
*/
public static final long JULIAN_EPOCH_OFFSET = 2440589L;
/**
* For unknown reasons, raw transfer uses a different epoch than the standard
* {@link #JULIAN_EPOCH_OFFSET} = 2440589 (01/01/1970).
* In reference manual it is called 'origin of the ABL DATE data type.' and experimentally was
* detected to have a value of a value of 2433405 which correspond to 05/02/1950. The size
* of a field holding this value (returned by this method) is hence 0.
* This value will not improve the bandwidth, instead it will make it worse.
*/
public static final long RAW_EPOCH = 2433405L;
/**
* The number of days between Progress base date and the OCX epoch start as used when interacting with OCX
* components (01/01/1990). Progress silently offsets date values passed to OCX methods by this number
* of days.
*/
public static final long OCX_EPOCH_OFFSET = 2415020L;
/** An invalid date (cannot exist in Progress 4GL). */
public static final long INVALID_DATE = -10246723L;
/** Represents the day component of the string formatted date. */
public static final byte DAY = 0;
/** Represents the month component of the string formatted date. */
public static final byte MONTH = 1;
/** Represents the year component of the string formatted date. */
public static final byte YEAR = 2;
/** MDY order of date components. */
public static final byte[] ORDER_MDY = new byte[] {MONTH, DAY, YEAR};
/** YMD order of date components. */
public static final byte[] ORDER_YMD = new byte[] {YEAR, MONTH, DAY};
/** The default data format, also used for parsing {@code date} literals in static context. */
public static final String DEFAULT_DATE_FORMAT = "MDY";
/** The default data format, also used for parsing {@code datetime} and {@code datetime-tz} literals. */
public static final String DEFAULT_DATETIME_FORMAT = "YMD";
/**
* The default windowing year, also used for parsing {@code date}, {@code datetime} and
* {@code datetime-tz} literals in static context.
*/
public static final int DEFAULT_DATE_WINDOWING_YEAR = 1950;
/** Number of milliseconds per day. */
public static final long MILLIS_PER_DAY = 1000 * 60 * 60 * 24;
/** The specific ISO-8601 date format used by 4GL. */
protected static final DateTimeFormatter ISO_LOCAL_DATE =
new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
.appendLiteral('-')
.appendValue(ChronoField.MONTH_OF_YEAR, 1, 2, SignStyle.NEVER)
.appendLiteral('-')
.appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NEVER)
.toFormatter()
.withResolverStyle(ResolverStyle.LENIENT)
.withChronology(IsoChronology.INSTANCE);
/** Minimum valid offset for SESSION:TIMEZONE. */
protected static final int MIN_VALID_TZ = -14 * 60;
/** Maximum valid offset for SESSION:TIMEZONE. */
protected static final int MAX_VALID_TZ = 14 * 60;
/** An invalid offset for SESSION:TIMEZONE. */
protected static final int INVALID_TZ = MAX_VALID_TZ + 1;
/** Stores context-local state variables. */
private static final ContextContainer work = new ContextContainer();
/** The default format string. */
private static final String DEFAULT_FORMAT = "99/99/99";
/** Format specification for first TIMEZONE attribute error. */
private static final String TZ_ATTR_ERR1 =
"Invalid value %d for SESSION:TIMEZONE attribute";
/** Format specification for second TIMEZONE attribute error. */
private static final String TZ_ATTR_ERR2 =
"Attribute TIMEZONE for the PSEUDO-WIDGET has an invalid value of %d";
/**
* The default calendar cache (thread local) used for validation and instantiation of {@code date} from
* day/month/year triplets.
*/
private static final NullCalendar nullGC = new NullCalendar();
/** The GMT calendar cache (thread local). */
private static GMTCalendar gmtGC = new GMTCalendar();
/** Pattern used for literal matches */
private static Pattern literalPattern = Pattern.compile("^(\\d+)[/|\\.|-](\\d+)[/|\\\\.|-]((\\-)?\\d+)$");
/** Cache of other timezone-specific calendar thread local caches. */
private static Map<TimeZone, ZoneCalendar> tzcals = null;
/** Cached mappings of tz offsets to their {@link TimeZone} instance. */
private static Map<Integer, TimeZone> offset2tz = new ConcurrentHashMap<>();
/** Pattern matching some date unknown values: string consisting of any (including zero)
* number of spaces, dots and slashes */
private final static Pattern unknownDateSpecRegexp = Pattern.compile("[?]|[- /.]*");
/** Stores the date for this instance in Progress 4GL format. */
private long julian = INVALID_DATE;
/** Indicates if this instance is set to the unknown value. */
private boolean unknown = false;
/** The working area where this instance was created. Caches costly access to context local. */
private WorkArea workArea = null;
/**
* Creates an instance that represents an unknown date.
*/
public date()
{
unknown = true;
}
/**
* 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 date(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
{
assign(value);
}
}
/**
* Creates an instance from a <code>java.util.Date</code> instance.
* <p>
* <b>WARNING: this will only work if the calendar in which this date
* 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 #date(Date,TimeZone)} version.
*
* @param d
* The <code>Date</code> instance from which to pattern the instance.
*/
public date(Date d)
{
this(d, TimeZone.getDefault());
}
/**
* Creates an instance from a <code>java.util.Date</code> instance.
* <p>
* If a timezone is specified, it is stored to be used when instantiating
* <code>java.util.Date</code> instances using this instance's data.
*
* @param d
* The <code>Date</code>instance from which to pattern the
* instance or <code>null</code> to obtain the current time.
* @param zone
* The timezone whose offset is to be used to calculate the
* date or <code>null</code> if the default JVM timezone is to be used.
*/
public date(Date d, TimeZone zone)
{
if (d == null)
{
setUnknown();
return;
}
julian = millisToJulianDay(dateToLocalMillis(d, zone));
}
/**
* Parses a string with a format based on the Progress 4GL rules for converting a string to a date.
* <p>
* The special string "TODAY" (case-insensitive) is treated as the same result as the built-in function
* {@code TODAY}. This means it generates a date that is equal to the current date based on the local
* date and time of day.
* <p>
* Date literals are generally matched in mm/dd/yyyy or mm-dd-yyyy or mm.dd.yyyy formats, although the 3
* types of separators ('-', '/' and '.') can even be mixed in the same string (e.g. (mm-dd/yyyy or
* mm.dd-yyyy). Both months and days can be specified in any number of digits (there must be at least 1
* digit, but there can 5 digits as long as the resulting month or day is valid). The month must be an
* integer between 1 and 12 and the day must be between 1 and the number of days in the given month. All
* separators and the year digits are optional. If separators are missing, the first 2 digits are treated
* as the month, the next 2 digits as the day and the remaining digits as the year. If the first separator
* is used and there is a year, then the second separator must also be used. Likewise, there cannot be a
* day/year separator without first having a month/day separator.
* <p>
* If the year digits are missing, the current year will be used. There can be an optional negative sign
* preceding any year digits and one can specify a dd and yyyy separator without specifying any year
* digits at all. Any number of year digits are valid, though in Progress the year 0 is treated
* specially, as follows:
* <ul>
* <li> Valid (all resolve to the year 2000)
* <ul>
* <li> 0
* <li> -0
* <li> 00
* </ul>
* <li> Invalid (all complain about specifying the year 0)
* <ul>
* <li> 000
* <li> 0000
* <li> 00000 (and so on...)
* <li> -00
* <li> -000 (and so on...)
* </ul>
* </ul>
* <p>
* So y, yy, yyy, yyyy, yyyyy... and -y, -yy, -yyy, -yyyy... all are valid year specifications, except for
* the specific year zero issue noted above. The largest positive year value is 32767 and the largest
* negative year value is -32768. Note that leading zeros are ignored. Negative years can only be
* specified in a string that has separators.
* <p>
* Any positive year of 1 or 2 digits is interpreted based on the current Y2K window which can be obtained
* using {@link #getWindowingYear}.
* <p>
* Leading and trailing spaces are allowed around the mm/dd/yyyy data but no spaces may be inserted inside
* the date value. Any additional non-whitespace included in the string after the date AND after
* intervening whitespace, is ignored.
*
* @param d
* The formatted string date to convert.
*
* @throws ErrorConditionException
*/
public date(String d)
throws ErrorConditionException
{
instantiateFromStringWorker(d, null/*auto*/, -1);
}
/**
* Converts a string to a date using a specified format and an optional windowing year.
* 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 date#date(String)
*
* @param d
* The formatted string date to convert.
* @param format
* The format string used for date conversion. May be {@code null}. In this case, the dynamic
* format is used if it was activated or {@code DEFAULT_DATE_FORMAT} otherwise.
* @param windowingYear
* The start of the 100 year window used for conversion when year is in 2-digits.
*
* @throws ErrorConditionException
*/
public date(String d, String format, int windowingYear)
throws ErrorConditionException
{
if (format == null)
{
format = getDynamicFormat();
if (format == null)
{
format = DEFAULT_DATE_FORMAT;
}
}
instantiateFromStringWorker(d, format, windowingYear);
}
/**
* Get the dynamic format.
*
* @return the dynamic format, if one was configured. Otherwise, return {@code null}.
*/
public static String getDynamicFormat()
{
return work.get().dynamicFormat;
}
/**
* 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 DATE() built-in function when called with a
* <code>date</code> subclass.
*
* @param d
* The instance from which to copy state.
*/
public date(date d)
{
deepAssign(d);
}
/**
* Parses a string with a format based on the Progress 4GL rules for
* converting a string to a date.
* <p>
* Date literals are generally matched in mm/dd/yyyy or mm-dd-yyyy or
* mm.dd.yyyy formats, although the 3 types of separators ('-', '/' and
* '.') can even be mixed in the same string (e.g. (mm-dd/yyyy or
* mm.dd-yyyy). Both months and days can be specified in any number of
* digits (there must be at least 1 digit, but there can 5 digits as long
* as the resulting month or day is valid). The month must be an integer
* between 1 and 12 and the day must be between 1 and the number of days
* in the given month. All separators and the year digits are optional.
* If separators are missing, the first 2 digits are treated as the month,
* the next 2 digits as the day and the remaining digits as the year. If
* the first separator is used and there is a year, then the second
* separator must also be used. Likewise, there cannot be a day/year
* separator without first having a month/day separator.
* <p>
* If the year digits are missing, the current year will be used. There
* can be an optional negative sign preceding any year digits and one can
* specify a dd and yyyy separator without specifying any year digits at
* all. Any number of year digits are valid, though in Progress the year
* 0 is treated specially, as follows:
* <ul>
* <li> Valid (all resolve to the year 2000)
* <ul>
* <li> 0
* <li> -0
* <li> 00
* </ul>
* <li> Invalid (all complain about specifying the year 0)
* <ul>
* <li> 000
* <li> 0000
* <li> 00000 (and so on...)
* <li> -00
* <li> -000 (and so on...)
* </ul>
* </ul>
* <p>
* So y, yy, yyy, yyyy, yyyyy... and -y, -yy, -yyy, -yyyy... all are valid
* year specifications, except for the specific year zero issue noted
* above. The largest positive year value is 32767 and the largest
* negative year value is -32768. Note that leading zeros are ignored.
* Negative years can only be specified in a string that has separators.
* <p>
* Any positive year of 1 or 2 digits is interpreted based on the current
* Y2K window which can be obtained using {@link #getWindowingYear}.
* <p>
* Leading and trailing spaces are allowed around the mm/dd/yyyy data but
* no spaces may be inserted inside the date value. Any additional
* non-whitespace included in the string after the date AND after
* intervening whitespace, is ignored.
* <p>
* If the parameter is <code>unknown value</code> the resulting date is
* the <code>unknown value</code>.
*
* @param d
* The formatted string date to convert.
*
* @throws ErrorConditionException
*/
public date(character d)
throws ErrorConditionException
{
if (BaseDataType.isProxy(d))
{
if (d.val() instanceof character)
{
d = (character) d.val();
}
else
{
initialize(d.val());
return;
}
}
if (d.isUnknown())
{
unknown = true;
}
else
{
instantiateFromStringWorker(d.toStringMessage(), null/*auto*/, -1);
}
}
/**
* Creates an instance based on specific numeric input.
*
* @param month
* The month from 1 to 12.
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less).
* @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 used unchanged, and will not be windowed.
*
* @throws ErrorConditionException
*/
public date(double month, double day, double year)
throws ErrorConditionException
{
yymmddWorker((int) year, (int) month, (int) day);
}
/**
* Creates an instance based on specific numeric input.
*
* @param month
* The month from 1 to 12.
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less).
* @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 used unchanged, and will not be windowed.
* @param forceFourDigits
* If true then 2-digit years will be windowed into the right
* century.
*
* @throws ErrorConditionException
*/
public date(double month, double day, double year, boolean forceFourDigits)
throws ErrorConditionException
{
yymmddWorker((int) year, (int) month, (int) day, forceFourDigits);
}
/**
* Creates an instance based on specific numeric input.
* <p>
* If the parameters are <code>unknown value</code> the resulting date is
* the <code>unknown value</code>.
*
* @param month
* The month from 1 to 12.
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less).
* @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 used unchanged, and will not be windowed.
*
* @throws ErrorConditionException
*/
public date(NumberType month, NumberType day, double year)
throws ErrorConditionException
{
if (month.isUnknown() || day.isUnknown())
{
unknown = true;
}
else
{
yymmddWorker((int) year, month.intValue(), day.intValue());
}
}
/**
* Creates an instance based on specific numeric input.
* <p>
* If the parameters are <code>unknown value</code> the resulting date is
* the <code>unknown value</code>.
*
* @param month
* The month from 1 to 12.
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less).
* @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 used unchanged, and will not be windowed.
*
* @throws ErrorConditionException
*/
public date(BaseDataType month, double day, BaseDataType year)
throws ErrorConditionException
{
if (month.isUnknown() || year.isUnknown())
{
unknown = true;
}
else
{
yymmddWorker(new integer(month).intValue(), (int) day, new integer(year).intValue());
}
}
/**
* Creates an instance based on specific numeric input.
* <p>
* If the parameters are <code>unknown value</code> the resulting date is
* the <code>unknown value</code>.
*
* @param month
* The month from 1 to 12.
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less).
* @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 used unchanged, and will not be windowed.
*
* @throws ErrorConditionException
*/
public date(double month, double day, BaseDataType year)
throws ErrorConditionException
{
if (year.isUnknown())
{
unknown = true;
}
else
{
yymmddWorker((int) month, (int) day, new integer(year).intValue());
}
}
/**
* Creates an instance based on specific numeric input.
* <p>
* If the parameters are <code>unknown value</code> the resulting date is
* the <code>unknown value</code>.
*
* @param month
* The month from 1 to 12.
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less).
* @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 used unchanged, and will not be windowed.
*
* @throws ErrorConditionException
*/
public date(double month, NumberType day, NumberType year)
throws ErrorConditionException
{
if (year.isUnknown()|| day.isUnknown())
{
unknown = true;
}
else
{
yymmddWorker(year.intValue(), (int) month, day.intValue());
}
}
/**
* Creates an instance based on specific numeric input.
* <p>
* If the parameters are <code>unknown value</code> the resulting date is
* the <code>unknown value</code>.
*
* @param month
* The month from 1 to 12.
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less).
* @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 used unchanged, and will not be windowed.
*
* @throws ErrorConditionException
*/
public date(NumberType month, double day, NumberType year)
throws ErrorConditionException
{
if (year.isUnknown() || month.isUnknown())
{
unknown = true;
}
else
{
yymmddWorker(year.intValue(), month.intValue(), (int) day);
}
}
/**
* Creates an instance based on specific numeric input.
* <p>
* If the parameters are <code>unknown value</code> the resulting date is
* the <code>unknown value</code>.
*
* @param month
* The month from 1 to 12.
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less).
* @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 used unchanged, and will not be windowed.
*
* @throws ErrorConditionException
*/
public date(double month, NumberType day, double year)
throws ErrorConditionException
{
if (day.isUnknown())
{
unknown = true;
}
else
{
yymmddWorker((int) year, (int) month, day.intValue());
}
}
/**
* Creates an instance based on specific numeric input.
* <p>
* If the parameters are <code>unknown value</code> the resulting date is
* the <code>unknown value</code>.
*
* @param month
* The month from 1 to 12.
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less).
* @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 used unchanged, and will not be windowed.
*
* @throws ErrorConditionException
*/
public date(NumberType month, double day, double year)
throws ErrorConditionException
{
if (month.isUnknown())
{
unknown = true;
}
else
{
yymmddWorker((int) year, month.intValue(), (int) day);
}
}
/**
* Creates an instance based on specific numeric input.
* <p>
* If the parameters are <code>unknown value</code> the resulting date is
* the <code>unknown value</code>.
*
* @param month
* The month from 1 to 12.
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less).
* @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 used unchanged, and will not be windowed.
*
* @throws ErrorConditionException
*/
public date(double month, double day, NumberType year)
throws ErrorConditionException
{
if (year.isUnknown())
{
unknown = true;
}
else
{
yymmddWorker(year.intValue(), (int) month, (int) day);
}
}
/**
* Creates an instance based on specific numeric input.
* <p>
* If the parameters are <code>unknown value</code> the resulting date is
* the <code>unknown value</code>.
*
* @param month
* The month from 1 to 12.
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less).
* @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 used unchanged, and will not be windowed.
*
* @throws ErrorConditionException
*/
public date(NumberType month, NumberType day, BaseDataType year)
throws ErrorConditionException
{
this(month, day, new int64(year));
}
/**
* Creates an instance based on specific numeric input.
* <p>
* If the parameters are <code>unknown value</code> the resulting date is
* the <code>unknown value</code>.
*
* @param month
* The month from 1 to 12.
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less).
* @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 used unchanged, and will not be windowed.
*
* @throws ErrorConditionException
*/
public date(NumberType month, NumberType day, NumberType year)
throws ErrorConditionException
{
if (year.isUnknown() || month.isUnknown() || day.isUnknown())
{
unknown = true;
}
else
{
yymmddWorker(year.intValue(), month.intValue(), day.intValue());
}
}
/**
* Creates an instance that represents the number of days (positive or
* negative) from the Progress 4GL base date of 12/31/-4714. Note the
* following examples (formatted in 'export' format):
* <p>
* <pre>
* date -1000 is 04/05/-4716
* date -1 is 12/30/-4714
* date 0 is 12/31/-4714
* date 1 is 01/01/-4713
* date 1685264 is 12/31/-100
* date 1685265 is 01/01/-099
* date 1721424 is 12/31/-001
* date 1721425 is 01/01/001
* date 2299161 is 10/04/1582
* date 2299162 is 10/15/1582 (note the gap in the Gregorian Calendar)
* date 2421684 is 03/30/1918
* date 2421685 is 03/31/1918 (first DST in U.S.A.)
* date 2421686 is 04/01/1918
* date 2433283 is 12/31/1949
* date 2433284 is 01/01/50 (first date in default Y2K window - 1950)
* date 2440589 is 01/01/70 (beginning of Java epoch)
* date 2451545 is 12/31/99
* date 2451546 is 01/01/00
* date 2453475 is 04/13/05
* date 2469808 is 12/31/49 (last date in default Y2K window - 2049)
* date 2469809 is 01/01/2050
* </pre>
*
* @param dayNumber
* The number of days since 12/31/-4714.
*/
public date(long dayNumber)
{
julian = dayNumber;
}
/**
* Creates an instance that represents the number of days (positive or
* negative) from the Progress 4GL base date of 12/31/-4714. See
* {@link #date(long)}.
*
* @param dayNumber
* The number of days since 12/31/-4714.
*/
public date(int dayNumber)
{
julian = dayNumber;
}
/**
* Creates an instance that represents the number of days (positive or
* negative) from the Progress 4GL base date of 12/31/-4714. See
* {@link #date(long)}.
* <p>
* If the parameter is <code>unknown value</code> the resulting date is
* the <code>unknown value</code>.
*
* @param dayNumber
* The number of days since 12/31/-4714.
*/
public date(NumberType dayNumber)
{
if (dayNumber.isUnknown())
{
unknown = true;
}
else
{
julian = dayNumber.intValue();
}
}
/**
* Creates an instance that represents today's date using the local system clock time.
* @return today's date
*/
public static date today()
{
date ret = new date();
ret.setDayNumber(_today());
return ret;
}
/**
* Parse a Progress date literal (format mm/dd/yyyy). The year may be in 2 or 4 digits format.
* If in 2 digits the current year windowing is used.
*
* @param str
* The string representation of the date literal.
*
* @return The date represented by the string str.
*/
public static date fromLiteral(String str)
{
Matcher matcher = literalPattern.matcher(str);
if (!matcher.matches())
{
return new date(); // format not recognized
}
try
{
int month = Integer.parseInt(matcher.group(1));
int day = Integer.parseInt(matcher.group(2));
String strYear = matcher.group(3);
int year = Integer.parseInt(strYear);
return new date(month, day, year, strYear.length() < 3);
}
catch (NumberFormatException e)
{
return new date(); // format not recognized
}
}
/**
* Get the type
* @return type
*/
@Override
public Type getType()
{
return Type.DATE;
}
/**
* Convert a UTC time into a timezone specific number of milliseconds
* since the epoch (January 1, 1970). If no timezone is passed in,
* the default JVM timezone will be used. If no date is passed in, the
* current time will be used.
*
* @param d
* The date object representing the UTC value to convert or
* <code>null</code> if the current UTC time should be used.
* @param zone
* The timezone object whose specific attributes should be used
* in the conversion of the UTC value or <code>null</code> if
* JVM default timezone should be used.
*
* @return The localized number of milliseconds from the Java epoch.
*/
public static long dateToLocalMillis(Date d, TimeZone zone)
{
if (zone == null)
{
zone = TimeZone.getDefault();
}
// use the passed/session/local timezone to create the calendar
GregorianCalendar gc = getZoneCalendar(zone);
if (d != null)
{
// take the current time
gc.setTime(d);
}
else
{
// reset the state to the current time
gc.setTimeInMillis(System.currentTimeMillis());
}
return calendarToLocalMillis(gc);
}
/**
* Convert a UTC time into a timezone specific number of milliseconds
* since the epoch (January 1, 1970).
*
* @param gc
* The <code>GregorianCalendar</code> representing the UTC value to convert.
*
* @return The localized number of milliseconds from Java epoch.
*/
public static long calendarToLocalMillis(GregorianCalendar gc)
{
// determine the timezone-specific offset
long offset = gc.get(Calendar.ZONE_OFFSET) + gc.get(Calendar.DST_OFFSET);
// convert the UTC to a local time and return it
return gc.getTimeInMillis() + offset;
}
/**
* Convert a time-neutral number of milliseconds since the epoch into a
* UTC time as represented by the <code>Date</code> class. If no timezone
* is passed in, the default JVM timezone will be used.
*
* @param millis
* The time-neutral number of milliseconds since the epoch.
* @param zone
* The timezone object whose specific attributes should be used
* in the conversion to the UTC value or <code>null</code> if
* JVM default timezone should be used.
*
* @return A date object based on the UTC value such that the same
* time-neutral result will be obtained when using the timezone
* aware resulting class.
*/
public static Date neutralMillisToDate(long millis, TimeZone zone)
{
return neutralMillisToCalendar(millis, zone).getTime();
}
/**
* Convert a time-neutral number of milliseconds since the epoch into a
* UTC time as represented by the <code>GregorianCalendar</code> class.
* If no timezone is passed in, the GMT timezone will be used which means
* that all timezone processing is effectively disabled.
*
* @param millis
* The time-neutral number of milliseconds since the epoch.
* @param zone
* The timezone object whose specific attributes should be used
* in the conversion to the UTC value or <code>null</code> if
* timezone processing should be disabled.
*
* @return A calendar object based on the UTC value such that the same
* time-neutral result will be obtained when using the timezone
* aware resulting class.
*/
public static GregorianCalendar neutralMillisToCalendar(long millis, TimeZone zone)
{
GregorianCalendar gc = null;
if (zone == null)
{
// no timezone correction is needed
gc = gmtGC.get();
// set the time into the calendar
gc.setTimeInMillis(millis);
}
else
{
// use the passed timezone
gc = getZoneCalendar(zone);
// determine the timezone-specific offset (raw offset from GMT in milliseconds)
int tzOffset = gc.get(Calendar.ZONE_OFFSET);
// convert the neutral time to a UTC
millis -= tzOffset;
// set the time into the calendar
gc.setTimeInMillis(millis);
// now that the date is set, the calendar can accurately compute an
// offset for Daylight Savings; if positive, adjust for it
int dstOffset = gc.get(Calendar.DST_OFFSET);
if (dstOffset > 0L)
{
gc.add(Calendar.MILLISECOND, -dstOffset);
}
}
// return
return gc;
}
/**
* Converts a Progress 4GL style Julian day into the number of
* milliseconds since the Java epoch. Note that the resulting milliseconds
* value is to be considered as specific to whatever timezone is in
* use as the time index itself is invalid and only the date is valid.
*
* @param jd
* The Julian day (days since 12/31/-4714).
*
* @return The number of milliseconds since the Java epoch.
*/
public static long julianDayToMillis(long jd)
{
return (jd - JULIAN_EPOCH_OFFSET) * MILLIS_PER_DAY;
}
/**
* Converts a number of milliseconds from the Java epoch into a Progress 4GL style Julian day.
* The milliseconds value is used 'raw' so any local timezone offset processing that is needed
* must have already been applied.
*
* @param millis
* The number of milliseconds from the Java epoch which specifies a specific point
* in time.
*
* @return The Julian day (days since 12/31/-4714). If <code>millis</code> does not
* represent a full day, the result is adjusted to correct result using Euclidian
* remainder arithmetic.
*/
public static long millisToJulianDay(long millis)
{
long julianDay = (millis / MILLIS_PER_DAY) + JULIAN_EPOCH_OFFSET;
if (millis % MILLIS_PER_DAY < 0)
{
// count full days, otherwise we will get negative time
// in the case millis < 0 (ie. before java epoch):
// - if the remainder is 0 then a full day is counted
// - otherwise the negative remainder represents the time from previous day, not yet
// counted, so we decrement the result
-- julianDay;
}
return julianDay;
}
/**
* Returns the instance from the list which has the latest date. This
* list may not include <code>null</code> values.
*
* @param list
* The list of dates to check.
*
* @return The latest date or <code>unknown value</code> if any
* entries in the list are unknown or <code>null</code> if
* an empty list is passed.
*/
public static date maximum(date ... list)
{
return (date) CompareOps.maximum(list);
}
/**
* Returns the instance from the list which has the earliest date. This
* list may not include <code>null</code> values.
*
* @param list
* The list of dates to check.
*
* @return The earliest date or <code>unknown value</code> if any
* entries in the list are unknown or <code>null</code> if
* an empty list is passed.
*/
public static date minimum(date ... list)
{
return (date) CompareOps.minimum(list);
}
/**
* Executes a piece of code, making sure the value of {@code dynamicFormat} reflects
* the current dynamic date-format. When the method ends, the flag is restored.
*
* @param r
* The code to be executed.
*/
public static <T> T withDynamicFormat(Supplier<T> r)
{
WorkArea wa = work.get();
if (wa.dynamicFormat != null)
{
// Already in a bracket.
// This can happen when the before table is constructed during the bracket of its after table.
return r.get();
}
try
{
wa.dynamicFormat = date.getDateOrderNative();
return r.get();
}
finally
{
wa.dynamicFormat = null;
}
}
/**
* Accesses the beginning year of the 100 year period where 2 digit dates
* implicitly correspond to 4 year dates. This value is context-specific
* to the current user's session.
* <p>
* The value returned may have been found via a search algorithm that
* is account (user or process) specific or group specific
* within the current server or a global default for all servers.
* <p>
* The implementation iteratively looks up the directory node under:
* /server/<serverID>/runtime/<account_or_group>/windowingYear
* <p>
* If no user/process or group nodes are present, then this is checked:
* /server/<serverID>/runtime/default/windowingYear
* <p>
* If no /server/<serverID>/runtime node exists, this is checked
* (it is the global default area for all servers):
* /server/default/runtime/<account_or_group>/windowingYear
* <p>
* Finally, if no user/process or group nodes are present in the global
* default area, then this is checked:
* /server/default/runtime/default/windowingYear
* <p>
* If no value is found via this lookup, then the Progress 4GL default of
* 1950 will be returned.
*
* @return The first year of the 2-digit to 4-digit mapping window for
* Y2K purposes.
*/
public static int getWindowingYear()
{
return work.obtain().windowingYear;
}
/**
* Sets the beginning year of the 100 year period where 2 digit dates
* implicitly correspond to 4 year dates. The Progress 4GL default is 1950.
* <p>
* The value set is specific to the current user's context.
*
* @param year
* The first year of the 2-digit to 4-digit mapping window for Y2K purposes.
*/
public static void setWindowingYear(long year)
{
// P4GL silently truncates any large value to fit the 32 bit representation
work.obtain().windowingYear = (int)year;
}
/**
* Implementation of SESSION:YEAR-OFFSET attribute setter. Sets the
* configured start year for the two-digit Y2K windowing range. This is
* the equivalent of the Progress -yy command line parameter. The
* default value is 1950 unless otherwise specified in the directory (see
* {@link #getWindowingYear}).
*
* @param year
* The new start year for the two-digit windowing range.
*/
static void setYearOffset(NumberType year)
{
if (year.isUnknown())
{
// P4GL prints no error, silently ignoring this.
return;
}
date.setWindowingYear(year.longValue());
}
/**
* Implementation of SESSION:YEAR-OFFSET attribute getter. Returns the
* currently configured start year for the two-digit Y2K windowing range.
* This is the equivalent to the Progress -yy command line parameter. The
* default value is 1950 unless otherwise specified in the directory (see
* {@link #getWindowingYear}).
*
* @return The start year for the two-digit windowing range.
*/
static integer getYearOffset()
{
return new integer(date.getWindowingYear());
}
/**
* Gets the order of date components for conversion to/from strings.
* <p>
* The order of the 3 date sub-components as a 3 byte array using the
* constants <code>DAY</code>, <code>MONTH</code> and <code>YEAR</code>
* once each. The byte order from index 0 to index 2 represents the
* left to right ordering of the components. So the leftmost date component
* will be defined by the byte at index position 0.
* <p>
* The value accessed is specific to the current user's context.
*
* @return Defines the order of date components for conversion to/from
* strings.
*/
public static byte[] getDateComponentOrder()
{
return work.obtain().dateFormat;
}
/**
* Sets the order of date components for conversion to/from strings.
* <p>
* The order of the 3 date sub-components as a 3 byte array using the
* constants <code>DAY</code>, <code>MONTH</code> and <code>YEAR</code>
* once each. The byte order from index 0 to index 2 represents the
* left to right ordering of the components. So the leftmost date component
* will be defined by the byte at index position 0.
* <p>
* The value set is specific to the current user's context.
*
* @param dateFormat
* Defines the order of date components for conversion to/from
* strings.
*/
public static void setDateComponentOrder(byte[] dateFormat)
{
work.obtain().dateFormat = dateFormat;
}
/**
* Gets the order of date components for conversion to/from strings.
* <p>
* The order of the 3 date sub-components is a 3 character string using
* "M", "D" and "Y" once each. The order from index 0 to index 2
* represents the left to right ordering of the components. So the leftmost
* date component will be defined by the character at index position 0.
* <p>
* The value accessed is specific to the current user's context.
*
* @return Defines the order of date components for conversion to/from strings.
*/
public static String getDateOrderNative()
{
WorkArea wa = work.obtain();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 3; i++)
{
if (wa.dateFormat[i] == DAY)
{
sb.append('d');
}
else if (wa.dateFormat[i] == MONTH)
{
sb.append('m');
}
else
{
sb.append('y');
}
}
return sb.toString();
}
/**
* Gets the order of date components for conversion to/from strings.
* <p>
* The order of the 3 date sub-components is a 3 character string using "M", "D" and "Y" once each. The
* order from index 0 to index 2 represents the left to right ordering of the components. So the leftmost
* date component will be defined by the character at index position 0.
* <p>
* The value accessed is specific to the current user's context.
*
* @return Defines the order of date components for conversion to/from strings.
*/
public static character getDateOrder()
{
return character.of(getDateOrderNative());
}
/**
* Sets the order of date components for conversion to/from strings.
* <p>
* The order of the 3 date sub-components as a 3 character string using
* "M", "D" and "Y" once each. The order from index 0 to index 2
* represents the left to right ordering of the components. So the leftmost
* date component will be defined by the character at index position 0.
* <p>
* The value set is specific to the current user's context.
*
* @param dateFormat
* Defines the order of date components for conversion to/from strings.
*/
public static void setDateOrder(String dateFormat)
{
setDateComponentOrder(initFormat(dateFormat));
}
/**
* Sets the order of date components for conversion to/from strings.
* <p>
* The order of the 3 date sub-components as a 3 character string using
* "M", "D" and "Y" once each. The order from index 0 to index 2
* represents the left to right ordering of the components. So the leftmost
* date component will be defined by the character at index position 0.
* <p>
* The value set is specific to the current user's context.
*
* @param dateFormat
* Defines the order of date components for conversion to/from strings.
*/
public static void setDateOrder(character dateFormat)
{
if (dateFormat != null && dateFormat.isUnknown())
{
genInvalidDateOrder();
return;
}
setDateOrder(dateFormat == null ? null : dateFormat.toStringMessage());
}
/**
* Sets the SESSION:TIMEZONE used as the default offset in minutes from
* UTC for this user's context.
*
* @param offset
* The new timezone offset which must be between -840 and 840
* inclusive. The value may also be <code>unknown</code> which
* is the equivalent of resetting this attribute to the default.
*/
public static void setTimeZone(NumberType offset)
{
final WorkArea wa = work.obtain();
if (offset.isUnknown())
{
// handle particular case first
wa.timezoneOffset = new integer();
return;
}
long tz = offset.longValue();
if (tz < MIN_VALID_TZ || tz > MAX_VALID_TZ)
{
// DO NOT RAISE AN ERROR CONDITION! Simply display these on the
// terminal's message line. Yes, the cast to a short is deliberate
// since the first error message is limited to that size number.
// The result is that the over/underflow will be displayed if the
// number can't fit. Progress is insane that way. The 2nd message
// doesn't act the same way. Go figure.
String err1 = String.format(TZ_ATTR_ERR1, (short) tz);
String err2 = String.format(TZ_ATTR_ERR2, tz);
ErrorManager.displayError(14800, err1, false);
ErrorManager.displayError(4057, err2, true);
// no change will be made
return;
}
wa.timezoneOffset = new integer(offset);
}
/**
* Sets the SESSION:TIMEZONE used as the default offset in minutes from
* UTC for this user's context.
*
* @param offset
* The new timezone offset which must be between -840 and 840 inclusive.
*/
public static void setSessionTimeZone(long offset)
{
setTimeZone(new int64(offset));
}
/**
* Sets the <code>TIME-SOURCE</code> attribute for the user's context.
* This attribute is can be read and written but it is not honored for
* date/time processing in P2J. The intention of the attribute is to define
* the system from which time values will be obtained. In P2J, time values
* are always obtained from the application server.
*
* @param timeSource
* The new value to set. If <code>null</code>, then the empty
* string will be used, which in the 4GL has the same affect as "local".
*/
public static void setTimeSource(String timeSource)
{
work.obtain().timeSource = (timeSource == null) ? "LOCAL" : timeSource;
}
/**
* Sets the <code>TIME-SOURCE</code> attribute for the user's context.
* This attribute is can be read and written but it is not honored for
* date/time processing in P2J. The intention of the attribute is to define
* the system from which time values will be obtained. In P2J, time values
* are always obtained from the application server.
*
* @param timeSource
* The new value to set. If <code>null</code>, then the empty
* string will be used, which in the 4GL has the same affect as
* "local".
*/
public static void setTimeSource(character timeSource)
{
if (timeSource == null || timeSource.isUnknown())
{
setTimeSource("LOCAL");
}
else
{
setTimeSource(timeSource.toStringMessage());
}
}
/**
* Sets the <code>DISPLAY-TIMEZONE</code> attribute for the user's context.
* This attribute is used for displaying DATETIME-TZ values that do not have
* the timezone specified in the display format.
*
* @param displayTimezone
* The new value to set.
*/
public static void setDisplayTimeZone(long displayTimezone)
{
setDisplayTimeZone(new integer(displayTimezone));
}
/**
* Sets the <code>DISPLAY-TIMEZONE</code> attribute for the user's context.
* This attribute is used for displaying DATETIME-TZ values that do not have
* the timezone specified in the display format.
*
* @param displayTimezone
* The new value to set.
*/
public static void setDisplayTimeZone(NumberType displayTimezone)
{
final WorkArea wa = work.obtain();
if (displayTimezone == null || displayTimezone.isUnknown())
{
wa.displayTimezone = new integer();
return;
}
// 4GL will allow any value here wrapping at 32768
wa.displayTimezone = new integer((short) displayTimezone.longValue());
}
/**
* Converts a 2 digit year to a 4 digit year, if needed. Negative numbers
* are not converted as these are considered to be in the BC era. The
* conversion is based on the windowing start year which can be specified
* using {@link #setWindowingYear}.
*
* @param year
* The year which will be converted to a 4 digit year based on
* the starting window year if and only if it is between 0 and
* 99 inclusive.
*
* @return The converted 4 digit year or the passed-in year if the
* parameter does not need conversion.
*/
public static int windowingImpl(int year)
{
return date.windowingImpl(year, work.obtain().windowingYear);
}
/**
* Calculates the current date using the local system clock time.
*
* @return The current date as a julian day number.
*/
private static long _today()
{
return millisToJulianDay(dateToLocalMillis(null, date.getDefaultTimeZone()));
}
/**
* Converts a 2 digit year to a 4 digit year, if needed. Negative numbers
* are not converted as these are considered to be in the BC era. The
* conversion is based on the windowing start year which can be specified
* using the second argument.
*
* @param year
* The year which will be converted to a 4 digit year based on
* the starting window year if and only if it is between 0 and
* 99 inclusive.
* @param windowingYear
* The window start year used for conversion.
*
* @return The converted 4 digit year or the passed-in year if the
* parameter does not need conversion.
*/
private static int windowingImpl(int year, int windowingYear)
{
int converted = year;
// is this year a 2 digit year?
if (year >= 0 && year <= 99)
{
// use integer arithmetic to "drop" everything but the century
int century = windowingYear / 100;
century *= 100;
// calculate the difference as the "break" year
int breakyr = windowingYear - century;
// everything at or later than the break year is in the earlier
// century
if (year >= breakyr)
{
converted = year + century;
}
else
{
converted = year + century + 100;
}
}
return converted;
}
/**
* Converts a 2 digit year to a 4 digit year, if needed. Negative numbers
* are not converted as these are considered to be in the BC era. The
* conversion is based on the windowing start year which can be specified
* using the second argument.
*
* @param year
* The year which will be converted to a 4 digit year based on
* the starting window year if and only if it is between 0 and
* 99 inclusive.
*
* @return The converted 4 digit year or the passed-in year if the
* parameter does not need conversion.
*/
public static int windowing(final int year)
{
return windowingImpl(year, getWindowingYear());
}
/**
* Returns the number of seconds since midnight in the current JVM timezone.
*
* @return The number of seconds since midnight.
*/
public static int64 secondsSinceMidnight()
{
// this represents the current time in the default JVM timezone
return secondsSinceMidnight(new GregorianCalendar());
}
/**
* Returns the number of seconds since midnight in the given calendar.
*
* @param gc
* The calendar to use for the calculation which includes the time
* (in millis since epoch) and timezone. This may be set to any
* time value (in the future or present) including the current time.
*
* @return The number of seconds since midnight.
*/
public static int64 secondsSinceMidnight(GregorianCalendar gc)
{
// query the time value
int hrs = gc.get(Calendar.HOUR_OF_DAY);
int min = gc.get(Calendar.MINUTE);
int secs = gc.get(Calendar.SECOND);
long ssm = (hrs * 3600) + (min * 60) + secs;
return int64.of(ssm);
}
/**
* Returns the number of milliseconds since the base milliseconds counter
* was reset or since the process started (this is an approximation).
*
* @return The number of elapsed milliseconds.
*/
public static int64 elapsed()
{
return elapsed(false);
}
/**
* Returns the number of milliseconds since the base milliseconds counter was reset or since
* the process started (this is an approximation).
*
* @param reset
* {@code true} to reset the base milliseconds counter to the current time.
*
* @return The number of elapsed milliseconds.
*/
public static int64 elapsed(boolean reset)
{
WorkArea wa = work.obtain();
int64 result = int64.of(System.currentTimeMillis() - wa.baseMillis);
if (reset)
{
wa.baseMillis = System.currentTimeMillis();
}
return result;
}
/**
* Returns the number of milliseconds since the base milliseconds counter was reset or since
* the process started (this is an approximation).
*
* @param reset
* {@code true} to reset the base milliseconds counter to the current time.
*
* @return The number of elapsed milliseconds.
*/
public static int64 elapsed(logical reset)
{
return elapsed(reset.booleanValue());
}
/**
* Adds <code>days</code> to the date of the left operand and returns a
* new instance representing that date.
*
* @param op1
* The left operand.
* @param op2
* The right operand (the number of days to add to the day
* number). Note that this can be a negative number to
* effectively subtract instead of add.
*
* @return The date which is <code>days</code> different from the
* left operand or <code>unknown value</code> if any input is
* <code>unknown value</code>.
*/
public static date plusDays(date op1, long op2)
{
if (op1.isUnknown())
{
return new date();
}
return new date(op1.julian + op2);
}
/**
* Adds <code>days</code> to the date of the left operand and returns a
* new instance representing that date.
*
* @param op1
* The left operand.
* @param op2
* The right operand (the number of days to add to the day
* number). Note that this can be a negative number to
* effectively subtract instead of add. As in Progress, the
* value is rounded to the nearest integral value before usage.
*
* @return The date which is <code>days</code> different from the
* left operand or <code>unknown value</code> if any input is
* <code>unknown value</code>.
*/
public static date plusDays(date op1, double op2)
{
if (op1.isUnknown())
{
return new date();
}
return new date(op1.julian + Math.round(op2));
}
/**
* Adds <code>days</code> to the date of the left operand and returns a
* new instance representing that date.
*
* @param op1
* The left operand.
* @param op2
* The right operand (the number of days to add to the day
* number). Note that this can be a negative number to
* effectively subtract instead of add.
*
* @return The date which is <code>days</code> different from the
* left operand or <code>unknown value</code> if any input is
* <code>unknown value</code>.
*/
public static date plusDays(date op1, NumberType op2)
{
if (op1.isUnknown() || op2.isUnknown())
{
return new date();
}
return plusDays(op1, op2.longValue());
}
/**
* Convenience method to subtract <code>days</code> from the date which
* is the left operand and return a new instance representing that date.
* This just uses {@link #plusDays} after negating the parameter's sign.
*
* @param op1
* The left operand.
* @param op2
* The right operand (the number of days to subtract from the
* day number.
*
* @return The date which is <code>days</code> different from the
* left operand or <code>unknown value</code> if any input is
* <code>unknown value</code>.
*/
public static date minusDays(date op1, long op2)
{
return plusDays(op1, -op2);
}
/**
* Convenience method to subtract <code>days</code> from the date which
* is the left operand and return a new instance representing that date.
* This just uses {@link #plusDays} after negating the parameter's sign.
*
* @param op1
* The left operand.
* @param op2
* The right operand (the number of days to subtract from the
* day number. As in Progress, the value is rounded to the
* nearest integral value before usage.
*
* @return The date which is <code>days</code> different from the
* left operand or <code>unknown value</code> if any input is
* <code>unknown value</code>.
*/
public static date minusDays(date op1, double op2)
{
return plusDays(op1, -op2);
}
/**
* Convenience method to subtract <code>days</code> from the date which
* is the left operand and return a new instance representing that date.
* This just uses {@link #minusDays}.
*
* @param op1
* The left operand.
* @param op2
* The right operand (the number of days to subtract from the
* day number.
*
* @return The date which is <code>days</code> different from the
* left operand or <code>unknown value</code> if any input is
* <code>unknown value</code>.
*/
public static date minusDays(date op1, NumberType op2)
{
if (op1.isUnknown() || op2.isUnknown())
{
return new date();
}
return minusDays(op1, op2.longValue());
}
/**
* Returns the number of days difference between the the dates of two
* instances.
*
* @param op1
* The left operand. Must NOT be <code>unknown</code>.
* @param op2
* The right operand (instance to subtract from the left
* operand). Must NOT be <code>unknown</code>.
*
* @return The difference in days between the two dates.
*/
public static int difference(date op1, date op2)
{
// this is safe since the difference will never overflow an int,
// because internally Progress limits years to a short (-32768 to 32767)
return (int) (op1.julian - op2.julian);
}
/**
* Returns the number of days difference between the dates 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 dates or <code>unknown value</code> if
* any input is <code>unknown value</code>.
*/
public static integer differenceNum(date op1, date op2)
{
if (op1.isUnknown() || op2.isUnknown())
{
return new integer();
}
return new integer(difference(op1, op2));
}
/**
* Returns the current day of the week for the given <code>date</code>.
*
* @param dd
* The date to query.
*
* @return The day of the month.
*/
public static integer weekday(date dd)
{
return dd.getWeekdayNum();
}
/**
* Returns the current day of the month for the given <code>date</code>.
*
* @param dd
* The date to query.
*
* @return The day of the month.
*/
public static integer day(date dd)
{
return dd.getDayNum();
}
/**
* Returns the month of the year for the given <code>date</code>.
*
* @param dd
* The date to query.
*
* @return The month (1-based).
*/
public static integer month(date dd)
{
return dd.getMonthNum();
}
/**
* Returns the year for the given <code>date</code>.
*
* @param dd
* The date to query.
*
* @return The year.
*/
public static integer year(date dd)
{
return dd.getYearNum();
}
/**
* Creates a new <code>date</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 date instantiateUnknownDate()
{
return new date();
}
/**
* Return the default display format string for this type.
*
* @return The default format string.
*/
@Override
public String defaultFormatString()
{
return DEFAULT_FORMAT;
}
/**
* 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;
}
long raw = julian - RAW_EPOCH;
if (raw == 0)
{
return 1;
}
else if (-127L <= raw && raw <= 127L) // 0x7F note: 7F, the other are 7E...
{
return 2;
}
else if (-32_512L <= raw && raw <= 32_511L) // 0x7EFFL
{
return 3;
}
else if (-8_323_072L <= raw && raw <= 8_323_071L) // 7E_FFFF
{
return 4;
}
else
{
return 5;
}
}
/**
* 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)
{
// compare to itself ?
return true;
}
if (o instanceof unknown && this.isUnknown())
{
// if this is unknown then it is equal to typeless unknown
return true;
}
if (!(o instanceof date) || o instanceof datetime)
{
// a date can be compared ONLY WITH other instances of date
return false;
}
date that = (date) 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.julian == that.julian;
}
/**
* 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;
}
result = 37 * result + (int) (julian ^ (julian >>> 32));
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.
*/
@Override
public date duplicate()
{
return new date(this);
}
/**
* 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 date();
}
/**
* 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 date();
}
/**
* Reports if this instance represents the <code>unknown value</code>.
*
* @return <code>true</code> if this instance is set to the <code>unknown value</code>.
*/
@Override
public boolean isUnknown()
{
return unknown;
}
/**
* Sets the state of this instance's <code>unknown value</code> flag to
* <code>true</code>.
* <p>
* <b>Warning: a separate call is needed to ensure that the data of this
* instance is set to the correct value.</b>
*/
@Override
public void setUnknown()
{
if (!unknown)
{
checkUndoable(true);
}
unknown = true;
}
/**
* 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>date</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 (including the unknown literal) or is a string with the
* value '?', 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 variant is meant to handle the cases of built-in functions and methods in the 4GL
* which have polymorphic return types (e.g. DYNAMIC-FUNCTION()). This should NOT be used for
* non-polymorphic assignments.
*
* @param value
* The instance from which to copy state.
*/
@Override
public void assign(BaseDataType value)
{
long oldJulian = julian;
if (value == null)
{
setUnknown();
return;
}
// the value may be a proxy; make sure to unwrap and work with the real value
value = value.val();
if (value.isUnknown())
{
if (value instanceof longchar || value instanceof clob || value instanceof blob)
{
incompatibleTypesOnConversion();
}
else
{
setUnknown();
}
}
else if (value instanceof date)
{
// this is the common path, also handles datetime and datetimetz
assign((date) value);
}
else
{
// this path is only used when type morphing is needed
if (value == null || value.isUnknown())
{
setUnknown();
}
else if (value instanceof character)
{
// covers both character and longchar
String txt = value.toStringMessage();
if ("?".equals(txt))
{
// quick out
setUnknown();
}
else
{
// try the more extensive datetime-tz parsing first
if (ErrorManager.nestedSilent(() ->
{
assign(new datetimetz(txt));
}))
{
// try the less-specific parsing approach
WorkArea wa = getWorkArea();
julian = parseWorker(value.toStringMessage(), wa.dateFormat, wa.windowingYear);
if (julian == INVALID_DATE)
{
setUnknown();
}
else
{
unknown = false;
}
}
}
}
else if (value instanceof logical)
{
/* true = 1 => [01.01.-4713], false = 0 => [31.12.-4714] */
julian = ((logical) value).booleanValue() ? 1 : 0;
unknown = false;
}
else if (value instanceof handle)
{
Long id = ((handle) value).getResourceId();
if (id != null)
{
julian = id;
unknown = false;
}
else
{
setUnknown();
}
}
else if (value instanceof NumberType)
{
julian = ((NumberType) value).longValue();
unknown = false;
}
else if (value instanceof jobject)
{
assign((jobject<?>) value);
}
else
{
setUnknown();
// conversion not possible
incompatibleTypesOnConversion();
}
}
if (oldJulian != julian)
{
checkUndoable(true);
}
}
/**
* Sets the state (data and unknown value) of this instance based on the state of the passed
* instance. If a derived class object is passed (datetime or datetimetz), then the extra
* informations (time, time-zone offset) are ignored. Overriding methods from derived classes
* must call supper to assign these bits of information.
*
* @param value
* The instance from which to copy state.
*/
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;
}
deepAssign(value.toLocalDatetime());
}
/**
* Sets the state (data and unknown value) of this instance based on the
* state of the passed instance.
*
* @param value
* The instance from which to copy state.
*/
@Override
public void assign(Undoable value)
{
if (value instanceof BaseDataType)
{
assign((BaseDataType) value);
}
else
{
throw new RuntimeException("Assignment of " + value.getClass() +
" to " + this.getClass() + " not implemented.");
}
}
/**
* 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(date value)
{
// the value may be a proxy; make sure to unwrap and work with the real value
if (BaseDataType.isProxy(value))
{
if (value.val() instanceof date)
{
value = (date) value.val();
}
else
{
assign(value.val());
return;
}
}
if (value == null || value.isUnknown())
{
setUnknown();
return;
}
checkUndoable(value);
julian = value.julian;
unknown = value.unknown;
}
/**
* Increments the Julian day number by 1.
*
* @return The instance of this date object
*/
public date increment()
{
checkUndoable(true);
julian++;
return this;
}
/**
* Decrements the Julian day number by 1.
*
* @return The instance of this date object
*/
public date decrement()
{
checkUndoable(true);
julian--;
return this;
}
/**
* Sets the Julian day number (independently of the state of the unknown
* value) as an integral number of days since the Progress 4GL base date
* of 12/31/-4714. This also sets the <code>unknown value</code> state
* for this instance to <code>false</code>.
*
* @param day
* The number of days since 12/31/-4714.
*/
public void setDayNumber(long day)
{
if (unknown || day != julian)
{
checkUndoable(true);
}
julian = day;
unknown = false;
}
/**
* Sets the Julian day number (independently of the state of the unknown
* value) as an integral number of days since the Progress 4GL base date
* of 12/31/-4714. This also sets the <code>unknown value</code> state
* for this instance to <code>false</code>.
*
* @param day
* The number of days since 12/31/-4714.
*/
public void setDayNumber(double day)
{
setDayNumber(new decimal(day));
}
/**
* Sets the Julian day number (independently of the state of the unknown
* value) as an integral number of days since the Progress 4GL base date
* of 12/31/-4714. This also sets the <code>unknown value</code> state
* for this instance to <code>false</code>.
*
* @param day
* The number of days since 12/31/-4714.
*/
public void setDayNumber(NumberType day)
{
if (day.isUnknown())
{
setUnknown();
}
else
{
setDayNumber(day.longValue());
}
}
/**
* Returns the instance which has a later date.
*
* @param d
* The instance to compare against.
*
* @return The instance with the latest date.
*/
public date maximum(date d)
{
return (date) super.maximum(d);
}
/**
* Returns the instance which has an earlier date.
*
* @param d
* The instance to compare against.
*
* @return The instance with the earliest date.
*/
public date minimum(date d)
{
return (date) super.minimum(d);
}
/**
* 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>.
*/
@Override
public int compareTo(Object obj)
{
Object tmp = obj;
if (obj instanceof BaseDataType)
{
BaseDataType bdt = (BaseDataType) obj;
tmp = new date(bdt);
/**
* An unknown value should be always converted to a unknown date value.
* A non-unknown value should be always converted to a non-unknown date value.
* If an unknown value is converted to a non-unknown value the conversion fails.
* If a non-unknown value is converted to an unknown value the conversion fails.
*/
if (((date) tmp).isUnknown() != bdt.isUnknown())
{
incompatibleTypesOnConversion();
}
}
else
{
/* 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");
// However, if the compare operator receives dynamically different kind of date-related
// only the "common parts" are compared - ie. ignore additional fields from derived classes.
date that = (date) tmp;
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;
}
if (this.julian < that.julian)
{
return -1;
}
else if (this.julian > that.julian)
{
return 1;
}
// otherwise they are equals
return 0;
}
/**
* Returns the month of the current instance from 1 to 12 independent of
* any timezone considerations.
*
* @return The month.
*/
public int getMonth()
{
long millis = julianDayToMillis(julian);
GregorianCalendar gc = neutralMillisToCalendar(millis, null);
// Java months are zero-indexed
return gc.get(Calendar.MONTH) + 1;
}
/**
* Returns the month of the current instance from 1 to 12 independent of
* any timezone considerations.
*
* @return The month or <code>unknown</code> if this instance is
* <code>unknown</code>.
*/
public integer getMonthNum()
{
return isUnknown() ? new integer() : new integer(getMonth());
}
/**
* Returns the day of the month of the current instance from 1 to the
* highest possible day of the corresponding month (maximum of 31)
* independent of any timezone considerations.
*
* @return The day of the month or <code>unknown</code> if this instance is
* <code>unknown</code>.
*/
public int getDay()
{
long millis = julianDayToMillis(julian);
GregorianCalendar gc = neutralMillisToCalendar(millis, null);
return gc.get(Calendar.DAY_OF_MONTH);
}
/**
* Returns the day of the month of the current instance from 1 to the
* highest possible day of the corresponding month (maximum of 31)
* independent of any timezone considerations.
*
* @return The day of the month or <code>unknown</code> if this instance
* is <code>unknown</code>.
*/
public integer getDayNum()
{
return isUnknown() ? new integer() : new integer(getDay());
}
/**
* Returns the day of the week of the current instance from 1 (Sunday) to
* 7 (Saturday) independent of any timezone considerations.
*
* @return The day of the week or <code>unknown</code> if this instance is
* <code>unknown</code>.
*/
public int getWeekday()
{
long millis = julianDayToMillis(julian);
GregorianCalendar gc = neutralMillisToCalendar(millis, null);
return gc.get(Calendar.DAY_OF_WEEK);
}
/**
* Returns the day of the week of the current instance from 1 (Sunday) to
* 7 (Saturday) independent of any timezone considerations.
*
* @return The day of the week or <code>unknown</code> if this instance is
* <code>unknown</code>.
*/
public integer getWeekdayNum()
{
return isUnknown() ? new integer() : new integer(getWeekday());
}
/**
* Returns the year of the current instance with a negative value used
* to represent the years in the BC era, independent of any timezone
* considerations.
*
* @return The year.
*/
public int getYear()
{
long millis = julianDayToMillis(julian);
GregorianCalendar gc = neutralMillisToCalendar(millis, null);
int year = gc.get(Calendar.YEAR);
if (gc.get(Calendar.ERA) == GregorianCalendar.BC)
year = -year;
return year;
}
/**
* Returns the year of the current instance with a negative value used
* to represent the years in the BC era, independent of any timezone
* considerations.
*
* @return The year or <code>unknown</code> if this instance is <code>unknown</code>.
*/
public integer getYearNum()
{
return isUnknown() ? new integer() : new integer(getYear());
}
/**
* 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(julian), override);
}
/**
* Returns the day number as a <code>double</code>.
*
* @return The date as the number of days since 12/31/-4714.
*/
public double doubleValue()
{
return julian;
}
/**
* Returns the day number as an {@link integer}.
*
* @return The date as the number of days since 12/31/-4714.
*/
public decimal decimalValue()
{
return new decimal(julian);
}
/**
* Returns the day number as an <code>int</code>.
*
* @return The date as the number of days since 12/31/-4714.
*/
public int intValue()
{
return (int) julian;
}
/**
* Returns the day number as an {@link integer}.
*
* @return The date as the number of days since 12/31/-4714.
*/
public integer integerValue()
{
return new integer(julian);
}
/**
* Returns the day number as a <code>long</code>.
*
* @return The date as the number of days since 12/31/-4714.
*/
public long longValue()
{
return julian;
}
/**
* Determines if the current date lies inside the 100 year window where
* a 2-digit date implicitly corresponds to a 4-digit date.
*
* @return <code>true</code> if the year is within the range specified
* beginning at the windowing year and ending 99 years later.
* By default, the windowing year is 1950 so the ending year
* is 2049. Any dates in this range can be specified as
* 2-digits.
*/
public boolean inY2KWindow()
{
WorkArea wa = getWorkArea();
int year = getYear();
return (year >= wa.windowingYear && year < (wa.windowingYear + 100));
}
/**
* Creates a SQL compatible string representation of the instance data
* (the format is YYYY-MM-DD).
* <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.
*/
public String toStringSQL()
{
NumberFormat nf = NumberFormat.getInstance();
nf.setMinimumIntegerDigits(4);
nf.setGroupingUsed(false);
StringBuilder sb = new StringBuilder();
sb.append(nf.format(getYear()));
sb.append('-');
nf.setMinimumIntegerDigits(2);
sb.append(nf.format(getMonth()));
sb.append('-');
sb.append(nf.format(getDay()));
return sb.toString();
}
/**
* Creates a string representation of the instance data using the default
* Progress 4GL format of '99/99/99' which is only useful for displaying
* dates in the Y2K window since any other date will generally not be
* displayable in such a format. Some dates are too large to fit in the
* default 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>
* 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.
*
* @throws IllegalArgumentException
* If the format string passed is invalid.
*/
@Override
public String toString()
{
return toString(null);
}
/**
* Creates a string representation of the instance data in a form that is
* compatible with the <code>MESSAGE</code> language statement. If the
* instance represents the <code>unknown value</code>, a '?' will be
* returned.
*
* @return The 'message' formatted string.
*/
@Override
public String toStringMessage()
{
return toStringExport();
}
/**
* 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.
* <p>
* This implementation creates the given string based on the order of
* date components (e.g. MDY, YMD...) defined for this context.
*
* @return The 'export' formatted string.
*/
@Override
public String toStringExport()
{
if (unknown)
{
return "?";
}
NumberFormat nf = NumberFormat.getInstance();
nf.setMinimumIntegerDigits(2);
nf.setGroupingUsed(false);
WorkArea wa = getWorkArea();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 3; i++)
{
if (wa.dateFormat[i] == MONTH)
{
sb.append(nf.format(getMonth()));
}
else if (wa.dateFormat[i] == DAY)
{
sb.append(nf.format(getDay()));
}
else
{
String yr = toYearString(3, 6, true);
if (yr == null)
{
return StringHelper.repeatChar('?', DEFAULT_FORMAT.length() - 2);
}
sb.append(yr);
}
// add separators between the 1st/2nd and 2nd/3rd components
if (i < 2)
{
sb.append('/');
}
}
return sb.toString();
}
/**
* Create a string representation of this date using a Progress 4GL style
* date format string such as 99/99/99 or 99-99-9999. The '/', '-' and
* '.' are valid separators. All 3 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 '99/99/99'.
*
* @return The formatted string or the 'failure' string if the date is too large to fit in
* the specified format.
* If the date is {@code unknown} an empty string is returned.
*
* @throws IllegalArgumentException
* If the format string passed is invalid.
*/
@Override
public String toString(String fmt)
throws IllegalArgumentException
{
// set our default format as needed
if (fmt == null || fmt.trim().isEmpty())
{
fmt = DEFAULT_FORMAT;
}
if (isUnknown())
{
char[] blanks = new char[fmt.length()];
Arrays.fill(blanks, ' ');
return new String(blanks);
}
final ParsedDateFormat parsed = ParsedDateFormat.getInstance(fmt);
final NumberFormat nf = NumberFormat.getInstance();
nf.setGroupingUsed(false);
final byte[] dateFormat = getWorkArea().dateFormat;
final StringBuilder sb = new StringBuilder();
// create our output string in proper component order
for (int n = 0; n < 3; n++)
{
final byte comp = dateFormat[n];
if(comp == YEAR)
{
// string output for the YEAR component
final int size = parsed.size[n];
final String yr = toYearString(size, size, false);
if (yr == null)
{
return genFailedString(fmt);
}
sb.append(yr);
}
else
{
final int val = comp == MONTH ? getMonth() : getDay();
// string output for MONTH or DAY components
// will it fit?
final int compSize = parsed.size[n];
if (compSize == 1 && val > 9)
{
return genFailedString(fmt);
}
nf.setMinimumIntegerDigits(compSize);
sb.append(nf.format(val));
}
if (parsed.hasSep())
{
if (n == 0)
{
// output the first separator
sb.append(parsed.sep1);
}
else if (n == 1)
{
// output the second separator
sb.append(parsed.sep2);
}
}
}
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.
*/
@Override
public void readExternal(ObjectInput in)
throws IOException,
ClassNotFoundException
{
unknown = in.readBoolean();
julian = in.readLong();
}
/**
* 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
{
out.writeBoolean(unknown);
out.writeLong(julian);
}
/**
* Parses a string with a format based on the Progress 4GL rules for converting a string to a
* {@code date}.
* <p>
* If the {@code spec} string is empty or equals '?', an unknown date object is instantiated.
*
* @param spec
* The date to parse.
* @param format
* 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.
*/
protected void instantiateFromStringWorker(final String spec, final String format, final int windowingYear)
{
if (spec == null || unknownDateSpecRegexp.matcher(spec).matches())
{
setUnknown();
return;
}
if ("today".equalsIgnoreCase(spec))
{
assign(date.today());
return;
}
int effWindowingYear = windowingYear < 0 ? getWindowingYear() : windowingYear;
checkUndoable(() -> new date(spec, format, effWindowingYear));
byte[] binaryFormat = format == null/*auto*/ ? getWorkArea().dateFormat : initFormat(format);
julian = parseWorker(spec, binaryFormat, effWindowingYear);
unknown = (julian == INVALID_DATE);
}
/**
* Returns the absolute time in milliseconds since Java 'Epoch' or 0
* if this instance is <code>unknown</code>.
*
* @return The absolute time in milliseconds.
*/
protected long getAbsoluteTimeOffset()
{
return isUnknown() ? 0 : julian * date.MILLIS_PER_DAY;
}
/**
* Return an object equal to this expressed in local time (current timezone offset).
*
* @return this object (it's already in local time)
*/
public datetime toLocalDatetime()
{
datetime ret = new datetime();
ret.deepAssign(this);
return ret;
}
/**
* All {@code date} and {@code datetime} used in 4GL are assumed at local time-zone. This method will
* convert them to the appropriate UTC instant.
*
* @return the UTC instant of this {@code date} instance.
*/
public datetime utc()
{
int tz = datetimetz.getDefaultTimezoneOffset(this); // minutes
return datetime.plusMillis(new datetime(this), -tz * 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-DD") or "?" if <code>unknown</code>.
*/
public String getIsoDate()
{
if (isUnknown())
{
return "?";
}
int year = getYear();
if (year < -999 || year > 9999)
{
// this is a strange thing (among other) that I discovered in Progress
ErrorManager.recordOrThrowError(0, "");
return "?";
}
int month = getMonth();
int day = getDay();
StringBuilder sb = new StringBuilder();
if (year < 1000)
{
if (year < 100)
{
if (year < 10)
{
sb.append('0');
}
sb.append('0');
}
sb.append('0');
}
sb.append(year);
sb.append("-");
if (month < 10)
{
sb.append('0');
}
sb.append(month);
sb.append("-");
if (day < 10)
{
sb.append('0');
}
sb.append(day);
return sb.toString();
}
/**
* Parses this ISO 8601 value as a date.
*
* @param val
* The date to parse.
*
* @throws DateTimeParseException
* If the format is not ISO 8601 compliant.
*/
public void fromIso(String val)
{
LocalDate od = LocalDate.parse(val, ISO_LOCAL_DATE);
Date date = Date.from(od.atStartOfDay()
.atZone(ZoneId.systemDefault())
.toInstant());
assign(new date(date));
}
/**
* 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.
* <p>
* {@code today} is also supported, in which case the current date is returned.
*
* @param text
* The text of the literal to be parsed.
*
* @return A {@code date} representing the respective value, taking into account the current session
* settings (like the date format and the windowing year).
*/
public static date parseInitial(String text)
{
if ("today".equalsIgnoreCase(text))
{
return today();
}
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 date(text, df, getWindowingYear());
}
// if not in dynamic format, the date format is constrained to American format (MDY), even if other
// format may be active in SESSION:DATE-FORMAT.
return new date(text, DEFAULT_DATE_FORMAT, DEFAULT_DATE_WINDOWING_YEAR);
}
/**
* Validate specified values and return error information if they don't consist of correct date components.
*
* @param month
* The month.
* @param day
* The day.
* @param year
* The year.
* @param forceFourDigits
* Flag for converting the year to 4 digits before processing.
*
* @return <code>null</code> if components are valid and error information otherwise.
*/
public static ErrorEntry validate(int month, int day, int year, boolean forceFourDigits)
{
if (forceFourDigits)
{
year = windowingImpl(year);
}
if (month < 1 || month > 12)
{
return new ErrorEntry(80, "The month of a date must be from 1 to 12");
}
// some fixups to match the J2SE format
if (year < 0)
year++;
// J2SE months are 0-indexed
month--;
// we are using the default timezone
GregorianCalendar gc = nullGC.get();
// we want to detect invalid day of month without rewriting the
// calendar ourselves, so we must take a step-wise approach rather
// than the more obvious simple constructor approach (note: setLenient
// cannot be used because it doesn't trigger any exception when
// invalid input is used, even though the documentation explicitly
// states that it will do so)
gc.clear();
gc.set(Calendar.YEAR, year);
gc.set(Calendar.MONTH, month);
// now that the month and year are set, the maximum can be calculated
int limit = gc.getActualMaximum(Calendar.DAY_OF_MONTH);
if (day < 1 || day > limit)
{
return new ErrorEntry(81, "Day in month is invalid");
}
return null;
}
/**
* Obtains the text form of the given date instance in ISO 8601 standard format.
*
* @param date
* The date to format as text.
*
* @return A string that is the ISO representation of this date
* (YYYY-MM-DD) or "?" if <code>unknown</code>.
*/
public static character isoDate(date date)
{
return date.isUnknown() ? new character() : new character(date.getIsoDate());
}
/**
* Parses this ISO 8601 value as a date, datetime or datetime-tz, depending on which format matches.
* The parsing is LENIENT, and this API is meant to be used in cases where this behavior is required
* (like for SOAP or REST requests, when parsing arguments).
*
* @param val
* The date to parse.
*
* @return The date value or <code>null</code> if it can't be parsed.
*/
public static final date parseIso8601(String val)
{
try
{
// check for date
date d = new date();
d.fromIso(val);
return d;
}
catch (DateTimeParseException e)
{
// ignore
}
try
{
// check for a datetime
datetime dt = new datetime();
dt.fromIso(val);
return dt;
}
catch (DateTimeParseException e)
{
// ignore
}
try
{
// check for a datetime-tz
datetimetz dtz = new datetimetz();
dtz.fromIso(val);
return dtz;
}
catch (DateTimeParseException e)
{
// ignore
}
return null;
}
/**
* Parses a ISO-8601 like literal as a date. The format is always <code>ymd</code>.
* <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)
{
return new date(val, DEFAULT_DATETIME_FORMAT, -1);
}
/**
* Obtains the timezone offset in minutes from UTC that corresponds to the
* given timezone specification. This is a form of the TIMEZONE built-in function.
* @param spec
* timezone spec
*
* @return The timezone offset in minutes from UTC for the given spec.
*/
public static integer getOffsetForSpec(character spec)
{
if (spec.isUnknown())
{
return new integer();
}
return getOffsetForSpec(spec.toStringMessage());
}
/**
* Obtains the timezone offset in minutes from UTC that corresponds to the
* given timezone specification. This is a form of the TIMEZONE built-in
* function.
*
* @param spec
* timezone spec
* @return The timezone offset in minutes from UTC for the given spec.
*/
public static integer getOffsetForSpec(String spec)
{
if (spec == null)
{
return new integer();
}
return new integer(specToOffset(spec));
}
/**
* Obtains the effective default timezone offset in minutes from UTC for
* this user's context. This will get the default timezone as specified in
* the SESSION:TIMEZONE offset or if not specified there, the default JVM
* timezone's "raw offset" (in milliseconds) converted to minutes. This is
* a form of the TIMEZONE built-in function.
*
* @return The effective default timezone offset in minutes from UTC.
*/
public static integer getDefaultTimeZoneOffset()
{
return new integer(getDefaultOffset());
}
/**
* Get the default timezone as specified in the SESSION:TIMEZONE offset or if not specified
* there, the default JVM timezone's "offset" (raw + dst) converted from milliseconds to
* minutes.
*
* @return The default timezone.
*/
public static int getDefaultOffset()
{
return getDefaultOffset(null);
}
/**
* Get the default timezone for the date received as parameter. In case of null,
* the default JVM timezone's "offset" (raw + dst) converted from milliseconds to
* minutes.
*
* @param d
* The date for which we want to calculate the timezone's offset.
*
* @return The default timezone.
*/
public static int getDefaultOffset(Date d)
{
integer tzAttr = work.obtain().timezoneOffset; // date.getSessionTimeZone()
int offset = 0;
if (tzAttr.isUnknown())
{
// get the raw offset in millis and convert it to minutes
TimeZone defTz = TimeZone.getDefault();
// it seems that in Progress the timezone also contains the DST
offset = defTz.getRawOffset();
if (d == null && defTz.inDaylightTime(new Date()) ||
d != null && defTz.inDaylightTime(d))
{
offset += defTz.getDSTSavings();
}
offset = offset / (1000 * 60);
}
else
{
offset = tzAttr.intValue();
}
return offset;
}
/**
* Get the default timezone as specified in the SESSION:TIMEZONE offset or
* if not specified there, the default JVM timezone.
*
* @return The default timezone.
*/
public static TimeZone getDefaultTimeZone()
{
return getZoneFromOffset(getDefaultOffset());
}
/**
* Convert the offset to a timezone ID and return the resulting timezone
* instance.
*
* @param offset
* The offset in minutes (positive or negative) from UTC.
*
* @return The corresponding timezone.
*/
public static TimeZone getZoneFromOffset(int offset)
{
TimeZone tz = offset2tz.get(offset);
if (tz == null)
{
offset2tz.put(offset, tz = TimeZone.getTimeZone(offsetToZoneId(offset)));
}
return tz;
}
/**
* Remove unnecessary characters and extra zero digits from input that represents
* a date. This compensates for "tricks" that can be done in the 4GL which
* have no useful value (they don't store any data).
*
* @param input
* The text to cleanup.
*
* @return The cleaned up text.
*/
public static String cleanFormattedDate(String input)
{
// remove leading whitespace
String clean = StringHelper.safeTrimLeading(input);
int end = clean.length();
int i;
boolean hasSeparators = false;
// remove trash at end of the string (except for trailing separator
// characters)
for (i = 0; i < end; i++)
{
char ch = clean.charAt(i);
if (Character.isDigit(ch))
{
continue;
}
else if (ch == '-' || ch == '/' || ch == '.')
{
hasSeparators = true;
continue;
}
// first invalid char exits
break;
}
String cleaner = clean.substring(0, i);
// any trailing separator chars have no purpose and must be ignored
for (i = cleaner.length() - 1; i >= 0; i--)
{
char ch = cleaner.charAt(i);
if (ch != '-' && ch != '/' && ch != '.')
{
// first non-separator exits
break;
}
}
String cleanest = cleaner.substring(0, i + 1);
StringBuilder sb = new StringBuilder(cleanest);
if (hasSeparators)
{
// if input does not have separators, keep all 0s
int next = 0;
while (true)
{
next = trimZeros(sb, next);
if (next == -1)
{
break;
}
}
}
return sb.toString();
}
/**
* 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 ContextIndependentDate(this.unknown, this.julian);
}
/**
* Obtains the timezone offset in minutes from UTC that corresponds to the
* given timezone specification.
* <p>
* The string cannot be empty and it must start with a sign character.
*
* @param spec
* see above
*
* @return The timezone offset in minutes from UTC for the given spec or
* INVALID_TZ on any error.
*/
protected static int specToOffset(String spec)
{
int len = spec.length();
char[] txt = spec.toCharArray();
// default this to a character that will fail for empty string
char sign = (len > 0) ? txt[0] : ' ';
// empty string or not a sign generates this error
if (sign != '-' && sign != '+')
{
ErrorManager.recordOrThrowError(11869, "Missing sign in timezone expression");
return INVALID_TZ;
}
if (len < 5 || len > 6)
{
ErrorManager.recordOrThrowError(11872, "Invalid timezone expression");
return INVALID_TZ;
}
// digit : or digit digit : must be next
if (!Character.isDigit(txt[1]) ||
(!Character.isDigit(txt[2]) && txt[2] != ':'))
{
ErrorManager.recordOrThrowError(11871, "Invalid digit in timezone expression");
return INVALID_TZ;
}
int idx = spec.indexOf(":");
// check on colon char
if ((len == 5 && idx != 2) || (len == 6 && idx != 3))
{
ErrorManager.recordOrThrowError(11870, "Invalid colon in timezone expression");
return INVALID_TZ;
}
// last 2 characters must be digits
if (!Character.isDigit(txt[len - 2]) || !Character.isDigit(txt[len - 1]))
{
ErrorManager.recordOrThrowError(11872, "Invalid timezone expression");
return INVALID_TZ;
}
// final parsing and calc
String hh = spec.substring(1, idx);
String mm = spec.substring(idx + 1);
int hours = Integer.parseInt(hh);
int mins = Integer.parseInt(mm) + (hours * 60);
return mins * ((sign == '-') ? -1 : 1);
}
/**
* Convert the Progress-style timezone offset (in minutes) to a JVM-style
* timezone ID that can be used to obtain a <code>TimeZone</code> instance.
*
* @param offset
* The offset in minutes (positive or negative) from UTC. Must be
* between -840 and 840 inclusive.
*
* @return The corresponding timezone ID.
*/
private static String offsetToZoneId(int offset)
{
boolean sign = (offset >= 0);
int absol = Math.abs(offset);
int hours = absol / 60; // must be between 0 and 23 incl
int minutes = (absol - (hours * 60)); // must be between 0 and 59 incl
// error processing should be done in the caller due to different error
// messages needed at the call sites
return String.format("GMT%s%02d:%02d", sign ? "+" : "-", hours, minutes);
}
/**
* Get the timezone-specific calendar out of the JVM-wide cache or if one
* is not yet in the cache, create it and add it to the cache before
* returning.
*
* @param zone
* The timezone the calendar must support.
*
* @return The timezone-specific calendar.
*/
protected static GregorianCalendar getZoneCalendar(TimeZone zone)
{
if (tzcals == null)
{
tzcals = new HashMap<>();
}
ZoneCalendar zc = tzcals.get(zone);
if (zc == null)
{
zc = new ZoneCalendar(zone);
tzcals.put(zone, zc);
}
return zc.get();
}
/**
* Accesses the SESSION:TIMEZONE used as the default offset in minutes from
* UTC for this user's context.
*
* @return A safe copy of timezone offset. If this has never been set, then {@code unknown}
* will be returned.
*/
static integer getTimeZone()
{
return integer.of(work.obtain().timezoneOffset);
}
/**
* Obtains the currently configured DISPLAY-TIMEZONE attribute for the current
* user's context. This attribute is used for displaying DATETIME-TZ values that do not have
* the timezone specified in the display format.
*
* @return The display-timezone attribute.
*/
static integer getDisplayTimeZone()
{
return new integer(work.obtain().displayTimezone);
}
/**
* Obtains the currently configured TIME-SOURCE attribute for the current
* user's context. This attribute is can be read and written but it is
* not honored for date/time processing in P2J. The intention of the
* attribute is to define the system from which time values will be obtained.
* In P2J, time values are always obtained from the application server.
* <p>
* The default value is "local" which in Progress means that the client
* machine is the source (this is the same as the empty string ""). It is
* common to use a database name in the 4GL, which makes the application
* use the database server as the time source.
*
* @return The time source attribute.
*/
static character getTimeSource()
{
return new character(work.obtain().timeSource);
}
/**
* Generate the output string for the condition where any component of the
* date cannot fit into the specified format string. For a format of
* "99/99/99", the result will be "??/??/??". The error message is
* also processed here.
*
* @param fmt
* The format string upon which to pattern the returned result.
*
* @return The output string corresponding to a failure.
*/
private String genFailedString(String fmt)
{
String err = String.format("Value %s cannot be displayed using %s",
toStringExport(),
fmt);
if (ErrorManager.isSilentError())
{
// we don't throw an error here, as we have to return a string
// even when one of these two failures occurs, however if silent
// mode is enabled we do need to add the error text to the list
ErrorManager.addError(74, err, false);
}
else
{
// TODO: Determine if this code is needed (a previous comment noted
// that it was needed, BUT a testcase is not known that proves
// the idea. Note that this code *is* needed for NumberType
// but possibly not here. Certainly in the case of unknown
// value, this display code should not be executed. For now,
// we leave it disabled until we can determine all the paths
// through and know for sure the behavior needed.
// DO NOT RAISE AN ERROR CONDITION! Simply display this on the
// terminal's message line.
// ErrorManager.displayError(ErrorManager.buildErrorText(74, err));
}
return fmt.replaceAll("9", "?");
}
/**
* Initializes an new instance based on explicit calendar components.
* This routine will set <code>unknown value</code> if the calculated
* Julian date in invalid.
*
* @param yy
* The Progress 4GL year which may be a negative number which
* represents the BC era or a positive AD era year. The year
* 0 is invalid in Progress but in Java it is equivalent to the
* 1st year BC. All BC years are offset in J2SE by 1 from the
* Progress equivalent. Thus, the value is fixed up to be
* J2SE compatible.
* @param mm
* The month from 1 to 12 (Progress format) which is then
* fixed up to be J2SE compatible before use (0-based).
* @param dd
* The day of the specified month between 1 and the largest
* possible date (31 or less depending on the month and year).
*
* @throws ErrorConditionException
* If the month or day of month are invalid.
*/
private void yymmddWorker(int yy, int mm, int dd)
throws ErrorConditionException
{
yymmddWorker(yy, mm, dd, false);
}
/**
* Initializes an new instance based on explicit calendar components.
* This routine will set <code>unknown value</code> if the calculated
* Julian date in invalid.
*
* @param yy
* The Progress 4GL year which may be a negative number which
* represents the BC era or a positive AD era year. The year
* 0 is invalid in Progress but in Java it is equivalent to the
* 1st year BC. All BC years are offset in J2SE by 1 from the
* Progress equivalent. Thus, the value is fixed up to be
* J2SE compatible.
* @param mm
* The month from 1 to 12 (Progress format) which is then
* fixed up to be J2SE compatible before use (0-based).
* @param dd
* The day of the specified month between 1 and the largest
* possible date (31 or less depending on the month and year).
* @param forceFourDigits
* If true then 2-digit years will be windowed into the right
* century.
*
* @throws ErrorConditionException
* If the month or day of month are invalid.
*/
private void yymmddWorker(int yy, int mm, int dd, boolean forceFourDigits)
throws ErrorConditionException
{
// convert 2-digit years into 4-digit
int yr = forceFourDigits ? windowingImpl(yy) : yy;
julian = instantiateDate(yr, mm, dd);
if (julian == INVALID_DATE)
{
unknown = true;
}
}
/**
* Get the current context-local data structure. If this is the first time it is called the
* value is cached for subsequent calls.
*
* @return the current context-local data structure.
*/
private WorkArea getWorkArea()
{
if (workArea == null)
{
workArea = work.obtain();
}
return workArea;
}
/**
* Returns the index position within the input string where the next separator
* (following the starting position) can be found. Any of the 3 possible separator
* characters will be found ('/', '-' or '.').
*
* @param input
* The text to scan.
* @param start
* The starting index for the search. Only characters starting at or
* following that index will be considered.
*
* @return The index of the next separator or -1 if no further separator can be
* found.
*/
private static int findNextSeparator(StringBuilder input, int start)
{
int len = input.length();
int idx = start < len ? start : len;
while (idx < len)
{
char ch = input.charAt(idx);
if (ch == '-' || ch == '/' || ch == '.')
{
return idx;
}
idx++;
}
return -1;
}
/**
* Remove leading zeros from one segment of a string that represents a date.
* All leading zeros from the given start index up to by not including the first
* non-zero character OR the character just before the next separator or end
* of text. The buffer passed in will be edited.
*
* @param sb
* The buffer to edit.
* @param start
* The index from which to remove the zeros.
*
* @return The index position just after the separator that was found or -1 if
* the end of the text was encountered.
*/
private static int trimZeros(StringBuilder sb, int start)
{
int idx = start;
int sep = findNextSeparator(sb, start);
int end = (sep != -1) ? (sep - 2) : (sb.length() - 4);
while (idx < end)
{
char z = sb.charAt(idx);
if (z == '0')
{
idx++;
}
else
{
break;
}
}
if (idx != start)
{
// there is something to remove
sb.delete(start, idx);
}
// if we removed something, we have to find the separator again
sep = findNextSeparator(sb, start);
return (sep != -1 && sep < sb.length()) ? ++sep : -1;
}
/**
* Core worker that implements the default Progress 4GL string to date
* conversion processing as documented by {@link #date(java.lang.String)}.
* <p>
* This implementation parses the given string based on the order of
* date components (e.g. MDY, YMD...) defined by the second argument.
*
* @param input
* The formatted string date to convert.
* @param dateFormat
* The format used for date conversion.
* @param windowingYear
* The year window used when year is represented with 2-digits.
*
* @return The corresponding Julian day number.
*
* @throws ErrorConditionException
*/
private long parseWorker(String input, byte[] dateFormat, int windowingYear)
throws ErrorConditionException
{
if (input == null || StringHelper.safeTrimLeading(input).isEmpty() || "?".equals(input))
{
return INVALID_DATE;
}
else if ("today".equalsIgnoreCase(input))
{
return _today();
}
else if ("now".equalsIgnoreCase(input))
{
invalidInitializer("NOW");
return INVALID_DATE;
}
// this processing would be better placed in a Progress-specific
// subclass of java.text.DateFormat
int mm = 0;
int dd = 0;
int yy = 0;
// initial cleanups
String clean = cleanFormattedDate(input);
if (StringHelper.safeTrimLeading(clean).isEmpty())
{
ErrorManager.recordOrThrowError(85, "Invalid date input");
return INVALID_DATE;
}
// standardize/normalize our separators to be '-' characters (it can
// be any mixture of '-' or '/' or '.' characters on input
String cleaner = clean.replaceAll("[./]", "-");
// handle the special case of no separators by adding them
String cleanest = addSeparators(dateFormat, cleaner);
String[] pieces = new String[3];
int len = cleanest.length();
int pos = 0;
int chk = 0;
int seps = 0;
for (int idx = 0; idx < len; idx++)
{
if (cleanest.charAt(idx) == '-' &&
(idx > 0 || idx == len - 1 || (idx != 0 && cleanest.charAt(idx + 1) != '-')))
{
seps = seps + 1;
while (idx < len && cleanest.charAt(idx + 1) == '-')
{
idx = idx + 1;
}
}
}
// a '-' char is not a separator when it is the negative sign in a
// year; there may also be bogus trailing '-' chars at the end of
// the proper date components; find the real separators and use them
// to split the data into the 2 or 3 components that are specified
for (int j = 0; j < 3; j++)
{
byte comp = dateFormat[j];
// test for the special MM-DD parsing mode that ignores the year
// component (and takes the current year instead)
if (seps == 1 && comp == YEAR )
{
// bypass the year component
continue;
}
// avoid treating a leading negative sign on the component, as a separator
if (cleanest.charAt(chk) == '-')
{
chk++;
}
// find the next separator
int idx = cleanest.indexOf('-', chk);
// there must always be at least 1 separator and 2 components
if (idx >= 0)
{
pieces[comp] = cleanest.substring(pos, idx);
pos = idx + 1;
chk = pos;
// if the input data ends with a separator, we are done
if (chk >= len)
break;
}
else
{
// get the rest of the string and exit
pieces[comp] = cleanest.substring(pos);
pos = len;
chk = pos;
break;
}
}
if (chk < len)
{
ErrorManager.recordOrThrowError(85, "Invalid date input");
return INVALID_DATE;
}
// safety code for empty strings (probably should not ever happen
// anyway, but let's be safe)
for (int x = 0; x < 3; x++)
{
if (pieces[x] != null && pieces[x].length() == 0)
{
pieces[x] = null;
}
}
// at this point our pieces array is indexed by our component types
// and should hold 2 or 3 non-null strings
if (pieces[DAY] == null || pieces[MONTH] == null)
{
if (pieces[MONTH] != null)
{
mm = Integer.parseInt(pieces[MONTH]);
if (mm < 1 || mm > 12)
{
ErrorManager.recordOrThrowError(80, "The month of a date must be from 1 to 12");
return INVALID_DATE;
}
}
ErrorManager.recordOrThrowError(85, "Invalid date input");
return INVALID_DATE;
}
dd = Integer.parseInt(pieces[DAY]);
mm = Integer.parseInt(pieces[MONTH]);
if (pieces[YEAR] != null)
{
yy = Integer.parseInt(pieces[YEAR]);
if (pieces[YEAR].length() <= 2)
yy = date.windowingImpl(yy, windowingYear);
else if (yy == 0)
{
ErrorManager.recordOrThrowError(79, "Year is out of range or 0");
return INVALID_DATE;
}
}
else
{
// query the current year
yy = date.today().getYear();
}
return instantiateDate(yy, mm, dd);
}
/**
* Add separators to a string (which has no separators) where the data
* represents a date. Progress date parsing from a string has a special
* mode in which there are no separators. Special parsing rules apply.
* This method adds the separators that are defined by those special rules.
* <p>
* The rules as they are known:
* <p>
* <pre>
* Input Length YMD Format MDY Format Notes
* ------------ ---------- ---------- ---------------------------------
* less than 3 n/a n/a "Invalid date format" error 85
* 3 MMD MMD
* 4 MMDD MMDD
* 5 YYMMD MMDDY
* 6 YYMMDD MMDDYY
* 7 YYMMDD MMDDYY the 7th character is dropped
* 8 YYYYMMDD MMDDYYYY
* more than 8 YYYYMMDD MMDDYYYY the 9th and following characters are dropped
* </pre>
* <p>
* This method adds '-' separators and only deals with '-' separators.
*
* @param dateFormat
* Caller provided dateFormat. Must not be <code>null</code>.
* @param input
* Text representation of a date with no separators. Must not
* be <code>null</code>.
*
* @return A new text representation of a date with separators added if
* they had not already been present. If separators existed in
* the input string, then the input string will be returned unchanged.
*/
private static String addSeparators(byte[] dateFormat, String input)
{
if (input.indexOf('-') < 0)
{
int len = input.length();
if (len < 3)
{
if (!"".equals(input.trim()))
{
ErrorManager.recordOrThrowError(85, "Invalid date input");
}
return input;
}
StringBuilder sb = new StringBuilder();
// all forms under 8 characters parse all components as 2 characters
// (or less in the case of a 3 character length), so the order of
// the components does not matter
if (len < 8)
{
// special case of trimming
if (len == 7)
{
// drop the extra character
input = input.substring(0, 6);
len = 6;
}
// all of the under 8 sized cases have at least 2 fields with the
// first field always being 2 characters in size
sb.append(input.substring(0, 2)).append('-');
if (len < 5)
{
// these cases only have 2 date components, read the rest
sb.append(input.substring(2));
}
else
{
// these cases have 3 fields
sb.append(input.substring(2, 4)).append('-').append(input.substring(4));
}
}
else
{
// special case of trimming
if (len > 8)
{
// drop the extra character(s)
input = input.substring(0, 8);
len = 8;
}
int pos = 0;
// the insertion of the separators must be made according to the
// order of date components since the year component will be
// parsed as 4 characters in this case
for (int j = 0; j < 3; j++)
{
int sz = (dateFormat[j] == YEAR) ? 4 : 2;
int end = pos + sz;
sb.append(input.substring(pos, end));
pos = end;
if (j < 2)
{
sb.append('-');
}
}
}
return sb.toString();
}
return input;
}
/**
* Convert the components of a date into a Julian day number that is Progress compatible.
*
* @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. In Java the year 0 is equivalent to
* the 1st year BC (all BC years are offset in J2SE by 1 from the
* Progress equivalent). Any valid values are fixed up to be
* J2SE compatible.
* @param month
* The month from 1 to 12 (Progress format) which is then
* fixed up to be J2SE compatible before use (0-based).
* @param day
* The day of the specified month between 1 and the largest
* possible date (31 or less depending on the month and year).
*
* @return The Progress compatible Julian day number.
*
* @throws ErrorConditionException
*
* @throws IllegalArgumentException
* If the month or day of month are invalid.
*/
protected static long instantiateDate(int year, int month, int day)
throws ErrorConditionException
{
if (year < -32768 || year > 32767 || year == 0)
{
ErrorManager.recordOrThrowError(79, "Year is out of range or 0");
return INVALID_DATE;
}
if (month < 1 || month > 12)
{
ErrorManager.recordOrThrowError(80, "The month of a date must be from 1 to 12");
return INVALID_DATE;
}
// some fixups to match the J2SE format
if (year < 0)
year++;
// J2SE months are 0-indexed
month--;
// we are using the default timezone
GregorianCalendar gc = nullGC.get();
// we want to detect invalid day of month without rewriting the
// calendar ourselves, so we must take a step-wise approach rather
// than the more obvious simple constructor approach (note: setLenient
// cannot be used because it doesn't trigger any exception when
// invalid input is used, even though the documentation explicitly
// states that it will do so)
gc.clear();
gc.set(Calendar.YEAR, year);
gc.set(Calendar.MONTH, month);
// now that the month and year are set, the maximum can be calculated
int limit = gc.getActualMaximum(Calendar.DAY_OF_MONTH);
if (day < 1 || day > limit)
{
ErrorManager.recordOrThrowError(81, "Day in month is invalid");
return INVALID_DATE;
}
// insert the final piece of the puzzle
gc.set(Calendar.DAY_OF_MONTH, day);
// remove useless timezone-specific mods and return the Julian day
return millisToJulianDay(calendarToLocalMillis(gc));
}
/**
* Generate a string representation of the year based on the unusual
* rules of the Progress 4GL.
*
* @param min
* Specifies the minimum number of output characters. The result
* will be zero padded on the left as necessary. This value
* cannot be greater than the 'max' parameter but can be set to
* the same value. Must be greater than zero.
* @param max
* Specifies the maximum number of output characters. If the
* result cannot fit into this number of characters, then
* a <code>null</code> string will be returned. As a special
* case, years within the Y2K window will be truncated to
* 1 (if possible without losing data) or 2 characters if
* max is set to these values respectively. All other values
* must 'naturally' fit inside the maximum. Must be greater
* than zero and larger than the 'min' parameter.
* @param force
* Determines whether values within the Y2K window will be
* forced to 2 digits on output. This is only used for the
* unusual 'export' format.
*
* @return The formatted string or <code>null</code> if the result is
* too large to fit.
*/
protected String toYearString(int min, int max, boolean force)
{
NumberFormat nf = NumberFormat.getInstance();
// make sure the laws of logic hold
if (min > max || min < 1 || max < 1)
return null;
int yy = getYear();
int actual = min;
// if we have a negative number, we can only pad up to min - 1 chars
// except when force is enabled
if (yy < 0 && !force)
actual--;
nf.setMinimumIntegerDigits(actual);
nf.setGroupingUsed(false);
String year = nf.format(yy);
// non-export mode windowing allows truncation
if (!force && inY2KWindow() && max < 3)
{
// even truncation doesn't help if we lose data
if ((yy % 100) > 9 && max == 1)
return null;
// truncate
if (year.length() > max)
return year.substring(year.length() - max);
}
else
{
// test if we fit
if (year.length() > max)
return null;
}
// do we need to force a 2 digit output during the Y2K window?
if (force)
{
year = inY2KWindow() ? year.substring(year.length() - 2) : year;
}
return year;
}
/**
* Method that calcultes the year, month and day stored in an integer array.
* For calculating these values it uses the Julian's number.
*
* @return Array of ints that contains in order the year, month and day.
*/
protected int[] getYearMonthDay()
{
long millis = julianDayToMillis(julian);
GregorianCalendar gc = neutralMillisToCalendar(millis, null);
int[] dateParts = new int[3];
//calculate the year
dateParts[0] = gc.get(Calendar.YEAR);
if (gc.get(Calendar.ERA) == GregorianCalendar.BC)
dateParts[0] = -dateParts[0];
//calculate the month
dateParts[1] = gc.get(Calendar.MONTH) + 1;
//calculate the day
dateParts[2] = gc.get(Calendar.DAY_OF_MONTH);
return dateParts;
}
/**
* Simple 'factory' method to create instances of a specific date using the
* <code>GregorianCalendar</code> and handling the 0-based month processing on behalf of the
* caller (the caller uses a 1-based month index).
*
* @param yy
* The year.
* @param mm
* The month from 1 - 12.
* @param dd
* The day of the month.
*
* @return A date object.
*/
protected static Date createDate(int yy, int mm, int dd)
{
// some fixups to match the J2SE format
if (yy < 0)
yy++;
// J2SE months are 0-indexed
mm--;
GregorianCalendar gc = new GregorianCalendar(yy, mm, dd);
return gc.getTime();
}
/**
* Determines the order of the 3 date sub-components by reading a 3
* character string from the directory where there is one character each
* for "D" (day), "M" (month) and "Y" (year). The default value is "MDY"
* in the case where the value is not encoded in the directory.
* <p>
* This string encoding of this order will be converted into a byte array
* with 3 elements. Each element is one of the <code>DAY</code>,
* <code>MONTH</code> or <code>YEAR</code> constants of this class. Each
* element may only appear once. The resulting array will be returned and
* it defines the order of the components in a date when converted to/from
* a string.
* <p>
* The value returned may have been found via a search algorithm that
* is account (user or process) specific or group specific
* within the current server or a global default for all servers.
* <p>
* The implementation iteratively looks up the directory node under:
* /server/<serverID>/runtime/<account_or_group>/dateFormat
* <p>
* If no user/process or group nodes are present, then this is checked:
* /server/<serverID>/runtime/default/dateFormat
* <p>
* If no /server/<serverID>/runtime node exists, this is checked
* (it is the global default area for all servers):
* /server/default/runtime/<account_or_group>/dateFormat
* <p>
* Finally, if no user/process or group nodes are present in the global
* default area, then this is checked:
* /server/default/runtime/default/dateFormat
* <p>
* If no value is found via this lookup, then the Progress 4GL default
* of MDY will be returned.
*
* @param fmt
* The explicit date format string to use or <code>null</code>
* to read from the directory.
*
* @return The encoded date component order.
*/
protected static byte[] initFormat(String fmt)
{
if (fmt == null)
{
fmt = SessionUtils._startupParameters().getDateFormat();
}
if (fmt == null)
{
Directory dir = DirectoryManager.getInstance();
fmt = dir.getString(Directory.ID_RELATIVE, "dateFormat", "MDY");
}
if (fmt == null || fmt.length() != 3)
{
genInvalidDateOrder();
return ORDER_MDY;
}
// act case-insensitively
fmt = fmt.toUpperCase();
byte[] order = new byte[3];
int didx = fmt.indexOf('D');
int midx = fmt.indexOf('M');
int yidx = fmt.indexOf('Y');
editOrder(order, didx, DAY, 'D');
editOrder(order, midx, MONTH, 'M');
editOrder(order, yidx, YEAR, 'Y');
if (didx == midx || midx == yidx || didx == yidx)
{
genInvalidDateOrder();
return ORDER_MDY;
}
return order;
}
/**
* Returns the beginning year of the 100 year period where 2 digit dates
* implicitly correspond to 4 year dates. This value is context-specific
* to the current user's session.
* <p>
* The value returned may have been found via a search algorithm that
* is account (user or process) specific or group specific
* within the current server or a global default for all servers.
* <p>
* The implementation iteratively looks up the directory node under:
* /server/<serverID>/runtime/<account_or_group>/windowingYear
* <p>
* If no user/process or group nodes are present, then this is checked:
* /server/<serverID>/runtime/default/windowingYear
* <p>
* If no /server/<serverID>/runtime node exists, this is checked
* (it is the global default area for all servers):
* /server/default/runtime/<account_or_group>/windowingYear
* <p>
* Finally, if no user/process or group nodes are present in the global
* default area, then this is checked:
* /server/default/runtime/default/windowingYear
* <p>
* If no value is found via this lookup, then the Progress 4GL default
* of 1950 will be returned.
*
* @return The start of the 100 year range for Y2K windowing.
*/
private static int initWindowingYear()
{
Directory dir = DirectoryManager.getInstance();
return dir.getInt(Directory.ID_RELATIVE, "windowingYear", 1950);
}
/**
* Returns the configured value in the directory for the default offset in
* minutes from UTC (SESSION:TIMEZONE). This value is context-specific to
* the current user's session.
* <p>
* The value returned will have been found via a search algorithm that
* is account (user or process) specific or group specific
* within the current server or a global default for all servers.
* <p>
* The implementation iteratively looks up the directory node under:
* /server/<serverID>/runtime/<account_or_group>/timezoneOffset
* <p>
* If no user/process or group nodes are present, then this is checked:
* /server/<serverID>/runtime/default/timezoneOffset
* <p>
* If no /server/<serverID>/runtime node exists, this is checked
* (it is the global default area for all servers):
* /server/default/runtime/<account_or_group>/timezoneOffset
* <p>
* Finally, if no user/process or group nodes are present in the global
* default area, then this is checked:
* /server/default/runtime/default/timezoneOffset
*
* @return The offset in minutes from UTC or <code>unknown</code> if not
* set in the directory.
*/
private static integer initTimezoneOffset()
{
Directory dir = DirectoryManager.getInstance();
int tz = dir.getInt(Directory.ID_RELATIVE, "timezoneOffset", INVALID_TZ);
return (tz < MIN_VALID_TZ || tz > MAX_VALID_TZ) ? new integer() : new integer(tz);
}
/**
* Returns the configured value in the directory for the display timezone
* (<code>SESSION:DISPLAY-TIMEZONE</code>). This value is context-specific to
* the current user's session and it's used to display datetime-tz values whose format
* does not include the timezone specifiers.
* <p>
* The value returned will have been found via a search algorithm that is account
* (user or process) specific or group specific within the current server or a global
* default for all servers.
* <p>
* The implementation iteratively looks up the directory node under:
* /server/<serverID>/runtime/<account_or_group>/displayTimezone
* <p>
* If no user/process or group nodes are present, then this is checked:
* /server/<serverID>/runtime/default/displayTimezone
* <p>
* If no /server/<serverID>/runtime node exists, this is checked (it is the global
* default area for all servers):
* /server/default/runtime/<account_or_group>/displayTimezone
* <p>
* Finally, if no user/process or group nodes are present in the global default area, then
* this is checked:
* /server/default/runtime/default/displayTimezone
*
* @return The display timezone or <code>unknown</code> if not set in the directory.
*/
private static integer initDisplayTimezone()
{
Directory dir = DirectoryManager.getInstance();
int tz = dir.getInt(Directory.ID_RELATIVE, "displayTimezone", INVALID_TZ);
return tz == INVALID_TZ ? new integer() : new integer((short)tz);
}
/**
* Returns the system from which time values will be obtained. This value
* is context-specific to the current user's session. In actuality, the
* application server will always be the source of time values.
* <p>
* The value returned may have been found via a search algorithm that
* is account (user or process) specific or group specific
* within the current server or a global default for all servers.
* <p>
* The implementation iteratively looks up the directory node under:
* /server/<serverID>/runtime/<account_or_group>/timeSource
* <p>
* If no user/process or group nodes are present, then this is checked:
* /server/<serverID>/runtime/default/timeSource
* <p>
* If no /server/<serverID>/runtime node exists, this is checked
* (it is the global default area for all servers):
* /server/default/runtime/<account_or_group>/timeSource
* <p>
* Finally, if no user/process or group nodes are present in the global
* default area, then this is checked:
* /server/default/runtime/default/timeSource
* <p>
* If no value is found via this lookup, then the Progress 4GL default
* of "local" will be returned.
*
* @return The system from which time values are obtained.
*/
private static String initTimeSource()
{
Directory dir = DirectoryManager.getInstance();
return dir.getString(Directory.ID_RELATIVE, "timeSource", "LOCAL");
}
/**
* Helper to maintain the component order array. This method exists to
* centralize the error checking and handling logic for this simple
* operation.
*
* @param order
* The array which determines the order of date components.
* @param idx
* The index position to set.
* @param type
* The component to set at the index.
* @param comp
* The descriptive character specifying the component.
*/
private static void editOrder(byte[] order, int idx, byte type, char comp)
{
if (idx < 0 || idx >= order.length)
{
String spec = "Missing '%c' component in date format.";
String msg = String.format(spec, comp);
throw new RuntimeException(msg);
}
else
{
order[idx] = type;
}
}
/**
* Generate the error for the condition where the specification of the
* date component order is not valid.
*/
private static void genInvalidDateOrder()
{
String err = "The DATE argument must be one of: mdy, myd, dmy, dym, ymd, or ydm";
ErrorManager.recordOrThrowError(299, err);
}
/**
* Provides a thread local cache for a default calendar.
* <p>
* Allows the caching and reuse of a calendar since construction of
* these are much more expensive than just reusing the same instance
* and resetting its state.
*/
private static class NullCalendar
extends ThreadLocal<GregorianCalendar>
{
/**
* Instantiate the calendar on first access for the current thread.
*
* @return The calendar.
*/
@Override
protected GregorianCalendar initialValue()
{
// TODO this looks right but may be wrong:
// return new GregorianCalendar(getDefaultTimeZone());
return new GregorianCalendar();
}
}
/**
* Provides a thread local cache for a GMT calendar.
* <p>
* Allows the caching and reuse of a calendar since construction of
* these are much more expensive than just reusing the same instance
* and resetting its state.
*/
private static class GMTCalendar
extends ThreadLocal<GregorianCalendar>
{
/**
* Instantiate the calendar on first access for the current thread.
*
* @return The calendar.
*/
@Override
protected GregorianCalendar initialValue()
{
return new GregorianCalendar(TimeZone.getTimeZone("GMT"));
}
}
/**
* Provides a thread local cache for a timezone-specific calendar.
* <p>
* Allows the caching and reuse of a calendar since construction of
* these are much more expensive than just reusing the same instance
* and resetting its state.
*/
private static class ZoneCalendar
extends ThreadLocal<GregorianCalendar>
{
/** The timezone for which a calendar must be returned. */
private TimeZone zone = null;
/**
* Construct a new instance of a timezone-specific calendar cache.
*
* @param zone
* The timezone.
*/
public ZoneCalendar(TimeZone zone)
{
this.zone = zone;
}
/**
* Instantiate the calendar on first access for the current thread.
*
* @return The calendar.
*/
@Override
protected GregorianCalendar initialValue()
{
return new GregorianCalendar(zone);
}
}
/**
* Stores global data relating to the state of the current context.
*/
private static class WorkArea
{
/** Track if initialization has occurred yet. */
private boolean initialized = false;
/** The start of the 100 year range for Y2K windowing. */
private int windowingYear = -1;
/** Order of date components for conversion to/from strings. */
private byte[] dateFormat = null;
/** Default offset in minutes from UTC (SESSION:TIMEZONE attribute). */
private integer timezoneOffset = null;
/** Timezone to be used when displaying timedate-tz values (DISPLAY-TIMEZONE attribute). */
private integer displayTimezone = null;
/** System from which to get the time (SESSION:TIME-SOURCE attribute). */
private String timeSource = "LOCAL";
/**
* Stores the base time for calculating elapsed milliseconds for this user's session.
*/
private long baseMillis;
/**
* During temp-table-prepare() method, the date* literals will use the session date-format instead of
* DEFAULT_DATE_FORMAT and DEFAULT_DATETIME_FORMAT.
*/
private String dynamicFormat = null;
/**
* Initialize this instance.
*/
private WorkArea()
{
// this is always set at the session startup
baseMillis = System.currentTimeMillis();
}
}
/**
* Simple container that stores and returns a context-local instance of
* the global work area.
*/
private static class ContextContainer
extends ContextLocal<WorkArea>
{
/**
* Obtains the context-local instance of the contained global work
* area.
*
* @return The work area associated with this context.
*/
public synchronized WorkArea obtain()
{
WorkArea wa = this.get();
// normally, we would place this init logic in the initialValue()
// method OR inline in the WorkArea constructor/init (which is also
// done during initialValue(); however this is can cause a deadlock;
// the get() MUST be separated from the initialization routines since
// on the client these routines call the server and can deadlock with
// other classes which are needed to complete the communications path;
// once the get() returns, the access to the context-local maps is
// complete and it is safe to trigger processing that may call the
// server
if (!wa.initialized)
{
wa.windowingYear = date.initWindowingYear();
wa.dateFormat = date.initFormat(null);
wa.timezoneOffset = date.initTimezoneOffset();
wa.displayTimezone = date.initDisplayTimezone();
wa.timeSource = date.initTimeSource();
wa.initialized = true;
}
return wa;
}
/**
* Initializes the work area, the first time it is requested within a
* new context.
*
* @return The newly instantiated work area.
*/
@Override
protected synchronized WorkArea initialValue()
{
return new WorkArea();
}
}
/**
* Convenient class that wraps the low-level {@link #julian} parameter
* to avoid pinning the whole context-local of the date.
*/
public static class ContextIndependentDate
{
/** Stores the date for this instance in Progress 4GL format. */
long julian;
/** Flag to indicate this value is unknown */
boolean unknown;
/**
* Basic c'tor.
*
* @param unknown
* {@code true} if this value represents an unknown value.
* @param julian
* The date for this instance in Progress 4GL format.
*/
public ContextIndependentDate(boolean unknown, long julian)
{
this.unknown = unknown;
this.julian = julian;
}
/**
* 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 ContextIndependentDate) ||
(other instanceof datetime.ContextIndependentDateTime))
{
// a date can be compared ONLY WITH other instances of date
return false;
}
ContextIndependentDate that = (ContextIndependentDate) 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;
}
/**
* Implementation of hash code.
*/
@Override
public int hashCode()
{
int result = 17;
if (isUnknown())
{
return result;
}
result = 37 * result + (int) (julian ^ (julian >>> 32));
return result;
}
/**
* Check if this is unknown.
*
* @return {@code true} if this is unknown.
*/
boolean isUnknown()
{
return unknown;
}
}
/**
* Simple testcase wrapper to check Julian day number calculations.
*/
private static class TestCase
{
/** The date instance to test. */
private date date;
/** The resulting formatted output string to match to the testcase. */
private String result;
/** The month to compare against. */
private int mm;
/** The day of month to compare against. */
private int dd;
/** The weekday to compare against. */
private int ww;
/** The year to compare against. */
private int yy;
/**
* The format in which to generate output, <code>null</code> for
* 'export' format.
*/
private String fmt;
/**
* Construct an instance of a test case with all data needed to test
* the passed instance of <code>date</code>.
*
* @param date
* The instance to test.
* @param result
* The result from the string formatting functions.
* @param fmt
* The Progress compatible format string to use or
* <code>null</code> for export format.
* @param mm
* The month that the instance should report.
* @param dd
* The day of month that the instance should report.
* @param ww
* The weekday that the instance should report.
* @param yy
* The year that the instance should report.
*/
private TestCase(date date, String result, String fmt, int mm, int dd, int ww, int yy)
{
this.date = date;
this.result = result;
this.fmt = fmt;
this.mm = mm;
this.dd = dd;
this.ww = ww;
this.yy = yy;
}
/**
* Run the test.
*
* @return <code>true</code> if the created date's exported string
* matches the expected result.
*/
private boolean test()
{
String compare = null;
if (fmt == null)
{
compare = date.toStringExport();
}
else
{
compare = date.toString(fmt);
}
if ( !( result.equals(compare) &&
date.getMonth() == mm &&
date.getDay() == dd &&
date.getWeekday() == ww &&
date.getYear() == yy ))
{
System.out.printf("julian %d res '%s' comp '%s' getMonth %d mm %d getDay %d dd %d getWeekDay %d ww %d getYear %d yy %d\n",
(int) date.julian, result, compare, date.getMonth(), mm, date.getDay(), dd, date.getWeekday(), ww, date.getYear(), yy);
}
return ( result.equals(compare) &&
date.getMonth() == mm &&
date.getDay() == dd &&
date.getWeekday() == ww &&
date.getYear() == yy );
}
/**
* Renders this instance as a string.
*
* @return The text representation of this test.
*/
@Override
public String toString()
{
return "day # = " + date.julian +
"; string = " + date.toString(fmt) +
"; result = " + result +
"; month = " + date.getMonth() +
"; day = " + date.getDay() +
"; weekday = " + date.getWeekday() +
"; year = " + date.getYear() +
";";
}
}
/**
* Command line interface which provides a test harness to regression
* test this class. No parameters are necessary.
* <p>
* If parameters are passed, then an alternate mode is used which simply
* displays the output of {@link #toString(String)} and
* {@link #toStringExport}.
* @param args
* command line args.
*/
public static void main(String[] args)
{
if (args.length > 1)
{
try
{
date d = new date(args[0]);
System.out.println("Formatted = '" + d.toString(args[1]) +
"', Export = " + d.toStringExport());
}
catch (Exception exc)
{
LOG.severe("", exc);
}
System.exit(0);
}
// if still true after all tests, everything passed
boolean flag = true;
// need a calendar with the current time
GregorianCalendar gc = new GregorianCalendar();
// setup comparison values for tests dependent upon the current date
int mm = gc.get(Calendar.MONTH) + 1;
int dd = gc.get(Calendar.DAY_OF_MONTH);
int ww = gc.get(Calendar.DAY_OF_WEEK);
int yy = gc.get(Calendar.YEAR);
NumberFormat nf = NumberFormat.getInstance();
nf.setGroupingUsed(false);
nf.setMinimumIntegerDigits(2);
String todayMDY = nf.format(mm) + "/" +
nf.format(dd) + "/" +
Integer.toString(yy);
String todayYMD = Integer.toString(yy) + "/" +
nf.format(mm) + "/" +
nf.format(dd);
// setup comparison values for tests dependent upon the current year
gc.set(Calendar.MONTH, 0); // January == 0
gc.set(Calendar.DAY_OF_MONTH, 1);
int year = gc.get(Calendar.YEAR);
int wday = gc.get(Calendar.DAY_OF_WEEK);
String yr4 = Integer.toString(year);
String yr2 = yr4.substring(yr4.length() - 2);
String dynyearMDY = "01/01/" + yr2;
String dynyearYMD = yr2 + "/01/01";
// force MDY mode
work.obtain().dateFormat = ORDER_MDY;
// define our MDY tests
TestCase[] mdyTests =
{
// test of default constructor and the default date constructor
new TestCase(date.today(), todayMDY, "99/99/9999", mm, dd, ww, yy),
new TestCase(new date(date.today()), todayMDY, "99/99/9999", mm, dd, ww, yy),
// test the date constructor with specific dates
new TestCase(new date(createDate(-4716, 4, 5)), "04/05/-4716", null, 4, 5, 2, -4716),
new TestCase(new date(createDate(-4714, 12, 30)), "12/30/-4714", null, 12, 30, 7, -4714),
new TestCase(new date(createDate(-4714, 12, 31)), "12/31/-4714", null, 12, 31, 1, -4714),
new TestCase(new date(createDate(-4713, 1, 1)), "01/01/-4713", null, 1, 1, 2, -4713),
new TestCase(new date(createDate( -100, 12, 31)), "12/31/-100", null, 12, 31, 1, -100),
new TestCase(new date(createDate( -99, 1, 1)), "01/01/-099", null, 1, 1, 2, -99),
new TestCase(new date(createDate( -1, 1, 1)), "01/01/-001", null, 1, 1, 5, -1),
new TestCase(new date(createDate( -1, 12, 31)), "12/31/-001", null, 12, 31, 6, -1),
new TestCase(new date(createDate( 1, 1, 1)), "01/01/001", null, 1, 1, 7, 1),
new TestCase(new date(createDate( 1582, 10, 4)), "10/04/1582", null, 10, 4, 5, 1582),
new TestCase(new date(createDate( 1582, 10, 5)), "10/15/1582", null, 10, 15, 6, 1582),
new TestCase(new date(createDate( 1582, 10, 15)), "10/15/1582", null, 10, 15, 6, 1582),
new TestCase(new date(createDate( 1582, 10, 14)), "10/24/1582", null, 10, 24, 1, 1582),
new TestCase(new date(createDate( 1918, 3, 30)), "03/30/1918", null, 3, 30, 7, 1918),
new TestCase(new date(createDate( 1918, 3, 31)), "03/31/1918", null, 3, 31, 1, 1918),
new TestCase(new date(createDate( 1918, 4, 1)), "04/01/1918", null, 4, 1, 2, 1918),
new TestCase(new date(createDate( 1918, 10, 26)), "10/26/1918", null, 10, 26, 7, 1918),
new TestCase(new date(createDate( 1918, 10, 27)), "10/27/1918", null, 10, 27, 1, 1918),
new TestCase(new date(createDate( 1918, 10, 28)), "10/28/1918", null, 10, 28, 2, 1918),
new TestCase(new date(createDate( 1949, 12, 31)), "12/31/1949", null, 12, 31, 7, 1949),
new TestCase(new date(createDate( 1950, 1, 1)), "01/01/50", null, 1, 1, 1, 1950),
new TestCase(new date(createDate( 1970, 1, 1)), "01/01/70", null, 1, 1, 5, 1970),
new TestCase(new date(createDate( 1999, 12, 31)), "12/31/99", null, 12, 31, 6, 1999),
new TestCase(new date(createDate( 2000, 1, 1)), "01/01/00", null, 1, 1, 7, 2000),
new TestCase(new date(createDate( 2005, 1, 1)), "01/01/05", null, 1, 1, 7, 2005),
new TestCase(new date(createDate( 2005, 4, 13)), "04/13/05", null, 4, 13, 4, 2005),
new TestCase(new date(createDate( 2049, 12, 31)), "12/31/49", null, 12, 31, 6, 2049),
new TestCase(new date(createDate( 2050, 1, 1)), "01/01/2050", null, 1, 1, 7, 2050),
// tests based on hard-coded day numbers
/*30*/ new TestCase(new date( -1000), "04/05/-4716", null, 4, 5, 2, -4716),
new TestCase(new date( -1), "12/30/-4714", null, 12, 30, 7, -4714),
new TestCase(new date( 0), "12/31/-4714", null, 12, 31, 1, -4714),
new TestCase(new date( 1), "01/01/-4713", null, 1, 1, 2, -4713),
new TestCase(new date(1685264), "12/31/-100", null, 12, 31, 1, -100),
new TestCase(new date(1685265), "01/01/-099", null, 1, 1, 2, -99),
new TestCase(new date(1721424), "12/31/-001", null, 12, 31, 6, -1),
new TestCase(new date(1721425), "01/01/001", null, 1, 1, 7, 1),
new TestCase(new date(2299161), "10/04/1582", null, 10, 4, 5, 1582),
new TestCase(new date(2299162), "10/15/1582", null, 10, 15, 6, 1582),
new TestCase(new date(2421684), "03/30/1918", null, 3, 30, 7, 1918),
new TestCase(new date(2421685), "03/31/1918", null, 3, 31, 1, 1918),
new TestCase(new date(2421686), "04/01/1918", null, 4, 1, 2, 1918),
new TestCase(new date(2421894), "10/26/1918", null, 10, 26, 7, 1918),
new TestCase(new date(2421895), "10/27/1918", null, 10, 27, 1, 1918),
new TestCase(new date(2421896), "10/28/1918", null, 10, 28, 2, 1918),
new TestCase(new date(2433283), "12/31/1949", null, 12, 31, 7, 1949),
new TestCase(new date(2433284), "01/01/50", null, 1, 1, 1, 1950),
new TestCase(new date(2440589), "01/01/70", null, 1, 1, 5, 1970),
new TestCase(new date(2451545), "12/31/99", null, 12, 31, 6, 1999),
new TestCase(new date(2451546), "01/01/00", null, 1, 1, 7, 2000),
new TestCase(new date(2453475), "04/13/05", null, 4, 13, 4, 2005),
new TestCase(new date(2469808), "12/31/49", null, 12, 31, 6, 2049),
new TestCase(new date(2469809), "01/01/2050", null, 1, 1, 7, 2050),
// tests based on string parsing and dynamic year assignments
new TestCase(new date("01/01" ), dynyearMDY, "99/99/99", 1, 1, wday, year),
new TestCase(new date("1.1" ), dynyearMDY, "99/99/99", 1, 1, wday, year),
new TestCase(new date("0101" ), dynyearMDY, "99/99/99", 1, 1, wday, year),
new TestCase(new date(" 01/01. "), dynyearMDY, "99/99/99", 1, 1, wday, year),
// tests based on string parsing
new TestCase(new date("01-01.05" ), "01/01/05", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("01-1/5" ), "01/01/05", "99/99/99", 1, 1, 7, 2005),
/*60*/ new TestCase(new date("1/1-2005" ), "01/01/05", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("010105" ), "01/01/05", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("01012005" ), "01/01/05", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date(" 01/01/2005trash"), "01/01/05", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("01/01/05 trash"), "01/01/05", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("01.01.05 ---"), "01/01/05", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("01-01--01"), "01/01/-1", "99/99/99", 1, 1, 5, -1),
// hard coded (yy, mm, dd) combinations
new TestCase(new date(1, 1, 2005), "01/01/2005", "99/99/9999", 1, 1, 7, 2005),
new TestCase(new date(1, 1, 5), "01-01.0005", "99-99.9999", 1, 1, 5, 5),
new TestCase(new date(1, 1, 5), "01.01/0005", "99.99/9999", 1, 1, 5, 5),
new TestCase(new date(1, 1, 5), "?????", "9-9-9", 1, 1, 5, 5), // TODO: fails in FWD
new TestCase(new date(1, 1, 5), "00001.00001.0005", "99999.9999.99999", 1, 1, 5, 5), // TODO: fails in FWD
new TestCase(new date(10, 5, 1582), "10/15/1582", "99/99/9999", 10, 15, 6, 1582),
new TestCase(new date(10, 14, 1582), "10/24/1582", "99/99/9999", 10, 24, 1, 1582),
new TestCase(new date(9, 17, -4624), "09/17/-4624", null, 9, 17, 2, -4624),
};
// run the MDY tests
for (int i = 0; i < mdyTests.length; i++)
{
boolean result = false;
try
{
result = mdyTests[i].test();
}
catch (Exception exc)
{
LOG.severe("", exc);
}
if (!result)
{
flag = false;
String msg = "MDY Test " + i + " failed (" + mdyTests[i].toString() + ").";
System.out.println(msg);
}
}
// force YMD mode
work.obtain().dateFormat = ORDER_YMD;
// define our YMD tests
TestCase[] ymdTests =
{
// test of default constructor and the default date constructor
new TestCase(date.today(), todayYMD, "9999/99/99", mm, dd, ww, yy),
new TestCase(new date(date.today()), todayYMD, "9999/99/99", mm, dd, ww, yy),
// test the date constructor with specific dates
new TestCase(new date(createDate(-4716, 4, 5)), "-4716/04/05", null, 4, 5, 2, -4716),
new TestCase(new date(createDate(-4714, 12, 30)), "-4714/12/30", null, 12, 30, 7, -4714),
new TestCase(new date(createDate(-4714, 12, 31)), "-4714/12/31", null, 12, 31, 1, -4714),
new TestCase(new date(createDate(-4713, 1, 1)), "-4713/01/01", null, 1, 1, 2, -4713),
new TestCase(new date(createDate( -100, 12, 31)), "-100/12/31", null, 12, 31, 1, -100),
new TestCase(new date(createDate( -99, 1, 1)), "-099/01/01", null, 1, 1, 2, -99),
new TestCase(new date(createDate( -1, 1, 1)), "-001/01/01", null, 1, 1, 5, -1),
new TestCase(new date(createDate( -1, 12, 31)), "-001/12/31", null, 12, 31, 6, -1),
new TestCase(new date(createDate( 1, 1, 1)), "001/01/01", null, 1, 1, 7, 1),
new TestCase(new date(createDate( 1582, 10, 4)), "1582/10/04", null, 10, 4, 5, 1582),
new TestCase(new date(createDate( 1582, 10, 5)), "1582/10/15", null, 10, 15, 6, 1582),
new TestCase(new date(createDate( 1582, 10, 15)), "1582/10/15", null, 10, 15, 6, 1582),
new TestCase(new date(createDate( 1582, 10, 14)), "1582/10/24", null, 10, 24, 1, 1582),
new TestCase(new date(createDate( 1918, 3, 30)), "1918/03/30", null, 3, 30, 7, 1918),
new TestCase(new date(createDate( 1918, 3, 31)), "1918/03/31", null, 3, 31, 1, 1918),
new TestCase(new date(createDate( 1918, 4, 1)), "1918/04/01", null, 4, 1, 2, 1918),
new TestCase(new date(createDate( 1918, 10, 26)), "1918/10/26", null, 10, 26, 7, 1918),
new TestCase(new date(createDate( 1918, 10, 27)), "1918/10/27", null, 10, 27, 1, 1918),
new TestCase(new date(createDate( 1918, 10, 28)), "1918/10/28", null, 10, 28, 2, 1918),
new TestCase(new date(createDate( 1949, 12, 31)), "1949/12/31", null, 12, 31, 7, 1949),
new TestCase(new date(createDate( 1950, 1, 1)), "50/01/01", null, 1, 1, 1, 1950),
new TestCase(new date(createDate( 1970, 1, 1)), "70/01/01", null, 1, 1, 5, 1970),
new TestCase(new date(createDate( 1999, 12, 31)), "99/12/31", null, 12, 31, 6, 1999),
new TestCase(new date(createDate( 2000, 1, 1)), "00/01/01", null, 1, 1, 7, 2000),
new TestCase(new date(createDate( 2005, 1, 1)), "05/01/01", null, 1, 1, 7, 2005),
new TestCase(new date(createDate( 2005, 4, 13)), "05/04/13", null, 4, 13, 4, 2005),
new TestCase(new date(createDate( 2049, 12, 31)), "49/12/31", null, 12, 31, 6, 2049),
new TestCase(new date(createDate( 2050, 1, 1)), "2050/01/01", null, 1, 1, 7, 2050),
// tests based on hard-coded day numbers
new TestCase(new date( -1000), "-4716/04/05", null, 4, 5, 2, -4716),
new TestCase(new date( -1), "-4714/12/30", null, 12, 30, 7, -4714),
new TestCase(new date( 0), "-4714/12/31", null, 12, 31, 1, -4714),
new TestCase(new date( 1), "-4713/01/01", null, 1, 1, 2, -4713),
new TestCase(new date(1685264), "-100/12/31", null, 12, 31, 1, -100),
new TestCase(new date(1685265), "-099/01/01", null, 1, 1, 2, -99),
new TestCase(new date(1721424), "-001/12/31", null, 12, 31, 6, -1),
new TestCase(new date(1721425), "001/01/01", null, 1, 1, 7, 1),
new TestCase(new date(2299161), "1582/10/04", null, 10, 4, 5, 1582),
new TestCase(new date(2299162), "1582/10/15", null, 10, 15, 6, 1582),
new TestCase(new date(2421684), "1918/03/30", null, 3, 30, 7, 1918),
new TestCase(new date(2421685), "1918/03/31", null, 3, 31, 1, 1918),
new TestCase(new date(2421686), "1918/04/01", null, 4, 1, 2, 1918),
new TestCase(new date(2421894), "1918/10/26", null, 10, 26, 7, 1918),
new TestCase(new date(2421895), "1918/10/27", null, 10, 27, 1, 1918),
new TestCase(new date(2421896), "1918/10/28", null, 10, 28, 2, 1918),
new TestCase(new date(2433283), "1949/12/31", null, 12, 31, 7, 1949),
new TestCase(new date(2433284), "50/01/01", null, 1, 1, 1, 1950),
new TestCase(new date(2440589), "70/01/01", null, 1, 1, 5, 1970),
new TestCase(new date(2451545), "99/12/31", null, 12, 31, 6, 1999),
new TestCase(new date(2451546), "00/01/01", null, 1, 1, 7, 2000),
new TestCase(new date(2453475), "05/04/13", null, 4, 13, 4, 2005),
new TestCase(new date(2469808), "49/12/31", null, 12, 31, 6, 2049),
new TestCase(new date(2469809), "2050/01/01", null, 1, 1, 7, 2050),
// tests based on string parsing and dynamic year assignments
new TestCase(new date("01/01" ), dynyearYMD, "99/99/99", 1, 1, wday, year),
new TestCase(new date("1.1" ), dynyearYMD, "99/99/99", 1, 1, wday, year),
new TestCase(new date("0101" ), dynyearYMD, "99/99/99", 1, 1, wday, year),
new TestCase(new date(" 01/01. "), dynyearYMD, "99/99/99", 1, 1, wday, year),
// tests based on string parsing
new TestCase(new date("05-01.01" ), "05/01/01", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("5-01/1" ), "05/01/01", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("2005/1-1" ), "05/01/01", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("050101" ), "05/01/01", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("20050101" ), "05/01/01", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date(" 2005/01/01trash"), "05/01/01", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("05/01/01 trash"), "05/01/01", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("05.01.01 ---"), "05/01/01", "99/99/99", 1, 1, 7, 2005),
new TestCase(new date("-01-01-01"), "-1/01/01", "99/99/99", 1, 1, 5, -1),
// hard coded (yy, mm, dd) combinations
new TestCase(new date(1, 1, 2005), "2005/01/01", "9999/99/99", 1, 1, 7, 2005),
new TestCase(new date(1, 1, 5), "0005-01.01", "9999-99.99", 1, 1, 5, 5),
new TestCase(new date(1, 1, 5), "0005.01/01", "9999.99/99", 1, 1, 5, 5),
new TestCase(new date(1, 1, 5), "?????", "9-9-9", 1, 1, 5, 5), // TODO: fails in FWD
new TestCase(new date(1, 1, 5), "0005.00001.00001", "99999.9999.99999", 1, 1, 5, 5), // TODO: fails in FWD
new TestCase(new date(10, 5, 1582), "1582/10/15", "9999/99/99", 10, 15, 6, 1582),
new TestCase(new date(10, 14, 1582), "1582/10/24", "9999/99/99", 10, 24, 1, 1582),
new TestCase(new date(9, 17, -4624), "-4624/09/17", null, 9, 17, 2, -4624),
};
// run the YMD tests
for (int i = 0; i < ymdTests.length; i++)
{
boolean result = false;
try
{
result = ymdTests[i].test();
}
catch (Exception exc)
{
LOG.severe("", exc);
}
if (!result)
{
flag = false;
String msg = "YMD Test " + i + " failed (" + ymdTests[i].toString() + ").";
System.out.println(msg);
}
}
if (flag)
{
System.out.println("All tests passed successfully.");
}
}
}