/*
This file is part of the software framework JStatCom,
a library for building applications for data based analysis.
Copyright (C) 2000-2005 Alexander Benkwitz, 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 java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import com.jstatcom.parser.TSDateParser;
import com.jstatcom.util.DateDiff;
import com.jstatcom.util.UMath;
/**
* Representation of a date for an observation of a time series. It defines a
* mainPeriod, a subPeriod and a
* subPeriodicity. The subPeriodicity quantifies
* the number of sub periods which constitute one main period. It is also called
* frequency. A range of two TSDate objects is represented by the
* TSDateRange class.
*
* This class is immutable.
*
* @author A. Benkwitz, Markus Kraetzig
* @see com.jstatcom.ts.TSDateRange
*/
public final class TSDate implements Comparable {
/**
* Date format for parsing daily date specification.
*/
private static final DateFormat dateFormatUS = new SimpleDateFormat(
"MM/dd/yyyy", Locale.US);
/**
* Formatter for daily data.
*/
private static final DateFormat dailyFormat = new SimpleDateFormat(
"MM/dd/yyyy, EEE", Locale.US);
/**
* Short gormatter for daily data.
*/
private static final DateFormat dailyFormatShort = new SimpleDateFormat(
"M/d/yy", Locale.US);
/**
* Millisecond of Monday 01/05/1970 00:00.
*/
private static final Date START = new Date(342000000);
/**
* Milliseconds in a week.
*/
private static final long WEEK_MILLIS = 604800000;
/**
* Milliseconds of a day.
*/
private static final long DAY_MILLIS = 86400000;
private Date date = null;
private static final Calendar calendar = new GregorianCalendar();
/**
* Field that stores the main period.
*/
private final int mainPeriod;
/**
* Field that stores the sub period.
*/
private final int subPeriod;
/**
* Field that stores the sub periodicity. Will not change over the lifetime
* of the object.
*/
private final int subPeriodicity;
private final int subPeriodFormat;
/**
* Default constructor creates a standard TSDate object with
* string representation 1.
*/
public TSDate() {
this(1, 1, 1);
}
/**
* Creates a TSDate with a double coded date and a sub
* periodicity.
*
* @param newDate
* double coded date.
* @param newSubPeriodicity
* maximum number of sub periods.
* @throws IllegalArgumentException
* if subPeriodicity < 1 or subPeriod < 1 or subPeriodicity
* < subPeriod
*/
public TSDate(double newDate, int newSubPeriodicity) {
int newSubPeriodFormat = parseSubPeriodFormat(newSubPeriodicity);
int[] i = parseDate(newDate, newSubPeriodFormat);
int newMainPeriod = i[0];
int newSubPeriod = new Long(i[1]).intValue();
if (newSubPeriodicity < 1)
throw new IllegalArgumentException("Illegal sub periodicity ("
+ newSubPeriodicity + " < 1).");
if (newSubPeriod < 1)
throw new IllegalArgumentException("Illegal sub period ("
+ newSubPeriod + " < 1).");
if (newSubPeriodicity < newSubPeriod)
throw new IllegalArgumentException("Sub period > sub periodicity ("
+ newSubPeriod + " > " + newSubPeriodicity + ").");
this.mainPeriod = newMainPeriod;
this.subPeriod = newSubPeriod;
this.subPeriodicity = newSubPeriodicity;
this.subPeriodFormat = newSubPeriodFormat;
}
/**
* Creates a TSDate object from a main period, a sub period,
* and a sub periodicity.
*
* @param mainPeriod
* the main period (left of the decimal point).
* @param subPeriod
* the sub period ('right' of the decimal point).
* @param subPeriodicity
* the sub periodicity.
* @throws IllegalArgumentException
* if (subPeriodicity < 1
* || subPeriod < 1 || subPeriodicity < subPeriod)
*/
public TSDate(int mainPeriod, int subPeriod, int subPeriodicity) {
if (subPeriodicity < 1)
throw new IllegalArgumentException("Illegal sub periodicity ("
+ subPeriodicity + " < 1).");
if (subPeriod < 1)
throw new IllegalArgumentException("Illegal sub period ("
+ subPeriod + " < 1).");
if (subPeriodicity < subPeriod)
throw new IllegalArgumentException("Sub period > sub periodicity ("
+ subPeriod + " > " + subPeriodicity + ").");
this.mainPeriod = mainPeriod;
this.subPeriod = subPeriod;
this.subPeriodicity = subPeriodicity;
this.subPeriodFormat = parseSubPeriodFormat(subPeriodicity);
}
/**
* Adds n periods to a copy of the TSDate object.
*
* @param n
* number of periods to be added, can be negative
* @return new TSDate with n periods added
*/
public TSDate addPeriods(int n) {
if (n == 0)
return this;
/**
* 2 cases for n > 0 and n < 0.
*/
int newSubPeriod = subPeriodFor(n);
int newMainPeriod = 0;
if (n > 0) {
// Computes how many mainPeriods have to be added.
newMainPeriod = mainPeriod + n / subPeriodicity;
// Maybe one more needs to be added.
if (newSubPeriod < subPeriod)
newMainPeriod++;
} else {
//
n = Math.abs(n);
// Computes how many mainPeriods have to be added.
newMainPeriod = mainPeriod - n / subPeriodicity;
// Maybe one more needs to be added.
if (newSubPeriod > subPeriod)
newMainPeriod--;
}
return new TSDate(newMainPeriod, newSubPeriod, subPeriodicity);
}
/**
* Compares two TSDate objects.
*
* @return 0 if they are equal, otherwise number of periods between the two
* dates, which is < 0 if (TSDate) o is earlier
* than this
*
* @throws ClassCastException
* if o is not a TSDate
* @throws IllegalArgumentException
* if sub periodicities do not match, because there is no
* meaningful way to compare the dates
*/
public int compareTo(Object o) {
TSDate otherDate = (TSDate) o;
if (subPeriodicity != otherDate.subPeriodicity())
throw new IllegalArgumentException(
"Sub periodicities do not match between " + this + " and "
+ o + ".");
if (equals(otherDate))
return 0;
long result = (otherDate.mainPeriod() - mainPeriod) * subPeriodicity
+ otherDate.subPeriod() - subPeriod;
if (result > Integer.MAX_VALUE)
throw new IllegalStateException("Integer.MAX_VALUE overflow.");
if (result < Integer.MIN_VALUE)
throw new IllegalStateException("Integer.MIN_VALUE overflow.");
return (int) result;
}
/**
* Returns this date in number form.
*
* @return double representation of this TSDate
*/
public double doubleValue() {
if (subPeriodicity > 1)
return (mainPeriod + ((mainPeriod >= 0) ? 1 : -1)
* (double) subPeriod / parseSubPeriodFormat(subPeriodicity));
return mainPeriod;
}
/**
* Overrides equals to provide logical comparison of two
* different TSDate objects.
*
* @param o
* object to compare with
* @return true if mainPeriod, subPeriod and subPeriodicity
* are the same, false otherwise
*/
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof TSDate))
return false;
TSDate otherDate = (TSDate) o;
boolean ret = (mainPeriod == otherDate.mainPeriod()
&& subPeriod == otherDate.subPeriod() && subPeriodicity == otherDate
.subPeriodicity());
return ret;
}
/**
* Returns the main period of this date.
*
* @return the main period (left of the decimal point):
*/
public int mainPeriod() {
return mainPeriod;
}
/**
* Returns the sub period of this date.
*
* @return sub period, is always within [1, subPeriodicity]
*/
public int subPeriod() {
return subPeriod;
}
/**
* Gets the sub period is reached when n periods are added to
* this date.
*
* @param n
* number of periods to be added, can be negative
* @return the sub period as int
*/
public int subPeriodFor(int n) {
if (n == 0)
return subPeriod;
int newSubPeriod = 1;
/**
* 2 cases for n > 0 and n < 0.
*/
if (n > 0) {
int allSubPeriods = subPeriod + n;
// Computes new subperiod.
newSubPeriod = allSubPeriods % subPeriodicity;
// Special case, when last subPeriod is reached.
if (newSubPeriod == 0)
newSubPeriod = subPeriodicity;
} else {
//
n = Math.abs(n);
//
// Computes new subperiod.
int diff = subPeriod - (n % subPeriodicity);
if (diff >= 0)
newSubPeriod = diff;
else
newSubPeriod = subPeriodicity - Math.abs(diff);
// Special case, when last subPeriod is reached.
if (newSubPeriod == 0)
newSubPeriod = subPeriodicity;
}
return newSubPeriod;
}
/**
* Returns the sub period format for the double representation of this date.
* This number acts as a place holder for the digits of the sub period. If
* subPeriodicity < 10, this number returns 10, if
* 10 <= subPeriodicity < 100, this number returns 100
* and so on. If one multiplies the double coded date with the sub period
* format, one always gets a value that is equal to an integer, because the
* decimal digits vanish.
*
* @return the sub period format
*/
public int subPeriodFormat() {
return this.subPeriodFormat;
}
/**
* Returns the sub periodicity of this date. The subPeriodicity
* quantifies the number of sub periods which constitute one main period.
*
* @return the sub periodicity
*/
public int subPeriodicity() {
return subPeriodicity;
}
/**
* 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 + mainPeriod;
result = 37 * result + subPeriod;
result = 37 * result + subPeriodicity;
return result;
}
/**
* This class method parses the coded time series date. If no sub period was
* given, the method assumes that the first sub period is meant, e.g.
* 1960 is parsed as 1960.1.
*
* @param arg
* the double coded date.
* @param subPeriodFormat
* the sub period format parameter.
* @return two dimensional array of integers: first element is main period,
* second element is sub period
*/
private int[] parseDate(double arg, int subPeriodFormat) {
int[] i = new int[2];
i[0] = (int) arg;
if ((Math.rint(arg) - arg) == 0) // No subperiod given.
i[1] = 1;
else
i[1] = (int) Math.abs(Math.rint((arg - i[0]) * subPeriodFormat));
return i;
}
/**
* This method returns the format number for the sub periods that defines,
* how many digits can appear after the decimal point.
*
* @param subPeriodicity
* the frequency
* @return parsed sub period format
*/
private int parseSubPeriodFormat(int subPeriodicity) {
double[] d = UMath.base10(subPeriodicity);
return (int) Math.pow(10, d[1] + 1);
}
/**
* Gets a nicely formatted string representing this date. Unlike the
* toString method, the resulting string is not necessarily a
* valid argument for the valueOf method. Especially daily
* data is printed differently with the weekday and without "(5)" or "(7)".
*
* @return formatted string
*/
public String printNice() {
if (subPeriodicity == 5 || subPeriodicity == 7) {
return dailyFormat.format(getDate());
}
return toString();
}
/**
* Gets a formatted string representing this date that is very short. Unlike
* the toString method, the resulting string is not
* necessarily a valid argument for the valueOf method.
* Especially daily data is printed differently with the weekday and without
* "(5)" or "(7)".
*
* @return formatted string
*/
public String printShort() {
StringBuffer s = new StringBuffer();
if (subPeriodicity == 5 || subPeriodicity == 7) {
s.append(dailyFormatShort.format(getDate()));
} else {
s.append(mainPeriod);
switch (subPeriodicity) {
case (1):
break;
case (2):
if (subPeriod == 1)
s.append("I");
else
s.append("II");
break;
case (4):
s.append("Q");
s.append(subPeriod);
break;
case (12):
s.append("M");
s.append(subPeriod);
break;
default:
s.append(" ");
s.append(subPeriod);
s.append("/");
s.append(subPeriodicity);
}
}
return s.toString();
}
/**
* This method creates a time axis starting from this date as an array of
* Strings of length t, for example
* {"1960 Q1", "1960 Q2", ..., "1982 Q4"}.
*
* @param t
* int Length of the time axis.
* @return String[] time axis with dates for every time index
* @throws IllegalArgumentException
* if t < 1
*/
public String[] timeAxisStringArray(int t) {
if (t < 1)
throw new IllegalArgumentException("t < 1 for axis creation of "
+ this + ".");
String[] timeAxis = new String[t];
for (int i = 0; i < t; i++)
timeAxis[i] = addPeriods(i).printNice();
return timeAxis;
}
/**
* Currently only implemented for daily dates, otherwise an Exception is
* thrown.
*
* Assembles a java.util.Date object from this. The returned
* date always denotes the start of this date.
*
* @return date
* @throws IllegalStateException
* if this does not represent daily data
*/
public Date getDate() {
if (date == null) {
if (subPeriodicity == 7 || subPeriodicity == 5) {
// first set date and then get it
calendar.setTime(START);
calendar.add(Calendar.WEEK_OF_YEAR, mainPeriod);
calendar.add(Calendar.DAY_OF_WEEK, subPeriod - 1);
date = calendar.getTime();
} else
throw new IllegalStateException(
"Conversion to java.util.Date is not implemented for this frequency.");
}
return (Date) date.clone();
}
/**
* Returns a string representation of this TSDate. A
* TSDate object can be constructed from a string using the
* TSDateParser or respective the TSDate.valueOf
* initializer. Do not use for comparisons, but use equals instead.
*
* @return String representation of this object.
* @see com.jstatcom.parser.TSDateParser
*/
public String toString() {
StringBuffer s = new StringBuffer();
// daily data
if (subPeriodicity == 5 || subPeriodicity == 7) {
s.append(dateFormatUS.format(getDate()) + " (" + subPeriodicity + ")");
} else {
s.append(mainPeriod);
switch (subPeriodicity) {
case (1):
break;
case (2):
if (subPeriod == 1)
s.append(" I");
else
s.append(" II");
break;
case (4):
s.append(" Q");
s.append(subPeriod);
break;
case (12):
s.append(" M");
s.append(subPeriod);
break;
default:
s.append(" ");
s.append(subPeriod);
s.append("/");
s.append(subPeriodicity);
}
}
return s.toString();
}
/**
* Static initializer that creates a TSDate from a string
* expression. The format of the string is based on the following rules:
*
*
* XX x"/"y, for example 1960 1/1 is
* recognized for arbitrary sub periodicities with mainPeriod=XX,
* subPeriod=x, subPeriodicity=y
* XX ["M","m"]x, for example 1960 M1 is
* recognized as mainPeriod=XX, subPeriod=x, subPeriodicity=12 (monthly)
* XX ["Q","q"]x, for example 1960 Q1 is
* recognized as mainPeriod=XX, subPeriod=x, subPeriodicity=4 (quarterly)
* XX ["I", "II"], for example 1960 I is
* recognized as mainPeriod=XX, subPeriod=1 or 2, subPeriodicity=2 (half
* yearly)
* XX , for example 1960 is recognized as
* mainPeriod=XX, subPeriod=1, subPeriodicity=1 (yearly)
*
*
* Daily Data:
* Daily data is a special case. It is always assumed by this class when the
* periodicity is either 5 (working days) or 7 (all days). It can be
* specified by using the default date format for the US locale, followed by
* "(5)" or "(7)", for example:
*
*
* 1/1/73 (5)
* 10/12/2004 (7)
* If a 5 day week is specified and the date falls on a weekend, it
* is automatically set to the following monday without further notice.
*
*
* @param exp
* expression to parse to a date
* @return a TSDate object created from exp
* @throws IllegalArgumentException
* if object could not be constructed from given string or if
* exp == null
* @see com.jstatcom.parser.TSDateParser
*
*/
public static TSDate valueOf(String exp) {
TSDate tsDate = null;
if (exp == null)
throw new IllegalArgumentException(
"TSDate construction from null argument failed.");
tsDate = parseDailyData(exp);
// return if 1st parser run was successful
if (tsDate != null)
return tsDate;
// try to parse again
try {
tsDate = new TSDateParser().parseString(exp, -1);
} catch (Throwable ex) {
throw new IllegalArgumentException("TSDate construction from \""
+ exp + "\" failed.\n" + ex);
}
return tsDate;
}
/**
* Parses the argument to a date for daily data. Returns null
* if exp is not specified for daily data with a (5) or (7).
* Daily data is internally represented with a main period that counts the
* number of weeks since Monday 01/05/1970 00:00 and a sub periodicity of
* either 5 or 7. The sub period 1 always corresponds to monday.
*
* @param exp
* the string to be parsed
* @return date object or null
* @throws IllegalArgumentException
* if the argument string contained errors
*/
private static TSDate parseDailyData(String exp) {
// check whether daily data is used
String orig = exp;
exp = exp.trim();
String ending = "";
int index = exp.indexOf("(");
if (index == -1)
return null;
ending = exp.substring(index);
if (ending.endsWith(")")) {
ending = ending.substring(1, ending.length() - 1);
ending = ending.trim();
exp = exp.substring(0, index);
Date parseDate = null;
try {
parseDate = dateFormatUS.parse(exp);
} catch (Throwable ex) {
throw new IllegalArgumentException(
"TSDate construction from \"" + orig + "\" failed.\n"
+ ex);
}
if (ending.equals("5") || ending.equals("7")) {
calendar.setTime(parseDate);
int day = calendar.get(Calendar.DAY_OF_WEEK) - 1;
if (day == 0)
day = 7;
int periodicity = 7;
// correction if weekend day is specified
if (ending.equals("5")) {
periodicity = 5;
day = calendar.get(Calendar.DAY_OF_WEEK);
if (day == Calendar.SATURDAY) {// set to monday
parseDate.setTime(parseDate.getTime() + 2 * DAY_MILLIS);
day = 1;
} else {
if (day == Calendar.SUNDAY) {// set to monday
parseDate.setTime(parseDate.getTime() + DAY_MILLIS);
day = 1;
} else
day = day - 1; // because MONDAY should be 1
// instead
// of SUNDAY
}
}
// compare with Monday 01/05/1970 00:00
double num = ((double) parseDate.getTime() - (double) START
.getTime())
/ WEEK_MILLIS;
int main = DateDiff.getDateDiff(Calendar.WEEK_OF_YEAR, START,
parseDate);
// correction only if not a monday
if (num < 0 && day > 1)
--main;
int subperiod = day;
TSDate tsDate = new TSDate(main, subperiod, periodicity);
tsDate.date = parseDate;
return tsDate;
}
// illegal number of weekdays
throw new IllegalArgumentException("TSDate construction from \""
+ orig + "\" argument failed.\nIllegal number of weekdays.");
}
// unclosed bracket
throw new IllegalArgumentException("TSDate construction from \"" + orig
+ "\" argument failed.\nThere was an unclosed \"(\".");
}
}
Ads help maintain this website.