/*
 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.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.PrintStream;
import java.util.Stack;
import java.util.Vector;

import org.apache.log4j.Logger;

import com.jstatcom.model.JSCConstants;
import com.jstatcom.parser.CalcEngine;
import com.jstatcom.parser.CalcFunctions;
import com.jstatcom.util.PrintfFormat;

/**
 * This class pools information about a particular time series:
 * 
 * Missing values at the beginning and the end are automatically truncated when
 * the data is set. Every TS object must have at least one
 * observation, the data is therefore initialized with
 * new double[1]. Any attempt to set the data to a zero length
 * array or to null is ignored.
 * 

* Once a time series is created, it is not possible anymore to change the * underlying data, instead, a new TS must be instantiated. The * type may be changed. Type changes throw a TS_TYPE_CHANGED * PropertyChangeEvent. *

* This class is thread-safe. All access to mutable data is synchronized. * * @author A. Benkwitz, Markus Kraetzig */ public final class TS { private static final Logger log = Logger.getLogger(TS.class); private final int hashPartResult; /** * Type changed property event name. */ public static final String TS_TYPE_CHANGED = "TS_TYPE_CHANGED"; /** * Stores the current date range of the TS. */ private TSDateRange tSDateRange = null; /** * This field stores the values of the observations. */ private double[] dataArray = new double[] { Double.NaN }; /** * This field stores the date of the first observation. */ private TSDate startTSDate = null; /** * This field stores the project that the TS belongs to. */ private TSProject tSProject = null; /** * This field stores the name of the time series. This name is used in other * parts of the program to identify the time series (e.g. selection from * list). */ private String name = null; /** * Holds PropertyChangeListeners. */ private final Vector propertyChangeListeners = new Vector(); private TSTypes tSType = TSTypes.ENDOGENOUS; private static final PrintfFormat printfFormat = new PrintfFormat( java.util.Locale.ENGLISH, "%-" + 12 + "s"); /** * Creates a TS object with the type being guessed and the * project is the default. For the start date the TSDate * default constructor is used. * * @param newObservations * the array of observations * @param newName * the identifying name * @throws IllegalArgumentException * if newName is null */ public TS(double[] newObservations, String newName) { this(newObservations, newName, new TSDate(), null, null); } /** * Creates a TS object, with the type being guessed and the * project is the default. * * @param newObservations * the array of observations * @param newName * the identifying name * @param newDate * the date of the first observation. * @throws IllegalArgumentException * if newName or newDate is * null */ public TS(double[] newObservations, String newName, TSDate newDate) { this(newObservations, newName, newDate, null, null); } /** * Creates a TS object with the default project. * * @param newObservations * the array of observations * @param newName * the identifying name * @param newDate * the date of the first observation. * @param tsType * the type or null if it should be automatically * defined * @throws IllegalArgumentException * if newName or newDate is * null */ public TS(double[] newObservations, String newName, TSDate newDate, TSTypes tsType) { this(newObservations, newName, newDate, tsType, null); } /** * Preferred constructor for a TS object with everything * explicitely specified. * * @param newObservations * the array of observations * @param newName * the identifying name * @param newDate * the date of the first observation. * @param tsType * the type or null if it should be automatically * defined * @param newProject * the time series project * @throws IllegalArgumentException * if newName or newDate is * null */ public TS(double[] newObservations, String newName, TSDate newDate, TSTypes tsType, TSProject newProject) { setName(newName); setStartTSDate(newDate); setData(newObservations); setTSProject(newProject); setTSType(tsType); // Compute expensive and immutable part of hashCode. int result = 17; result = 37 * result + startTSDate.hashCode(); for (int i = 0; i < dataArray.length; i++) { long f = Double.doubleToLongBits(dataArray[i]); result = 37 * result + (int) (f ^ (f >>> 32)); } hashPartResult = result; } /** * TS copy-constructor. * * @param ts * the TS to copy */ public TS(TS ts) { this(ts.values(), ts.name(), ts.start(), ts.type(), ts.project()); } /** * */ public synchronized void addPropertyChangeListener( PropertyChangeListener listener) { if (!propertyChangeListeners.contains(listener)) propertyChangeListeners.add(listener); } /** * Applies the difference operator to a copy of the series and returns the * new series with the name oldName + "_d" + Math.abs(periods). * If periods is the same as the sub periodicity and the sub periodicity * > 1, the new name is oldName + "_dseas". * * @param periods * the number of lags of the part being subtracted from the * original series, it may also be < 0 * @return the differenced time series or null if the * operation failed. */ public TS createDiff(int periods) { TSDate date; double[] data; TSTypes type; String thisnam; // For thread savety. synchronized (this) { date = startTSDate; data = dataArray; type = tSType; thisnam = name; } String newName = thisnam + "_d" + Math.abs(periods); if (periods == date.subPeriodicity() && periods > 1) newName = thisnam + "_dseas"; TS diffTS = null; CalcEngine parser = new CalcEngine(); // OK, here we know that putVariable creates a copy of data. parser.putVariable(thisnam, data); try { parser.parseString(newName + "=" + thisnam + "-lagn(" + thisnam + "," + periods + ")"); diffTS = new TS(parser.getVariable(newName), newName, date, type); } catch (Throwable ex) { log.error("Differencing " + periods + " periods failed.", ex); return null; } return diffTS; } /** * Applies the log operator to a copy of the series and returns the new * series with the name oldName + "_log". * * @return TS the logged time series or null, if the * operation failed. */ public TS createLog() { TSDate date; double[] data; TSTypes type; String thisnam; // For thread savety. synchronized (this) { date = startTSDate; data = dataArray; type = tSType; thisnam = name; } String newName = thisnam + "_log"; TS loggedTS = null; Stack stack = new Stack(); // OK, here we trust that CalcFunctions.LOG does not change our array. stack.push(data); try { CalcFunctions.LOG.execute(1, stack); loggedTS = new TS(stack.pop(), newName, date, type); } catch (Throwable ex) { log.error("Taking log for " + name + " failed.", ex); return null; } return loggedTS; } /** * Overrides equals() to provide logical comparison of two * different TS. * * @param o * the object to compare * @return true if name, start, all observations and type are * the same, false otherwise */ public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof TS)) return false; TS otherTS = (TS) o; boolean dataCheck = true; synchronized (this) { synchronized (o) { if (!startTSDate.equals(otherTS.start())) return false; if (!name.equalsIgnoreCase(otherTS.name())) return false; if (type() != otherTS.type()) return false; dataCheck = numOfObs() == otherTS.numOfObs(); for (int i = 0; i < numOfObs() && dataCheck; i++) if (valueAt(i) != otherTS.valueAt(i)) dataCheck = false; } } return dataCheck; } /** * Notifies all PropertyChangeListeners. * * @param propName * String * @param oldValue * Object * @param newValue * Object */ private void firePropertyChange(String propName, Object oldValue, Object newValue) { PropertyChangeEvent evt = new PropertyChangeEvent(this, propName, oldValue, newValue); for (int i = 0; i < propertyChangeListeners.size(); i++) propertyChangeListeners.get(i).propertyChange(evt); } /** * Returns a copy of the array of observations. * * @return copied data array */ public synchronized double[] values() { double[] retObs = new double[dataArray.length]; for (int i = 0; i < dataArray.length; i++) retObs[i] = dataArray[i]; return retObs; } /** * Returns the observation at index i. * * @param i * the index to retrieve the data from * @return the respective observation if i is within the * range of this time series, otherwise Double.NaN */ public synchronized double valueAt(int i) { if (i < 0 || i > dataArray.length - 1) return Double.NaN; return dataArray[i]; } /** * Returns the observation at a given date. * * @param date * the TSDate for the observation, if it is * invalid, then Double.NaN is returned * @return the respective observation if date is within the range of this * time series, otherwise Double.NaN */ public synchronized double valueAt(TSDate date) { if (tSDateRange.encloses(date) != null) return Double.NaN; return dataArray[tSDateRange.indexForDate(date)]; } /** * Gets the merged data 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 data starting from the earliest till the latest observation * over all time series in the array, filled with * Double.NaN if no observation is available for a * series in the array at a certain date * * @throws IllegalArgumentException * if one TS is null or if sub * periodicities are different */ public static double[][] getMergedData(TS[] tsArray) { TSDateRange mergedRange = tsArray[0].range(); for (int i = 1; i < tsArray.length; i++) mergedRange = mergedRange.mergedRange(tsArray[i].range()); // data double[][] data = new double[mergedRange.numOfObs()][tsArray.length]; for (int i = 0; i < mergedRange.numOfObs(); i++) { TSDate currentDate = mergedRange.dateForIndex(i); for (int j = 0; j < tsArray.length; j++) { data[i][j] = tsArray[j].valueAt(currentDate); } } return data; } /** * Returns the identifying name of this time series object. * * @return the name of this series */ public synchronized String name() { return name; } /** * Returns the number of observations. * * @return number of observations. */ public synchronized int numOfObs() { return dataArray.length; } /** * Returns the date when the first observation in this TS * occurs. * * @return the date of the first observation. */ public synchronized TSDate start() { return startTSDate; } /** * Returns the TSDateRange defining the beginning and the end * of this time series. * * @return the range for the observations */ public synchronized TSDateRange range() { return tSDateRange; } /** * Gets project that this TS belongs to. * * @return the current project */ public synchronized TSProject project() { return tSProject; } /** * Gets the type of this time series. * * @return the type of this series * @see #setTSType */ public synchronized TSTypes type() { return tSType; } /** * Returns the most likely type for the underlying TS. The * method checks for patterns that indicate a that the series is * deterministic. If nothing is found, TSTypes.ENDOGENOUS is * returned. * * @return TSTypes.ENDOGENOUS or * TSTypes.DETERMINISTIC */ private TSTypes guessTSType() { double[] obs = values(); if (obs.length < 3) return TSTypes.DETERMINISTIC; // Checks whether series contains only 2 distinct values (like a dummy) // or whether it is constant in 1st differences. double distinct1 = obs[0]; double distinct2 = obs[0]; double diff = Double.NaN; boolean diffCheck = true; boolean distinctCheck = true; boolean startingNull = obs[0] == 0; for (int i = 1; i < obs.length; i++) { // get 2nd distinct value if (obs[i] != distinct1 && distinct1 == distinct2) distinct2 = obs[i]; // 3rd value check. distinctCheck = obs[i] == distinct1 || obs[i] == distinct2; // const diff check, but start it only after leading zeros are gone // also calculate diff after leading zeros if (startingNull) startingNull = obs[i - 1] == 0; else { // calculate diff only one time if (Double.isNaN(diff)) diff = obs[i] - obs[i - 1]; if (obs[i] != 0) // this is to ignore trailing zeros diffCheck = Math.abs(obs[i] - obs[i - 1] - diff) < 1e-15; } if (!diffCheck && !distinctCheck) return TSTypes.ENDOGENOUS; } return TSTypes.DETERMINISTIC; } /** * 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 = hashPartResult; synchronized (this) { result = 37 * result + name.toLowerCase().hashCode(); result = 37 * result + tSType.hashCode(); } return result; } /** * Prints this object to the argument print stream with the time index and * the respective observation next to it. * * @param p * the stream to print to */ public void print(PrintStream p) { String[] timeIndex = range().timeAxisStringArray(); p.println(name + " " + range()); for (int i = 0; i < timeIndex.length; i++) p.println(printfFormat.sprintf(timeIndex[i]) + " " + valueAt(i)); } /** * */ public synchronized void removePropertyChangeListener( PropertyChangeListener listener) { if (propertyChangeListeners.contains(listener)) propertyChangeListeners.remove(listener); } /** * Sets the observations to new values. It takes care to truncate * NaN values at the beginning and the end. Missing values in * the middle are allowed. * * @param newValue * the array of observations, if null, then the * method does nothing */ private void setData(double[] newValues) { if (newValues == null) return; if (newValues.length == 0) return; /** * Find index of first non NaN in NewValue, return if only NaN. */ int firstNonNanIndex = newValues.length - 1; for (int i = 0; i < newValues.length; i++) if (!Double.isNaN(newValues[i])) { // Found first value. firstNonNanIndex = i; break; } /** * Check whether there are possible non NaN values. This needs to be * checked again after removing NaN's from the end. */ if (firstNonNanIndex > newValues.length) return; /** * Starting from the end, find last index that is not NaN. */ int lastNonNanIndex = 0; for (int i = newValues.length - 1; i >= firstNonNanIndex; i--) if (!new Double(newValues[i]).equals(JSCConstants.NaN)) { // Found first value. lastNonNanIndex = i; break; } /** * Check whether there are non NaN values. */ if (lastNonNanIndex - firstNonNanIndex + 1 < 0) return; /** * Copy all observations to new vector and compute the new start date. */ double[] newObservations = new double[lastNonNanIndex - firstNonNanIndex + 1]; for (int i = 0; i < newObservations.length; i++) newObservations[i] = newValues[i + firstNonNanIndex]; TSDate newDate = startTSDate.addPeriods(firstNonNanIndex); dataArray = newObservations; setStartTSDate(newDate); } /** * Sets the name of this time series to a new name. The name must not be * null or empty. The name must begin either with '_' . * * @param newValue * the new name. * @throws IllegalArgumentException * if newValue is null, empty or if it does not * start with a letter or '_' */ public void setName(String newValue) { String error = JSCConstants.isValidName(newValue); if (error != null) throw new IllegalArgumentException(error); synchronized (this) { name = newValue; } } /** * Sets the date of the first observation of this time series to a new * value. * * @param newValue * new date of first observation * @throws IllegalArgumentException * if newValue is null */ private void setStartTSDate(TSDate newValue) { if (newValue == null) throw new IllegalArgumentException("startdate cannot be null"); startTSDate = newValue; tSDateRange = new TSDateRange(startTSDate, startTSDate .addPeriods(dataArray.length - 1)); } /** * Sets project that this TS belongs to. * * @param newTSProject * the new project */ public synchronized void setTSProject(TSProject newTSProject) { if (newTSProject == null) newTSProject = TSProject.DEFAULT_PROJECT; tSProject = newTSProject; } /** * Sets the type of this time series. If the argument is null, * the type is determined internally to be either deterministic or * endogenous. * * @param newTSType * the new type of the TS * @see #type */ public void setTSType(TSTypes newTSType) { TSTypes oldValue; if (newTSType == null) newTSType = guessTSType(); synchronized (this) { oldValue = tSType; tSType = newTSType; } // Outside synchronized region. if (oldValue != newTSType) firePropertyChange(TS_TYPE_CHANGED, name, name); } /** * String representation of a timeseries. Do not use for comparisons, * implementation might change without notice. * * @return a string representing this series */ public String toString() { StringBuffer ts = new StringBuffer(); synchronized (this) { ts.append(super.toString() + " [name=" + name); ts.append(",start=" + startTSDate); ts.append(",obs=" + numOfObs()); if (dataArray.length > 0) { ts.append(",first=" + dataArray[0]); ts.append(",last=" + dataArray[dataArray.length - 1]); } ts.append(",project=" + project()); ts.append(",type=" + type()); } ts.append("]"); return ts.toString(); } }

Ads help maintain this website.