/*
 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 *

* 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.