/*
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:
*
* - name
*
- observations
*
- start date
*
- project
*
* 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.