DateOps.java
/*
** Module : DateOps.java
** Abstract : Progress 4GL compatible date operations.
**
** Copyright (c) 2013-2022, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20130121 Created initial version.
** 002 OM 20130531 Added missing implementation.
** 003 CA 20191211 Do not use datetimetz when adding the millis, but a duplicate of the existing instance.
** 004 IAS 20211015 Fixed potential ClassCastException in the addInterval method
** 005 CA 20220622 Fixed INTERVAL and ADD-INTERVAL - they weren't addressing the timezone in a datetimetz
** properly.
** OM 20220622 Further improve interval operations.
*/
/*
** 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.util.*;
/**
* A class that provides Progress 4GL compatible date functions. This class
* provides all functionality as statics and has no instance data.
* <p>
* The following is the mapping of Progress language features to the
* corresponding feature in this class:
* <p>
* <pre>
* add-interval function {@link #addInterval}
* interval function {@link #interval}
* </pre>
* <p>
* The above methods all return an instance of wrapper classes which allow
* the Progress unknown value to be properly handled. Overloaded versions of
* the methods exist which handle different combinations of primative type
* parameters, allowing the resulting code to be implemented without
* unnecessary construction of the wrapper types for all parameters.
*/
public final class DateOps
{
/**
* Adds or subtracts a time amount to/from an instance of a date class
* and returns the new date instance. This can be used with {@link date}
* (or any subclass) and the same type will be returned.
*
* @param dat
* The date to add to or subtract from.
* @param amount
* If positive, the amount to add. If negative, the amount to subtract.
* @param unit
* One of the following time units: 'years', 'months', 'weeks', 'days', 'hours',
* 'minutes', 'seconds' or 'milliseconds'. Any case is accepted and it may be
* specified as singular.
*
* @return The computed value or <code>unknown</code> if any input is unknown.
*/
@SuppressWarnings("unchecked") // duplicate(), instantiateUnknown() and instantiateDefault() are safe
public static <T extends date> T addInterval(T dat, long amount, String unit)
{
int calField = getGregorianField(unit);
if (calField < 0)
{
// parameter validation
ErrorManager.recordOrThrowError(11262,
"Third parameter for the INTERVAL/ADD-INTERVAL function must be unit of measure");
return (T) dat.instantiateUnknown();
}
// validation, trivial case: no computation needed
if (amount == 0)
{
return (T) dat.duplicate();
}
// validation, second trivial case: no computation needed
if (dat.isUnknown())
{
return (T) dat.instantiateUnknown();
}
// simple case: add a fixed amount of time that will be converted to milliseconds
long delta = getIntervalInMillis(amount, unit);
if (delta != 0)
{
datetime inter = dat instanceof datetime ? (datetime) dat.duplicate() : new datetime(dat);
inter = datetime.plusMillis(inter, delta);
T ret = (T) dat.duplicate();
ret.assign(inter);
return ret;
}
// second simple case: add a variable amount of days (years/months)
Calendar jcal = GregorianCalendar.getInstance();
jcal.setTime(dat.dateValue());
jcal.add(calField, (int) amount);
T ret = (T) dat.duplicate();
date inter2 = new date(jcal.getTime());
ret.setDayNumber(inter2.intValue());
return ret;
// no other cases
}
/**
* Adds or subtracts a time amount to/from an instance of a date class
* and returns the new date instance. This can be used with {@link date}
* (or any subclass) and the same type will be returned.
*
* @param dat
* The date to add to or subtract from.
* @param amount
* If positive, the amount to add. If negative, the amount to
* subtract.
* @param unit
* One of the following time units: 'years', 'months', 'weeks',
* 'days', 'hours', 'minutes', 'seconds' or 'milliseconds'. Any
* case is accepted and it may be specified as singular.
*
* @return The computed value or <code>unknown</code> if any input is
* unknown.
*/
@SuppressWarnings("unchecked")
public static <T extends date> T addInterval(T dat, NumberType amount, String unit)
{
if (dat.isUnknown() || amount.isUnknown())
{
return (T) dat.instantiateUnknown();
}
return addInterval(dat, amount.longValue(), unit);
}
/**
* Adds or subtracts a time amount to/from an instance of a date class
* and returns the new date instance. This can be used with {@link date}
* (or any subclass) and the same type will be returned.
*
* @param dat
* The date to add to or subtract from.
* @param amount
* If positive, the amount to add. If negative, the amount to
* subtract.
* @param unit
* One of the following time units: 'years', 'months', 'weeks',
* 'days', 'hours', 'minutes', 'seconds' or 'milliseconds'. Any
* case is accepted and it may be specified as singular.
*
* @return The computed value or <code>unknown</code> if any input is
* unknown.
*/
@SuppressWarnings("unchecked")
public static <T extends date> T addInterval(T dat, long amount, character unit)
{
if (dat.isUnknown() || unit.isUnknown())
{
return (T) dat.instantiateUnknown();
}
return addInterval(dat, amount, unit.toStringMessage());
}
/**
* Adds or subtracts a time amount to/from an instance of a date class
* and returns the new date instance. This can be used with {@link date}
* (or any subclass) and the same type will be returned.
*
* @param dat
* The date to add to or subtract from.
* @param amount
* If positive, the amount to add. If negative, the amount to
* subtract.
* @param unit
* One of the following time units: 'years', 'months', 'weeks',
* 'days', 'hours', 'minutes', 'seconds' or 'milliseconds'. Any
* case is accepted and it may be specified as singular.
*
* @return The computed value or <code>unknown</code> if any input is
* unknown.
*/
@SuppressWarnings("unchecked")
public static <T extends date> T addInterval(T dat, NumberType amount, character unit)
{
if (dat.isUnknown() || amount.isUnknown() || unit.isUnknown())
{
return (T) dat.instantiateUnknown();
}
return addInterval(dat, amount.longValue(), unit.toStringMessage());
}
/**
* Calculate the time interval between two date values and return it in user-specified units.
*
* @param d1
* The first date value.
* @param d2
* The second date value.
* @param unit
* One of the following time units: 'years', 'months', 'weeks',
* 'days', 'hours', 'minutes', 'seconds' or 'milliseconds'. Any
* case is accepted and it may be specified as singular.
*
* @return The computed value or <code>unknown</code> if any input is unknown.
*/
public static int64 interval(date d1, date d2, String unit)
{
int calField = getGregorianField(unit);
if (calField < 0)
{
// parameter validation
ErrorManager.recordOrThrowError(11262,
"Third parameter for the INTERVAL/ADD-INTERVAL function must be unit of measure");
return new int64();
}
if (d1.isUnknown() || d2.isUnknown())
{
return new int64();
}
// adjust both to UTC (to have a common denominator with the other date instant)
datetime d1utc = d1.utc();
datetime d2utc = d2.utc();
unit = unit.toLowerCase();
// day difference between dates:
long dd = d1utc.longValue() - d2utc.longValue();
// compute time difference between dates:
long dt = d1utc.getTime() - d2utc.getTime();
// adjust dd & dt to have the same sign
if (dd < 0)
{
if (dt > 0)
{
++dd;
dt -= date.MILLIS_PER_DAY;
}
// else they are both negative
}
else if (dd > 0)
{
if (dt < 0)
{
--dd;
dt += date.MILLIS_PER_DAY;
}
// else they are both positive
}
// else dd is 0 so only compute using dt
long multiplier = getIntervalInMillis(1, unit);
if (multiplier > 0)
{
// we know the exact unit size
return new int64(((dd * date.MILLIS_PER_DAY) + dt) / multiplier);
}
// else unit is 'year' or 'month' or.. other invalid value
boolean umYear = "years".equals(unit) || "year".equals(unit);
boolean umMonth = "month".equals(unit) || "months".equals(unit);
if (!umYear && !umMonth)
{
// unrecognized unit, returning unknown value
return new int64();
}
// try to order the dates so that they are in REVERSE chronological order
// as date/datetime and datetime-tz are not directly comparable; we do it ad-hoc:
boolean rev = false;
if (d1utc.longValue() < d2utc.longValue())
{
// different days, d1 is less, they will be switched
rev = true;
}
else if (d1utc.longValue() == d2utc.longValue())
{
// same day, check the time
if (d1utc.getTime() < d2utc.getTime())
{
// if d1 is early, the values will be switched
rev = true;
}
}
if (rev)
{
datetime tmp = d1utc;
d1utc = d2utc;
d2utc = tmp;
}
// now d1 > d2 so the difference d1 - d2 is always positive.
int y1 = d1utc.getYear();
int y2 = d2utc.getYear();
int m1 = d1utc.getMonth();
int m2 = d2utc.getMonth();
int z1 = d1utc.getDay();
int z2 = d2utc.getDay();
long t1 = d1utc.getTime();
long t2 = d2utc.getTime();
if (umYear)
{
// dy is always positive as the date were eventually reordered (rev is true in this case)
int dy = y1 - y2;
if (m1 > m2)
{
return new int64(rev ? -dy : dy);
}
else if (m1 == m2)
{
if (z1 > z2)
{
return new int64(rev ? -dy : dy);
}
else if (z1 == z2)
{
if (t1 >= t2)
{
return new int64(rev ? -dy : dy);
}
}
}
// there is not yet a full year
return new int64(rev ? 1 - dy : dy - 1);
}
else // umMonth = true
{
// dm is always positive as the date were eventually reordered (rev is true in this case)
long dm = (y1 - y2) * 12L + (m1 - m2);
if (z1 > z2)
{
return new int64(rev ? -dm : dm);
}
else if (z1 == z2)
{
if (t1 >= t2)
{
return new int64(rev ? -dm : dm);
}
}
// there is not yet a full month
return new int64(rev ? 1 - dm : dm - 1);
}
}
/**
* Calculate the time interval between two date values and return it
* in user-specified units.
*
* @param d1
* The first date value.
* @param d2
* The second date value.
* @param unit
* One of the following time units: 'years', 'months', 'weeks',
* 'days', 'hours', 'minutes', 'seconds' or 'milliseconds'. Any
* case is accepted and it may be specified as singular.
*
* @return The computed value or <code>unknown</code> if any input is
* unknown.
*/
public static int64 interval(date d1, date d2, character unit)
{
if (unit.isUnknown())
{
return new int64();
}
return interval(d1, d2, unit.toStringMessage());
}
/**
* Computes and returns an interval of a fixed size time unit.
*
* @param amount
* The amount of units.
* @param unit
* One of the following time units: 'weeks', 'days', 'hours', 'minutes', 'seconds'
* or 'milliseconds'. Any case is accepted and it may be specified as singular.
*
* @return The milliseconds in terms of the given unit.
*/
private static long getIntervalInMillis(long amount, String unit)
{
unit = unit.toLowerCase();
if ("milliseconds".equals(unit) || "millisecond".equals(unit))
{
return amount;
}
amount *= 1000;
if ("seconds".equals(unit) || "second".equals(unit))
{
return amount;
}
amount *= 60;
if ("minutes".equals(unit) || "minute".equals(unit))
{
return amount;
}
amount *= 60;
if ("hours".equals(unit) || "hour".equals(unit))
{
return amount;
}
amount *= 24;
if ("days".equals(unit) || "day".equals(unit))
{
return amount;
}
amount *= 7;
if ("weeks".equals(unit) || "week".equals(unit))
{
return amount;
}
return 0;
}
/**
* Maps Progress calendar fields to Java Calendar fields.
*
* @param pUnit
* The name of time unit from Progress.
*
* @return The corresponding Calendar field Id from java.util.Calendar.
*/
private static int getGregorianField(String pUnit)
{
// to simplify things, convert to lowercase
pUnit = pUnit.toLowerCase();
if ("milliseconds".equals(pUnit) || "millisecond".equals(pUnit))
{
return Calendar.MILLISECOND;
}
if ("seconds".equals(pUnit) || "second".equals(pUnit))
{
return Calendar.SECOND;
}
if ("minutes".equals(pUnit) || "minute".equals(pUnit))
{
return Calendar.MINUTE;
}
if ("hours".equals(pUnit) || "hour".equals(pUnit))
{
return Calendar.HOUR_OF_DAY;
}
if ("days".equals(pUnit) || "day".equals(pUnit))
{
return Calendar.DATE;
}
if ("weeks".equals(pUnit) || "week".equals(pUnit))
{
return Calendar.WEEK_OF_YEAR;
}
if ("months".equals(pUnit) || "month".equals(pUnit))
{
return Calendar.MONTH;
}
if ("years".equals(pUnit) || "year".equals(pUnit))
{
return Calendar.YEAR;
}
return -1;
}
/**
* Compares two date-related values.
* The arguments can be any date-related values. If needed they are converted to local time
* before doing actual comparison.
*
* @param d1
* The first date to compare.
* @param d2
* The second date to compare.
*
* @return 0 if operands are equals, -1 if fist is less and 1 if first is greater than second.
*/
public static int compare(date d1, date d2)
{
boolean u1 = d1.isUnknown();
boolean u2 = d2.isUnknown();
if (u1 && u2)
{
// two unknown are equals
return 0;
}
if (u1 || u2)
{
// only of is unknown, the other is not
return -1;
}
datetime dt1 = d1.utc();
datetime dt2 = d2.utc();
long d1AbsTime = dt1.longValue() * date.MILLIS_PER_DAY + dt1.getTime();
long d2AbsTime = dt2.longValue() * date.MILLIS_PER_DAY + dt2.getTime();
if (d1AbsTime < d2AbsTime)
{
return -1;
}
else if (d1AbsTime > d2AbsTime)
{
return +1;
}
// else they are equals:
return 0;
}
}