/*
This file is part of the software framework JStatCom,
a library for building applications for data based analysis.
Copyright (C) 2000-2005 Markus Kraetzig
This library is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2.1 of the
License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
USA
*/
package com.jstatcom.ts;
import javax.swing.JOptionPane;
import com.jstatcom.component.TopFrameReference;
import com.jstatcom.util.PrintfFormat;
/**
* Holds a range for possible TSDate objects defined as
* [lowerBound, upperBound] and provides checks against it. The
* class can be used by classes that allow to specify a date that needs to be
* validated.
*
* This class is immutable by design, see (Joshua Bloch, "Effective Java",
* Item 13) .
*
* @see com.jstatcom.ts.TSDate
* @author Markus Kraetzig
*/
public final class TSDateRange {
/**
* The lower bound.
*/
private final TSDate lowerBound;
/**
* The upper bound.
*/
private final TSDate upperBound;
/**
* Creates a default range starting from the default TSDate
* with 100 observations.
*/
public TSDateRange() {
this(new TSDate(), 100);
}
/**
* Creates TSDateRange from specified start date and number
* of observations.
*
* @param t1
* @param numberOfObs
* the number of observations within the range
* @throws IllegalArgumentException
* if numberOfObs <= 0
*/
public TSDateRange(TSDate t1, int numberOfObs) {
if (numberOfObs <= 0)
throw new IllegalArgumentException(
"Illegal number of observations (" + numberOfObs
+ " <= 0).");
lowerBound = t1;
upperBound = lowerBound.addPeriods(numberOfObs - 1);
}
/**
* Creates TSDateRange from specified start date and end
* date.
*
* @param t1
* the TSDate to start from (inklusive)
* @param t2
* the TSDate to end with (inklusive)
* @throws IllegalArgumentException
* if upperBound is earlier than
* lowerBound or sub periodicities do not match
*/
public TSDateRange(TSDate t1, TSDate t2) {
if (t1.subPeriodicity() != t2.subPeriodicity())
throw new IllegalArgumentException(
"Sub periodicities do not match (" + t1.subPeriodicity()
+ " != " + t2.subPeriodicity() + ").");
if (t1.compareTo(t2) < 0)
throw new IllegalArgumentException(
"Upper bound earlier than lower bound (" + t1 + " >= " + t2
+ ").");
lowerBound = t1;
upperBound = t2;
}
/**
* Gets a new TSDateRange range object with the start date
* shifted by periods.
*
* @param periods
* the number of periods to shift the start date, can be
* negative, which results in an earlier start, 0 creates a copy
* of this
* @return a new range object with the same end date and a new start date
* @throws IllegalArgumentException
* if new range would be invalid
*/
public TSDateRange addPeriodsToStart(int periods) {
return new TSDateRange(lowerBound.addPeriods(periods), upperBound);
}
/**
* Returns the biggest date range that is common to this range and another
* range. If the two ranges do not overlap with at least a single date or if
* the sub periodicities do not match, null is returned.
*
* The function is symmetric in the sense that
* x.commonRange(y).equals(y.commonRange(x)).
*
* @param otherRange
* the range to be checked
* @return common range, null if no common range exists
* @throws IllegalArgumentException
* if argument is null
*/
public TSDateRange commonRange(TSDateRange otherRange) {
if (otherRange == null)
throw new IllegalArgumentException("Range argument was null.");
TSDate otherStart = otherRange.lowerBound();
TSDate otherEnd = otherRange.upperBound();
// Sub periodicities do not match.
if (otherStart.subPeriodicity() != lowerBound.subPeriodicity())
return null;
// Check whether any periods overlap.
if (lowerBound.compareTo(otherEnd) < 0
|| upperBound.compareTo(otherStart) > 0)
return null;
// otherRange is completely within this range, makes common range.
if (encloses(otherRange) == null)
return otherRange;
TSDate newStart = lowerBound;
// Other start is later than this start.
if (lowerBound.compareTo(otherStart) > 0)
newStart = otherStart;
TSDate newEnd = upperBound;
// Other start is earlier than this end.
if (upperBound.compareTo(otherEnd) < 0)
newEnd = otherEnd;
return new TSDateRange(newStart, newEnd);
}
/**
* Creates seasonal dummy series as double[][], see
* {@link #createSeasDumTS}.
*
* @param centered
* the sum over all dummy elements at every time index is 0, if
* this is true, then all is ignored (and
* false)
* @param all
* if true then sub periodicity
* dummies are created, if false then
* sub periodicity - 1 dummies are created
* @return dummy data as double[][],null if
* sub periodicity == 1 or if not enough observations
*/
public double[][] createSeasDum(boolean centered, boolean all) {
if (centered)
all = false;
int periodicity = lowerBound.subPeriodicity();
if (periodicity == 1) {
JOptionPane.showMessageDialog(TopFrameReference.getTopFrameRef(),
"Seasonal dummies could not be created for\n" + this
+ ", because periodicity is 1.", "Info",
JOptionPane.INFORMATION_MESSAGE);
return null;
}
int nr = numOfObs();
double[][] dumObs;
if (all)
dumObs = new double[nr][periodicity];
else
dumObs = new double[nr][periodicity - 1];
double period = periodicity;
for (int i = 0; i < dumObs[0].length; i++) {
if (!centered) // Not Centered
for (int j = 0; j < dumObs.length; j++)
dumObs[j][i] = (lowerBound.subPeriodFor(j) == (i + 1)) ? 1
: 0;
else
// Centered
for (int j = 0; j < dumObs.length; j++)
dumObs[j][i] = (lowerBound.subPeriodFor(j) == (i + 1)) ? (period - 1)
/ period
: -1 / period;
}
return dumObs;
}
/**
* Creates seasonal dummy series according to the periodicity of this time
* series. The returned array contains the single dummy series as elements,
* where the 1st dummy is set to 1 always at the first possible sub period
* (for example Q1 for quarterly data) and the others respectively in
* ascending order.
*
* If the number of observations of this time series is less than the sub
* periodicity then null is returned, because the resulting
* array could contain a zero vector.
*
* @param centered
* the sum over all dummy elements at every time index is 0, if
* this is true, then all is ignored (and
* false)
* @param all
* if true then sub periodicity
* dummies are created, if false then
* sub periodicity - 1 dummies are created
* @return array with the single dummy series, null if sub
* periodicity == 1 or if not enough observations
*/
public TS[] createSeasDumTS(boolean centered, boolean all) {
double[][] dums = createSeasDum(centered, all);
if (dums == null)
return null;
TS[] dummyArray = new TS[dums[0].length];
for (int i = 0; i < dummyArray.length; i++) {
double[] dumObs = new double[dums.length];
for (int j = 0; j < dumObs.length; j++)
dumObs[j] = dums[j][i];
dummyArray[i] = new TS(dumObs, "S" + (i + 1), lowerBound);
}
return dummyArray;
}
/**
* Creates a double[] that is a shift dummy for this range,
* see {@link #createShiftDumTS}.
*
* @param shiftRange
* the range within this range to specify the shift
* @return the created shift dummy as double[]
*
* @throws IllegalArgumentException
* if shiftRange is not a valid range for this
* range
*/
public double[] createShiftDum(TSDateRange shiftRange) {
String check = encloses(shiftRange);
if (check != null)
throw new IllegalArgumentException(check);
double[] obs = new double[numOfObs()];
int[] indices = indicesForRange(shiftRange);
for (int i = indices[0]; i <= indices[1]; i++)
obs[i] = 1;
return obs;
}
/**
* Creates a double[] that is a trend shift dummy for this
* range, see {@link #createTrendShiftDumTS}.
*
* @param shiftRange
* the range within this range to specify the trend shift
* @return the created trend shift dummy as double[]
*
* @throws IllegalArgumentException
* if shiftRange is not a valid range for this
* range
*/
public double[] createTrendShiftDum(TSDateRange shiftRange) {
String check = encloses(shiftRange);
if (check != null)
throw new IllegalArgumentException(check);
double[] obs = new double[numOfObs()];
int[] indices = indicesForRange(shiftRange);
for (int i = indices[0]; i <= indices[1]; i++)
obs[i] = i + 1;
return obs;
}
/**
* Creates a time series that is a trend shift dummy for this range. A trend
* shift dummy has zeros outside the shift range and values [index + 1]
* within the shift range.
*
* @param name
* the name of the new TS
* @param shiftRange
* the range within this range to specify the trend shift
*
* @return the created trend shift dummy
*
* @throws IllegalArgumentException
* if name is null or empty, or if
* shiftRange is not a valid range for this range
*/
public TS createTrendShiftDumTS(String name, TSDateRange shiftRange) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException(
"Trend shift name cannot be empty.");
String check = encloses(shiftRange);
if (check != null)
throw new IllegalArgumentException(check);
double[] obs = createTrendShiftDum(shiftRange);
TS dummy = new TS(obs, name, lowerBound(), TSTypes.DETERMINISTIC);
return dummy;
}
/**
* Creates a time series that is a shift dummy for this range. A shift dummy
* has zeros outside the shift range and values [1] within the shift range.
*
* @param name
* the name of the new TS
* @param shiftRange
* the range within this range to specify the shift
*
* @return the created shift dummy
*
* @throws IllegalArgumentException
* if name is null or empty, or if
* shiftRange is not a valid range for this range
*/
public TS createShiftDumTS(String name, TSDateRange shiftRange) {
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Dummy name cannot be empty.");
String check = encloses(shiftRange);
if (check != null)
throw new IllegalArgumentException(check);
double[] obs = createShiftDum(shiftRange);
TS dummy = new TS(obs, name, lowerBound(), TSTypes.DETERMINISTIC);
return dummy;
}
/**
* Checks whether a given date is within the range defined by this object.
* If not, an error message is returned.
*
* @param date
* the date to check for
* @return null if within range, error string otherwise
*/
public String encloses(TSDate date) {
if (date == null)
return date + " is invalid date for " + this + ".";
if (date.subPeriodicity() != lowerBound.subPeriodicity())
return date + " is invalid date for " + this + ".";
if (lowerBound.compareTo(date) < 0 || upperBound.compareTo(date) > 0)
return date + " is not in " + toString() + ".";
return null;
}
/**
* Checks whether a given date range is included within the range defined by
* this object. If not, an error message is returned.
*
* @param otherRange
* the range to check for
* @return null if within range, error string otherwise
*/
public String encloses(TSDateRange otherRange) {
if (otherRange == null)
return otherRange + " is invalid range for " + this + ".";
TSDate otherStart = otherRange.lowerBound();
TSDate otherEnd = otherRange.upperBound();
if (otherStart.subPeriodicity() != lowerBound.subPeriodicity())
return otherRange + " is invalid range for " + this + ".";
if (lowerBound.compareTo(otherStart) < 0
|| upperBound.compareTo(otherEnd) > 0)
return otherRange + " is not in " + toString() + ".";
return null;
}
/**
* Overrides equals to provide logical comparison of two
* different TSDateRange objects.
*
* @param o
* object to compare with
* @return true if start and end dates are both equal,
* false otherwise
*/
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof TSDateRange))
return false;
TSDateRange otherRange = (TSDateRange) o;
return lowerBound.equals(otherRange.lowerBound())
&& upperBound.equals(otherRange.upperBound());
}
/**
* Gets a nicely formatted version of this date range with a description and
* the number of observations, for example
* sample range: [1960 Q3, 1982 Q4], T = 90
* with "sample range:" as description.
*
* @param description
* a desriptive string that will be put in front
* @param space
* the number of spaces between description and
* the rest, useful for alignment, should be greater than 0,
* otherwise will be set to 0
*
* @return the formatted string
*
*/
public String format(String description, int space) {
if (space < 0)
space = 0;
return new PrintfFormat("%-" + space + "s%s, T = %i")
.sprintf(new Object[] { description, this,
new Integer(this.numOfObs()) });
/*
* Format.sprintf( "%-" + space + "s%s, T = %i", new
* Parameters(description).add(this).add(this.getNumberOfObs()));
*/
}
/**
* Returns the date corresponding to an index respective to the range.
*
* @param index
* can also specify value outside the range
* @return the date for the index
*/
public TSDate dateForIndex(int index) {
return lowerBound.addPeriods(index);
}
/**
* Computes the index that a date has respective to the range specified by
* this object, starting with 0.
*
* @param date
* the TSDate to compute the index for
* @return 0 if date is equal to lower bound, difference of periods
* otherwise
* @throws IllegalArgumentException
* if subPeriodicities do not match or if date is null
*/
public int indexForDate(TSDate date) {
if (date == null
|| date.subPeriodicity() != lowerBound.subPeriodicity())
throw new IllegalArgumentException(date + " is invalid date for "
+ this + ".");
return lowerBound.compareTo(date);
}
/**
* Computes the indices that a date range has within the range specified by
* this object, starting with 0.
*
* @param otherRange
* the range to compute the bounds for
* @return array with int[0] = index of lower bound within this range,
* int[1] = index of upper bound within this range
* @throws IllegalArgumentException
* if sub periodicities do not match or if
* otherRange is not in the defined range,
* because this usually means a programming error
*/
public int[] indicesForRange(TSDateRange otherRange) {
int[] indices = new int[2];
String check = encloses(otherRange);
if (check != null)
throw new IllegalArgumentException(check);
indices[0] = indexForDate(otherRange.lowerBound());
indices[1] = indexForDate(otherRange.upperBound());
return indices;
}
/**
* Gets reference to start date of range.
*
* @return the date this range starts from
*/
public TSDate lowerBound() {
return lowerBound;
}
/**
* Gets the merged range of all time series in the given array. The merge
* range starts from the earliest start date till the latest end date of all
* series.
*
* @param tsArray
* an array of TS objects
* @return the range starting from the earliest till the latest observation
* over all time series in the array
* @throws IllegalArgumentException
* if one TS is null or if sub
* periodicities are different
*/
public static TSDateRange getMergedRange(TS[] tsArray) {
TSDateRange mergedRange = tsArray[0].range();
for (int i = 1; i < tsArray.length; i++)
mergedRange = mergedRange.mergedRange(tsArray[i].range());
return mergedRange;
}
/**
* Returns number of observations represented by this range. Every range
* object contains at least one observation.
*
* @return the number of obervations within this range
*/
public int numOfObs() {
return lowerBound.compareTo(upperBound) + 1;
}
/**
* Gets the sub periodicity of this date range. It quantifies the number of
* sub periods which constitute one main period.
*
* @return the sub periodicity
*/
public int subPeriodicity() {
return lowerBound.subPeriodicity();
}
/**
* Gets reference the last date of the range.
*
* @return the date this range ends with
*/
public TSDate upperBound() {
return upperBound;
}
/**
* Overrides hashCode(), because equals was overwritten. The
* algorithm was taken from (Joshua Bloch, "Effective Java", Item 8)
* .
*
* @return hash code
*/
public int hashCode() {
int result = 17;
result = 37 * result + lowerBound.hashCode();
result = 37 * result + upperBound.hashCode();
return result;
}
/**
* Returns the merged date range that starts at the earliest start date and
* ends at the latest enddate. If the sub periodicities do not match, null
* is returned.
*
* The function is symmetric in the sense that
* x.mergedRange(y).equals(y.mergedRange(x)).
*
* @param otherRange
* the range to merge with
* @return the merged range
* @throws IllegalArgumentException
* if argument is null or if sub periodicities do not match
*/
public TSDateRange mergedRange(TSDateRange otherRange) {
if (otherRange == null)
throw new IllegalArgumentException("Range argument was null.");
TSDate otherStart = otherRange.lowerBound();
TSDate otherEnd = otherRange.upperBound();
// Sub periodicities do not match.
if (otherStart.subPeriodicity() != lowerBound.subPeriodicity())
throw new IllegalArgumentException(otherRange + " and " + this
+ " cannot be merged.");
TSDate erliestStart = (lowerBound.compareTo(otherStart) > 0) ? lowerBound
: otherStart;
TSDate latestEnd = (upperBound.compareTo(otherEnd) < 0) ? upperBound
: otherEnd;
return new TSDateRange(erliestStart, latestEnd);
}
/**
* This method returns the range represented as an array of Strings, for
* example {"1960 Q1", "1960 Q2", ...., "1982 Q4"}.
*
* @return time axis as string array
*/
public String[] timeAxisStringArray() {
return lowerBound.timeAxisStringArray(numOfObs());
}
/**
* Returns a string representation of the specified range. Do not use for
* comparisons, but use equals instead.
*
* @return string representing this range
*/
public String toString() {
return "[" + lowerBound + ", " + upperBound + "]";
}
/**
* Static initializer that creates a TSDateRange from a
* string representation. The string should be constructed as
* TSDate string , TSDate string, and can optionally be
* enclosed with [ ], for example
*
* [1960 Q1, 1982 Q4]
* 1970, 2003
* 1953 6/12, 1994 1/12
* 1/1/60 (5), 10/3/73 (5)
*
* The information about the sub periodicity must be contained in the
* strings, therefore 1960.1, 1982.4 is NOT possible,
* because it is not unique. For the string representation of a
* TSDate object, see the documentation of its
* valueOf method.
*
* @param exp
* the string to parse to a TSDateRange
* @return a TSDateRange object created from exp
* @throws IllegalArgumentException
* if object could not be constructed from given string or if
* exp == null
* @see TSDate
*/
public static TSDateRange valueOf(String exp) {
String errorMsg = "TSDateRange construction from \"" + exp
+ "\" failed.";
if (exp == null)
throw new IllegalArgumentException(errorMsg);
exp = exp.trim();
if (exp.startsWith("[") && exp.endsWith("]"))
exp = exp.substring(1, exp.length() - 1);
// Split input in 2 parts, separated by a komma
// and parse single date strings afterwards.
int kommaIndex = exp.indexOf(",");
if (kommaIndex < 1 || kommaIndex > exp.length() - 2)
// No komma within string, cannot be a valid range.
throw new IllegalArgumentException(errorMsg);
String t1String = exp.substring(0, kommaIndex);
String t2String = exp.substring(kommaIndex + 1, exp.length());
TSDate t1;
TSDate t2;
try {
t1 = TSDate.valueOf(t1String);
t2 = TSDate.valueOf(t2String);
} catch (Throwable ex) {
// Something went wrong during parsing or range construction.
// Wrong input must be considered exceptional behaviour.
throw new IllegalArgumentException(errorMsg + "\n" + ex);
}
if (t1.compareTo(t2) < 0)
throw new IllegalArgumentException(errorMsg);
return new TSDateRange(t1, t2);
}
}
Ads help maintain this website.