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

*

*

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