    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors.
    006 *
    007 * Project Info:  http://www.jfree.org/jfreechart/index.html
    008 *
    009 * This library is free software; you can redistribute it and/or modify it
    010 * under the terms of the GNU Lesser General Public License as published by
    011 * the Free Software Foundation; either version 2.1 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This library is distributed in the hope that it will be useful, but
    015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017 * License for more details.
    018 *
    019 * You should have received a copy of the GNU Lesser General Public
    020 * License along with this library; if not, write to the Free Software
    021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022 * USA.
    023 *
    024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    025 * Other names may be trademarks of their respective owners.]
    026 *
    027 * ---------------
    028 * TimeSeries.java
    029 * ---------------
    030 * (C) Copyright 2001-2011, by Object Refinery Limited.
    031 *
    032 * Original Author:  David Gilbert (for Object Refinery Limited);
    033 * Contributor(s):   Bryan Scott;
    034 *                   Nick Guenther;
    035 *
    036 * Changes
    037 * -------
    038 * 11-Oct-2001 : Version 1 (DG);
    039 * 14-Nov-2001 : Added listener mechanism (DG);
    040 * 15-Nov-2001 : Updated argument checking and exceptions in add() method (DG);
    041 * 29-Nov-2001 : Added properties to describe the domain and range (DG);
    042 * 07-Dec-2001 : Renamed TimeSeries --> BasicTimeSeries (DG);
    043 * 01-Mar-2002 : Updated import statements (DG);
    044 * 28-Mar-2002 : Added a method add(TimePeriod, double) (DG);
    045 * 27-Aug-2002 : Changed return type of delete method to void (DG);
    046 * 04-Oct-2002 : Added itemCount and historyCount attributes, fixed errors
    047 *               reported by Checkstyle (DG);
    048 * 29-Oct-2002 : Added series change notification to addOrUpdate() method (DG);
    049 * 28-Jan-2003 : Changed name back to TimeSeries (DG);
    050 * 13-Mar-2003 : Moved to com.jrefinery.data.time package and implemented
    051 *               Serializable (DG);
    052 * 01-May-2003 : Updated equals() method (see bug report 727575) (DG);
    053 * 14-Aug-2003 : Added ageHistoryCountItems method (copied existing code for
    054 *               contents) made a method and added to addOrUpdate.  Made a
    055 *               public method to enable ageing against a specified time
    056 *               (eg now) as opposed to lastest time in series (BS);
    057 * 15-Oct-2003 : Added fix for setItemCount method - see bug report 804425.
    058 *               Modified exception message in add() method to be more
    059 *               informative (DG);
    060 * 13-Apr-2004 : Added clear() method (DG);
    061 * 21-May-2004 : Added an extra addOrUpdate() method (DG);
    062 * 15-Jun-2004 : Fixed NullPointerException in equals() method (DG);
    063 * 29-Nov-2004 : Fixed bug 1075255 (DG);
    064 * 17-Nov-2005 : Renamed historyCount --> maximumItemAge (DG);
    065 * 28-Nov-2005 : Changed maximumItemAge from int to long (DG);
    066 * 01-Dec-2005 : New add methods accept notify flag (DG);
    067 * ------------- JFREECHART 1.0.x ---------------------------------------------
    068 * 24-May-2006 : Improved error handling in createCopy() methods (DG);
    069 * 01-Sep-2006 : Fixed bugs in removeAgedItems() methods - see bug report
    070 *               1550045 (DG);
    071 * 22-Mar-2007 : Simplified getDataItem(RegularTimePeriod) - see patch 1685500
    072 *               by Nick Guenther (DG);
    073 * 31-Oct-2007 : Implemented faster hashCode() (DG);
    074 * 21-Nov-2007 : Fixed clone() method (bug 1832432) (DG);
    075 * 10-Jan-2008 : Fixed createCopy(RegularTimePeriod, RegularTimePeriod) (bug
    076 *               1864222) (DG);
    077 * 13-Jan-2009 : Fixed constructors so that timePeriodClass doesn't need to
    078 *               be specified in advance (DG);
    079 * 26-May-2009 : Added cache for minY and maxY values (DG);
    080 * 09-Jun-2009 : Ensure that TimeSeriesDataItem objects used in underlying
    081 *               storage are cloned to keep series isolated from external
    082 *               changes (DG);
    083 * 10-Jun-2009 : Added addOrUpdate(TimeSeriesDataItem) method (DG);
    084 * 31-Aug-2009 : Clear minY and maxY cache values in createCopy (DG);
    085 *
    086 */
    087
    088package org.jfree.data.time;
    089
    090import java.io.Serializable;
    091import java.lang.reflect.InvocationTargetException;
    092import java.lang.reflect.Method;
    093import java.util.Collection;
    094import java.util.Collections;
    095import java.util.Date;
    096import java.util.Iterator;
    097import java.util.List;
    098import java.util.TimeZone;
    099
    100import org.jfree.data.general.Series;
    101import org.jfree.data.general.SeriesChangeEvent;
    102import org.jfree.data.general.SeriesException;
    103import org.jfree.util.ObjectUtilities;
    104
    105/**
    106 * Represents a sequence of zero or more data items in the form (period, value)
    107 * where 'period' is some instance of a subclass of {@link RegularTimePeriod}.
    108 * The time series will ensure that (a) all data items have the same type of
    109 * period (for example, {@link Day}) and (b) that each period appears at
    110 * most one time in the series.
    111 */
    112public class TimeSeries extends Series implements Cloneable, Serializable {
    113
    114    /** For serialization. */
    115    private static final long serialVersionUID = -5032960206869675528L;
    116
    117    /** Default value for the domain description. */
    118    protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time";
    119
    120    /** Default value for the range description. */
    121    protected static final String DEFAULT_RANGE_DESCRIPTION = "Value";
    122
    123    /** A description of the domain. */
    124    private String domain;
    125
    126    /** A description of the range. */
    127    private String range;
    128
    129    /** The type of period for the data. */
    130    protected Class timePeriodClass;
    131
    132    /** The list of data items in the series. */
    133    protected List data;
    134
    135    /** The maximum number of items for the series. */
    136    private int maximumItemCount;
    137
    138    /**
    139     * The maximum age of items for the series, specified as a number of
    140     * time periods.
    141     */
    142    private long maximumItemAge;
    143
    144    /**
    145     * The minimum y-value in the series.
    146     *
    147     * @since 1.0.14
    148     */
    149    private double minY;
    150
    151    /**
    152     * The maximum y-value in the series.
    153     *
    154     * @since 1.0.14
    155     */
    156    private double maxY;
    157
    158    /**
    159     * Creates a new (empty) time series.  By default, a daily time series is
    160     * created.  Use one of the other constructors if you require a different
    161     * time period.
    162     *
    163     * @param name  the series name (<code>null</code> not permitted).
    164     */
    165    public TimeSeries(Comparable name) {
    166        this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION);
    167    }
    168
    169    /**
    170     * Creates a new time series that contains no data.
    171     * <P>
    172     * Descriptions can be specified for the domain and range.  One situation
    173     * where this is helpful is when generating a chart for the time series -
    174     * axis labels can be taken from the domain and range description.
    175     *
    176     * @param name  the name of the series (<code>null</code> not permitted).
    177     * @param domain  the domain description (<code>null</code> permitted).
    178     * @param range  the range description (<code>null</code> permitted).
    179     *
    180     * @since 1.0.13
    181     */
    182    public TimeSeries(Comparable name, String domain, String range) {
    183        super(name);
    184        this.domain = domain;
    185        this.range = range;
    186        this.timePeriodClass = null;
    187        this.data = new java.util.ArrayList();
    188        this.maximumItemCount = Integer.MAX_VALUE;
    189        this.maximumItemAge = Long.MAX_VALUE;
    190        this.minY = Double.NaN;
    191        this.maxY = Double.NaN;
    192    }
    193
    194    /**
    195     * Returns the domain description.
    196     *
    197     * @return The domain description (possibly <code>null</code>).
    198     *
    199     * @see #setDomainDescription(String)
    200     */
    201    public String getDomainDescription() {
    202        return this.domain;
    203    }
    204
    205    /**
    206     * Sets the domain description and sends a <code>PropertyChangeEvent</code>
    207     * (with the property name <code>Domain</code>) to all registered
    208     * property change listeners.
    209     *
    210     * @param description  the description (<code>null</code> permitted).
    211     *
    212     * @see #getDomainDescription()
    213     */
    214    public void setDomainDescription(String description) {
    215        String old = this.domain;
    216        this.domain = description;
    217        firePropertyChange("Domain", old, description);
    218    }
    219
    220    /**
    221     * Returns the range description.
    222     *
    223     * @return The range description (possibly <code>null</code>).
    224     *
    225     * @see #setRangeDescription(String)
    226     */
    227    public String getRangeDescription() {
    228        return this.range;
    229    }
    230
    231    /**
    232     * Sets the range description and sends a <code>PropertyChangeEvent</code>
    233     * (with the property name <code>Range</code>) to all registered listeners.
    234     *
    235     * @param description  the description (<code>null</code> permitted).
    236     *
    237     * @see #getRangeDescription()
    238     */
    239    public void setRangeDescription(String description) {
    240        String old = this.range;
    241        this.range = description;
    242        firePropertyChange("Range", old, description);
    243    }
    244
    245    /**
    246     * Returns the number of items in the series.
    247     *
    248     * @return The item count.
    249     */
    250    public int getItemCount() {
    251        return this.data.size();
    252    }
    253
    254    /**
    255     * Returns the list of data items for the series (the list contains
    256     * {@link TimeSeriesDataItem} objects and is unmodifiable).
    257     *
    258     * @return The list of data items.
    259     */
    260    public List getItems() {
    261        // FIXME: perhaps we should clone the data list
    262        return Collections.unmodifiableList(this.data);
    263    }
    264
    265    /**
    266     * Returns the maximum number of items that will be retained in the series.
    267     * The default value is <code>Integer.MAX_VALUE</code>.
    268     *
    269     * @return The maximum item count.
    270     *
    271     * @see #setMaximumItemCount(int)
    272     */
    273    public int getMaximumItemCount() {
    274        return this.maximumItemCount;
    275    }
    276
    277    /**
    278     * Sets the maximum number of items that will be retained in the series.
    279     * If you add a new item to the series such that the number of items will
    280     * exceed the maximum item count, then the FIRST element in the series is
    281     * automatically removed, ensuring that the maximum item count is not
    282     * exceeded.
    283     *
    284     * @param maximum  the maximum (requires >= 0).
    285     *
    286     * @see #getMaximumItemCount()
    287     */
    288    public void setMaximumItemCount(int maximum) {
    289        if (maximum < 0) {
    290            throw new IllegalArgumentException("Negative 'maximum' argument.");
    291        }
    292        this.maximumItemCount = maximum;
    293        int count = this.data.size();
    294        if (count > maximum) {
    295            delete(0, count - maximum - 1);
    296        }
    297    }
    298
    299    /**
    300     * Returns the maximum item age (in time periods) for the series.
    301     *
    302     * @return The maximum item age.
    303     *
    304     * @see #setMaximumItemAge(long)
    305     */
    306    public long getMaximumItemAge() {
    307        return this.maximumItemAge;
    308    }
    309
    310    /**
    311     * Sets the number of time units in the 'history' for the series.  This
    312     * provides one mechanism for automatically dropping old data from the
    313     * time series. For example, if a series contains daily data, you might set
    314     * the history count to 30.  Then, when you add a new data item, all data
    315     * items more than 30 days older than the latest value are automatically
    316     * dropped from the series.
    317     *
    318     * @param periods  the number of time periods.
    319     *
    320     * @see #getMaximumItemAge()
    321     */
    322    public void setMaximumItemAge(long periods) {
    323        if (periods < 0) {
    324            throw new IllegalArgumentException("Negative 'periods' argument.");
    325        }
    326        this.maximumItemAge = periods;
    327        removeAgedItems(true);  // remove old items and notify if necessary
    328    }
    329
    330    /**
    331     * Returns the smallest y-value in the series, ignoring any null and
    332     * Double.NaN values.  This method returns Double.NaN if there is no
    333     * smallest y-value (for example, when the series is empty).
    334     *
    335     * @return The smallest y-value.
    336     *
    337     * @see #getMaxY()
    338     *
    339     * @since 1.0.14
    340     */
    341    public double getMinY() {
    342        return this.minY;
    343    }
    344
    345    /**
    346     * Returns the largest y-value in the series, ignoring any Double.NaN
    347     * values.  This method returns Double.NaN if there is no largest y-value
    348     * (for example, when the series is empty).
    349     *
    350     * @return The largest y-value.
    351     *
    352     * @see #getMinY()
    353     *
    354     * @since 1.0.14
    355     */
    356    public double getMaxY() {
    357        return this.maxY;
    358    }
    359
    360    /**
    361     * Returns the time period class for this series.
    362     * <p>
    363     * Only one time period class can be used within a single series (enforced).
    364     * If you add a data item with a {@link Year} for the time period, then all
    365     * subsequent data items must also have a {@link Year} for the time period.
    366     *
    367     * @return The time period class (may be <code>null</code> but only for
    368     *     an empty series).
    369     */
    370    public Class getTimePeriodClass() {
    371        return this.timePeriodClass;
    372    }
    373
    374    /**
    375     * Returns a data item from the dataset.  Note that the returned object
    376     * is a clone of the item in the series, so modifying it will have no
    377     * effect on the data series.
    378     *
    379     * @param index  the item index.
    380     *
    381     * @return The data item.
    382     */
    383    public TimeSeriesDataItem getDataItem(int index) {
    384        TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(index);
    385        return (TimeSeriesDataItem) item.clone();
    386    }
    387
    388    /**
    389     * Returns the data item for a specific period.  Note that the returned
    390     * object is a clone of the item in the series, so modifying it will have
    391     * no effect on the data series.
    392     *
    393     * @param period  the period of interest (<code>null</code> not allowed).
    394     *
    395     * @return The data item matching the specified period (or
    396     *         <code>null</code> if there is no match).
    397     *
    398     * @see #getDataItem(int)
    399     */
    400    public TimeSeriesDataItem getDataItem(RegularTimePeriod period) {
    401        int index = getIndex(period);
    402        if (index >= 0) {
    403            return getDataItem(index);
    404        }
    405        return null;
    406    }
    407
    408    /**
    409     * Returns a data item for the series.  This method returns the object
    410     * that is used for the underlying storage - you should not modify the
    411     * contents of the returned value unless you know what you are doing.
    412     *
    413     * @param index  the item index (zero-based).
    414     *
    415     * @return The data item.
    416     *
    417     * @see #getDataItem(int)
    418     *
    419     * @since 1.0.14
    420     */
    421    TimeSeriesDataItem getRawDataItem(int index) {
    422        return (TimeSeriesDataItem) this.data.get(index);
    423    }
    424
    425    /**
    426     * Returns a data item for the series.  This method returns the object
    427     * that is used for the underlying storage - you should not modify the
    428     * contents of the returned value unless you know what you are doing.
    429     *
    430     * @param period  the item index (zero-based).
    431     *
    432     * @return The data item.
    433     *
    434     * @see #getDataItem(RegularTimePeriod)
    435     *
    436     * @since 1.0.14
    437     */
    438    TimeSeriesDataItem getRawDataItem(RegularTimePeriod period) {
    439        int index = getIndex(period);
    440        if (index >= 0) {
    441            return (TimeSeriesDataItem) this.data.get(index);
    442        }
    443        return null;
    444    }
    445
    446    /**
    447     * Returns the time period at the specified index.
    448     *
    449     * @param index  the index of the data item.
    450     *
    451     * @return The time period.
    452     */
    453    public RegularTimePeriod getTimePeriod(int index) {
    454        return getRawDataItem(index).getPeriod();
    455    }
    456
    457    /**
    458     * Returns a time period that would be the next in sequence on the end of
    459     * the time series.
    460     *
    461     * @return The next time period.
    462     */
    463    public RegularTimePeriod getNextTimePeriod() {
    464        RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
    465        return last.next();
    466    }
    467
    468    /**
    469     * Returns a collection of all the time periods in the time series.
    470     *
    471     * @return A collection of all the time periods.
    472     */
    473    public Collection getTimePeriods() {
    474        Collection result = new java.util.ArrayList();
    475        for (int i = 0; i < getItemCount(); i++) {
    476            result.add(getTimePeriod(i));
    477        }
    478        return result;
    479    }
    480
    481    /**
    482     * Returns a collection of time periods in the specified series, but not in
    483     * this series, and therefore unique to the specified series.
    484     *
    485     * @param series  the series to check against this one.
    486     *
    487     * @return The unique time periods.
    488     */
    489    public Collection getTimePeriodsUniqueToOtherSeries(TimeSeries series) {
    490        Collection result = new java.util.ArrayList();
    491        for (int i = 0; i < series.getItemCount(); i++) {
    492            RegularTimePeriod period = series.getTimePeriod(i);
    493            int index = getIndex(period);
    494            if (index < 0) {
    495                result.add(period);
    496            }
    497        }
    498        return result;
    499    }
    500
    501    /**
    502     * Returns the index for the item (if any) that corresponds to a time
    503     * period.
    504     *
    505     * @param period  the time period (<code>null</code> not permitted).
    506     *
    507     * @return The index.
    508     */
    509    public int getIndex(RegularTimePeriod period) {
    510        if (period == null) {
    511            throw new IllegalArgumentException("Null 'period' argument.");
    512        }
    513        TimeSeriesDataItem dummy = new TimeSeriesDataItem(
    514              period, Integer.MIN_VALUE);
    515        return Collections.binarySearch(this.data, dummy);
    516    }
    517
    518    /**
    519     * Returns the value at the specified index.
    520     *
    521     * @param index  index of a value.
    522     *
    523     * @return The value (possibly <code>null</code>).
    524     */
    525    public Number getValue(int index) {
    526        return getRawDataItem(index).getValue();
    527    }
    528
    529    /**
    530     * Returns the value for a time period.  If there is no data item with the
    531     * specified period, this method will return <code>null</code>.
    532     *
    533     * @param period  time period (<code>null</code> not permitted).
    534     *
    535     * @return The value (possibly <code>null</code>).
    536     */
    537    public Number getValue(RegularTimePeriod period) {
    538        int index = getIndex(period);
    539        if (index >= 0) {
    540            return getValue(index);
    541        }
    542        return null;
    543    }
    544
    545    /**
    546     * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
    547     * all registered listeners.
    548     *
    549     * @param item  the (timeperiod, value) pair (<code>null</code> not
    550     *              permitted).
    551     */
    552    public void add(TimeSeriesDataItem item) {
    553        add(item, true);
    554    }
    555
    556    /**
    557     * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
    558     * all registered listeners.
    559     *
    560     * @param item  the (timeperiod, value) pair (<code>null</code> not
    561     *              permitted).
    562     * @param notify  notify listeners?
    563     */
    564    public void add(TimeSeriesDataItem item, boolean notify) {
    565        if (item == null) {
    566            throw new IllegalArgumentException("Null 'item' argument.");
    567        }
    568        item = (TimeSeriesDataItem) item.clone();
    569        Class c = item.getPeriod().getClass();
    570        if (this.timePeriodClass == null) {
    571            this.timePeriodClass = c;
    572        }
    573        else if (!this.timePeriodClass.equals(c)) {
    574            StringBuffer b = new StringBuffer();
    575            b.append("You are trying to add data where the time period class ");
    576            b.append("is ");
    577            b.append(item.getPeriod().getClass().getName());
    578            b.append(", but the TimeSeries is expecting an instance of ");
    579            b.append(this.timePeriodClass.getName());
    580            b.append(".");
    581            throw new SeriesException(b.toString());
    582        }
    583
    584        // make the change (if it's not a duplicate time period)...
    585        boolean added = false;
    586        int count = getItemCount();
    587        if (count == 0) {
    588            this.data.add(item);
    589            added = true;
    590        }
    591        else {
    592            RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
    593            if (item.getPeriod().compareTo(last) > 0) {
    594                this.data.add(item);
    595                added = true;
    596            }
    597            else {
    598                int index = Collections.binarySearch(this.data, item);
    599                if (index < 0) {
    600                    this.data.add(-index - 1, item);
    601                    added = true;
    602                }
    603                else {
    604                    StringBuffer b = new StringBuffer();
    605                    b.append("You are attempting to add an observation for ");
    606                    b.append("the time period ");
    607                    b.append(item.getPeriod().toString());
    608                    b.append(" but the series already contains an observation");
    609                    b.append(" for that time period. Duplicates are not ");
    610                    b.append("permitted.  Try using the addOrUpdate() method.");
    611                    throw new SeriesException(b.toString());
    612                }
    613            }
    614        }
    615        if (added) {
    616            updateBoundsForAddedItem(item);
    617            // check if this addition will exceed the maximum item count...
    618            if (getItemCount() > this.maximumItemCount) {
    619                TimeSeriesDataItem d = (TimeSeriesDataItem) this.data.remove(0);
    620                updateBoundsForRemovedItem(d);
    621            }
    622
    623            removeAgedItems(false);  // remove old items if necessary, but
    624                                     // don't notify anyone, because that
    625                                     // happens next anyway...
    626            if (notify) {
    627                fireSeriesChanged();
    628            }
    629        }
    630
    631    }
    632
    633    /**
    634     * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
    635     * to all registered listeners.
    636     *
    637     * @param period  the time period (<code>null</code> not permitted).
    638     * @param value  the value.
    639     */
    640    public void add(RegularTimePeriod period, double value) {
    641        // defer argument checking...
    642        add(period, value, true);
    643    }
    644
    645    /**
    646     * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
    647     * to all registered listeners.
    648     *
    649     * @param period  the time period (<code>null</code> not permitted).
    650     * @param value  the value.
    651     * @param notify  notify listeners?
    652     */
    653    public void add(RegularTimePeriod period, double value, boolean notify) {
    654        // defer argument checking...
    655        TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
    656        add(item, notify);
    657    }
    658
    659    /**
    660     * Adds a new data item to the series and sends
    661     * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered
    662     * listeners.
    663     *
    664     * @param period  the time period (<code>null</code> not permitted).
    665     * @param value  the value (<code>null</code> permitted).
    666     */
    667    public void add(RegularTimePeriod period, Number value) {
    668        // defer argument checking...
    669        add(period, value, true);
    670    }
    671
    672    /**
    673     * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
    674     * to all registered listeners.
    675     *
    676     * @param period  the time period (<code>null</code> not permitted).
    677     * @param value  the value (<code>null</code> permitted).
    678     * @param notify  notify listeners?
    679     */
    680    public void add(RegularTimePeriod period, Number value, boolean notify) {
    681        // defer argument checking...
    682        TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
    683        add(item, notify);
    684    }
    685
    686    /**
    687     * Updates (changes) the value for a time period.  Throws a
    688     * {@link SeriesException} if the period does not exist.
    689     *
    690     * @param period  the period (<code>null</code> not permitted).
    691     * @param value  the value.
    692     *
    693     * @since 1.0.14
    694     */
    695    public void update(RegularTimePeriod period, double value) {
    696      update(period, new Double(value));
    697    }
    698
    699    /**
    700     * Updates (changes) the value for a time period.  Throws a
    701     * {@link SeriesException} if the period does not exist.
    702     *
    703     * @param period  the period (<code>null</code> not permitted).
    704     * @param value  the value (<code>null</code> permitted).
    705     */
    706    public void update(RegularTimePeriod period, Number value) {
    707        TimeSeriesDataItem temp = new TimeSeriesDataItem(period, value);
    708        int index = Collections.binarySearch(this.data, temp);
    709        if (index < 0) {
    710            throw new SeriesException("There is no existing value for the "
    711                    + "specified 'period'.");
    712        }
    713        update(index, value);
    714    }
    715
    716    /**
    717     * Updates (changes) the value of a data item.
    718     *
    719     * @param index  the index of the data item.
    720     * @param value  the new value (<code>null</code> permitted).
    721     */
    722    public void update(int index, Number value) {
    723        TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(index);
    724        boolean iterate = false;
    725        Number oldYN = item.getValue();
    726        if (oldYN != null) {
    727            double oldY = oldYN.doubleValue();
    728            if (!Double.isNaN(oldY)) {
    729                iterate = oldY <= this.minY || oldY >= this.maxY;
    730            }
    731        }
    732        item.setValue(value);
    733        if (iterate) {
    734            findBoundsByIteration();
    735        }
    736        else if (value != null) {
    737            double yy = value.doubleValue();
    738            this.minY = minIgnoreNaN(this.minY, yy);
    739            this.maxY = maxIgnoreNaN(this.maxY, yy);
    740        }
    741        fireSeriesChanged();
    742    }
    743
    744    /**
    745     * Adds or updates data from one series to another.  Returns another series
    746     * containing the values that were overwritten.
    747     *
    748     * @param series  the series to merge with this.
    749     *
    750     * @return A series containing the values that were overwritten.
    751     */
    752    public TimeSeries addAndOrUpdate(TimeSeries series) {
    753        TimeSeries overwritten = new TimeSeries("Overwritten values from: "
    754                + getKey());
    755        for (int i = 0; i < series.getItemCount(); i++) {
    756            TimeSeriesDataItem item = series.getRawDataItem(i);
    757            TimeSeriesDataItem oldItem = addOrUpdate(item.getPeriod(),
    758                    item.getValue());
    759            if (oldItem != null) {
    760                overwritten.add(oldItem);
    761            }
    762        }
    763        return overwritten;
    764    }
    765
    766    /**
    767     * Adds or updates an item in the times series and sends a
    768     * {@link SeriesChangeEvent} to all registered listeners.
    769     *
    770     * @param period  the time period to add/update (<code>null</code> not
    771     *                permitted).
    772     * @param value  the new value.
    773     *
    774     * @return A copy of the overwritten data item, or <code>null</code> if no
    775     *         item was overwritten.
    776     */
    777    public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
    778                                          double value) {
    779        return addOrUpdate(period, new Double(value));
    780    }
    781
    782    /**
    783     * Adds or updates an item in the times series and sends a
    784     * {@link SeriesChangeEvent} to all registered listeners.
    785     *
    786     * @param period  the time period to add/update (<code>null</code> not
    787     *                permitted).
    788     * @param value  the new value (<code>null</code> permitted).
    789     *
    790     * @return A copy of the overwritten data item, or <code>null</code> if no
    791     *         item was overwritten.
    792     */
    793    public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
    794                                          Number value) {
    795        return addOrUpdate(new TimeSeriesDataItem(period, value));
    796    }
    797
    798    /**
    799     * Adds or updates an item in the times series and sends a
    800     * {@link SeriesChangeEvent} to all registered listeners.
    801     *
    802     * @param item  the data item (<code>null</code> not permitted).
    803     *
    804     * @return A copy of the overwritten data item, or <code>null</code> if no
    805     *         item was overwritten.
    806     *
    807     * @since 1.0.14
    808     */
    809    public TimeSeriesDataItem addOrUpdate(TimeSeriesDataItem item) {
    810
    811        if (item == null) {
    812            throw new IllegalArgumentException("Null 'period' argument.");
    813        }
    814        Class periodClass = item.getPeriod().getClass();
    815        if (this.timePeriodClass == null) {
    816            this.timePeriodClass = periodClass;
    817        }
    818        else if (!this.timePeriodClass.equals(periodClass)) {
    819            String msg = "You are trying to add data where the time "
    820                    + "period class is " + periodClass.getName()
    821                    + ", but the TimeSeries is expecting an instance of "
    822                    + this.timePeriodClass.getName() + ".";
    823            throw new SeriesException(msg);
    824        }
    825        TimeSeriesDataItem overwritten = null;
    826        int index = Collections.binarySearch(this.data, item);
    827        if (index >= 0) {
    828            TimeSeriesDataItem existing
    829                    = (TimeSeriesDataItem) this.data.get(index);
    830            overwritten = (TimeSeriesDataItem) existing.clone();
    831            // figure out if we need to iterate through all the y-values
    832            // to find the revised minY / maxY
    833            boolean iterate = false;
    834            Number oldYN = existing.getValue();
    835            double oldY = oldYN != null ? oldYN.doubleValue() : Double.NaN;
    836            if (!Double.isNaN(oldY)) {
    837                iterate = oldY <= this.minY || oldY >= this.maxY;
    838            }
    839            existing.setValue(item.getValue());
    840            if (iterate) {
    841                findBoundsByIteration();
    842            }
    843            else if (item.getValue() != null) {
    844                double yy = item.getValue().doubleValue();
    845                this.minY = minIgnoreNaN(this.minY, yy);
    846                this.maxY = minIgnoreNaN(this.maxY, yy);
    847            }
    848        }
    849        else {
    850            item = (TimeSeriesDataItem) item.clone();
    851            this.data.add(-index - 1, item);
    852            updateBoundsForAddedItem(item);
    853
    854            // check if this addition will exceed the maximum item count...
    855            if (getItemCount() > this.maximumItemCount) {
    856                TimeSeriesDataItem d = (TimeSeriesDataItem) this.data.remove(0);
    857                updateBoundsForRemovedItem(d);
    858            }
    859        }
    860        removeAgedItems(false);  // remove old items if necessary, but
    861                                 // don't notify anyone, because that
    862                                 // happens next anyway...
    863        fireSeriesChanged();
    864        return overwritten;
    865
    866    }
    867
    868    /**
    869     * Age items in the series.  Ensure that the timespan from the youngest to
    870     * the oldest record in the series does not exceed maximumItemAge time
    871     * periods.  Oldest items will be removed if required.
    872     *
    873     * @param notify  controls whether or not a {@link SeriesChangeEvent} is
    874     *                sent to registered listeners IF any items are removed.
    875     */
    876    public void removeAgedItems(boolean notify) {
    877        // check if there are any values earlier than specified by the history
    878        // count...
    879        if (getItemCount() > 1) {
    880            long latest = getTimePeriod(getItemCount() - 1).getSerialIndex();
    881            boolean removed = false;
    882            while ((latest - getTimePeriod(0).getSerialIndex())
    883                    > this.maximumItemAge) {
    884                this.data.remove(0);
    885                removed = true;
    886            }
    887            if (removed) {
    888                findBoundsByIteration();
    889                if (notify) {
    890                    fireSeriesChanged();
    891                }
    892            }
    893        }
    894    }
    895
    896    /**
    897     * Age items in the series.  Ensure that the timespan from the supplied
    898     * time to the oldest record in the series does not exceed history count.
    899     * oldest items will be removed if required.
    900     *
    901     * @param latest  the time to be compared against when aging data
    902     *     (specified in milliseconds).
    903     * @param notify  controls whether or not a {@link SeriesChangeEvent} is
    904     *                sent to registered listeners IF any items are removed.
    905     */
    906    public void removeAgedItems(long latest, boolean notify) {
    907        if (this.data.isEmpty()) {
    908            return;  // nothing to do
    909        }
    910        // find the serial index of the period specified by 'latest'
    911        long index = Long.MAX_VALUE;
    912        try {
    913            Method m = RegularTimePeriod.class.getDeclaredMethod(
    914                    "createInstance", new Class[] {Class.class, Date.class,
    915                    TimeZone.class});
    916            RegularTimePeriod newest = (RegularTimePeriod) m.invoke(
    917                    this.timePeriodClass, new Object[] {this.timePeriodClass,
    918                            new Date(latest), TimeZone.getDefault()});
    919            index = newest.getSerialIndex();
    920        }
    921        catch (NoSuchMethodException e) {
    922            e.printStackTrace();
    923        }
    924        catch (IllegalAccessException e) {
    925            e.printStackTrace();
    926        }
    927        catch (InvocationTargetException e) {
    928            e.printStackTrace();
    929        }
    930
    931        // check if there are any values earlier than specified by the history
    932        // count...
    933        boolean removed = false;
    934        while (getItemCount() > 0 && (index
    935                - getTimePeriod(0).getSerialIndex()) > this.maximumItemAge) {
    936            this.data.remove(0);
    937            removed = true;
    938        }
    939        if (removed) {
    940            findBoundsByIteration();
    941            if (notify) {
    942                fireSeriesChanged();
    943            }
    944        }
    945    }
    946
    947    /**
    948     * Removes all data items from the series and sends a
    949     * {@link SeriesChangeEvent} to all registered listeners.
    950     */
    951    public void clear() {
    952        if (this.data.size() > 0) {
    953            this.data.clear();
    954            this.timePeriodClass = null;
    955            this.minY = Double.NaN;
    956            this.maxY = Double.NaN;
    957            fireSeriesChanged();
    958        }
    959    }
    960
    961    /**
    962     * Deletes the data item for the given time period and sends a
    963     * {@link SeriesChangeEvent} to all registered listeners.  If there is no
    964     * item with the specified time period, this method does nothing.
    965     *
    966     * @param period  the period of the item to delete (<code>null</code> not
    967     *                permitted).
    968     */
    969    public void delete(RegularTimePeriod period) {
    970        int index = getIndex(period);
    971        if (index >= 0) {
    972            TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.remove(
    973                    index);
    974            updateBoundsForRemovedItem(item);
    975            if (this.data.isEmpty()) {
    976                this.timePeriodClass = null;
    977            }
    978            fireSeriesChanged();
    979        }
    980    }
    981
    982    /**
    983     * Deletes data from start until end index (end inclusive).
    984     *
    985     * @param start  the index of the first period to delete.
    986     * @param end  the index of the last period to delete.
    987     */
    988    public void delete(int start, int end) {
    989        delete(start, end, true);
    990    }
    991
    992    /**
    993     * Deletes data from start until end index (end inclusive).
    994     *
    995     * @param start  the index of the first period to delete.
    996     * @param end  the index of the last period to delete.
    997     * @param notify  notify listeners?
    998     *
    999     * @since 1.0.14
    1000     */
    1001    public void delete(int start, int end, boolean notify) {
    1002        if (end < start) {
    1003            throw new IllegalArgumentException("Requires start <= end.");
    1004        }
    1005        for (int i = 0; i <= (end - start); i++) {
    1006            this.data.remove(start);
    1007        }
    1008        findBoundsByIteration();
    1009        if (this.data.isEmpty()) {
    1010            this.timePeriodClass = null;
    1011        }
    1012        if (notify) {
    1013            fireSeriesChanged();
    1014        }
    1015    }
    1016
    1017    /**
    1018     * Returns a clone of the time series.
    1019     * <P>
    1020     * Notes:
    1021     * <ul>
    1022     *   <li>no need to clone the domain and range descriptions, since String
    1023     *     object is immutable;</li>
    1024     *   <li>we pass over to the more general method clone(start, end).</li>
    1025     * </ul>
    1026     *
    1027     * @return A clone of the time series.
    1028     *
    1029     * @throws CloneNotSupportedException not thrown by this class, but
    1030     *         subclasses may differ.
    1031     */
    1032    public Object clone() throws CloneNotSupportedException {
    1033        TimeSeries clone = (TimeSeries) super.clone();
    1034        clone.data = (List) ObjectUtilities.deepClone(this.data);
    1035        return clone;
    1036    }
    1037
    1038    /**
    1039     * Creates a new timeseries by copying a subset of the data in this time
    1040     * series.
    1041     *
    1042     * @param start  the index of the first time period to copy.
    1043     * @param end  the index of the last time period to copy.
    1044     *
    1045     * @return A series containing a copy of this times series from start until
    1046     *         end.
    1047     *
    1048     * @throws CloneNotSupportedException if there is a cloning problem.
    1049     */
    1050    public TimeSeries createCopy(int start, int end)
    1051            throws CloneNotSupportedException {
    1052        if (start < 0) {
    1053            throw new IllegalArgumentException("Requires start >= 0.");
    1054        }
    1055        if (end < start) {
    1056            throw new IllegalArgumentException("Requires start <= end.");
    1057        }
    1058        TimeSeries copy = (TimeSeries) super.clone();
    1059        copy.minY = Double.NaN;
    1060        copy.maxY = Double.NaN;
    1061        copy.data = new java.util.ArrayList();
    1062        if (this.data.size() > 0) {
    1063            for (int index = start; index <= end; index++) {
    1064                TimeSeriesDataItem item
    1065                        = (TimeSeriesDataItem) this.data.get(index);
    1066                TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone();
    1067                try {
    1068                    copy.add(clone);
    1069                }
    1070                catch (SeriesException e) {
    1071                    e.printStackTrace();
    1072                }
    1073            }
    1074        }
    1075        return copy;
    1076    }
    1077
    1078    /**
    1079     * Creates a new timeseries by copying a subset of the data in this time
    1080     * series.
    1081     *
    1082     * @param start  the first time period to copy (<code>null</code> not
    1083     *         permitted).
    1084     * @param end  the last time period to copy (<code>null</code> not
    1085     *         permitted).
    1086     *
    1087     * @return A time series containing a copy of this time series from start
    1088     *         until end.
    1089     *
    1090     * @throws CloneNotSupportedException if there is a cloning problem.
    1091     */
    1092    public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end)
    1093        throws CloneNotSupportedException {
    1094
    1095        if (start == null) {
    1096            throw new IllegalArgumentException("Null 'start' argument.");
    1097        }
    1098        if (end == null) {
    1099            throw new IllegalArgumentException("Null 'end' argument.");
    1100        }
    1101        if (start.compareTo(end) > 0) {
    1102            throw new IllegalArgumentException(
    1103                    "Requires start on or before end.");
    1104        }
    1105        boolean emptyRange = false;
    1106        int startIndex = getIndex(start);
    1107        if (startIndex < 0) {
    1108            startIndex = -(startIndex + 1);
    1109            if (startIndex == this.data.size()) {
    1110                emptyRange = true;  // start is after last data item
    1111            }
    1112        }
    1113        int endIndex = getIndex(end);
    1114        if (endIndex < 0) {             // end period is not in original series
    1115            endIndex = -(endIndex + 1); // this is first item AFTER end period
    1116            endIndex = endIndex - 1;    // so this is last item BEFORE end
    1117        }
    1118        if ((endIndex < 0)  || (endIndex < startIndex)) {
    1119            emptyRange = true;
    1120        }
    1121        if (emptyRange) {
    1122            TimeSeries copy = (TimeSeries) super.clone();
    1123            copy.data = new java.util.ArrayList();
    1124            return copy;
    1125        }
    1126        return createCopy(startIndex, endIndex);
    1127    }
    1128
    1129    /**
    1130     * Tests the series for equality with an arbitrary object.
    1131     *
    1132     * @param obj  the object to test against (<code>null</code> permitted).
    1133     *
    1134     * @return A boolean.
    1135     */
    1136    public boolean equals(Object obj) {
    1137        if (obj == this) {
    1138            return true;
    1139        }
    1140        if (!(obj instanceof TimeSeries)) {
    1141            return false;
    1142        }
    1143        TimeSeries that = (TimeSeries) obj;
    1144        if (!ObjectUtilities.equal(getDomainDescription(),
    1145                that.getDomainDescription())) {
    1146            return false;
    1147        }
    1148        if (!ObjectUtilities.equal(getRangeDescription(),
    1149                that.getRangeDescription())) {
    1150            return false;
    1151        }
    1152        if (!ObjectUtilities.equal(this.timePeriodClass,
    1153                that.timePeriodClass)) {
    1154            return false;
    1155        }
    1156        if (getMaximumItemAge() != that.getMaximumItemAge()) {
    1157            return false;
    1158        }
    1159        if (getMaximumItemCount() != that.getMaximumItemCount()) {
    1160            return false;
    1161        }
    1162        int count = getItemCount();
    1163        if (count != that.getItemCount()) {
    1164            return false;
    1165        }
    1166        if (!ObjectUtilities.equal(this.data, that.data)) {
    1167            return false;
    1168        }
    1169        return super.equals(obj);
    1170    }
    1171
    1172    /**
    1173     * Returns a hash code value for the object.
    1174     *
    1175     * @return The hashcode
    1176     */
    1177    public int hashCode() {
    1178        int result = super.hashCode();
    1179        result = 29 * result + (this.domain != null ? this.domain.hashCode()
    1180                : 0);
    1181        result = 29 * result + (this.range != null ? this.range.hashCode() : 0);
    1182        result = 29 * result + (this.timePeriodClass != null
    1183                ? this.timePeriodClass.hashCode() : 0);
    1184        // it is too slow to look at every data item, so let's just look at
    1185        // the first, middle and last items...
    1186        int count = getItemCount();
    1187        if (count > 0) {
    1188            TimeSeriesDataItem item = getRawDataItem(0);
    1189            result = 29 * result + item.hashCode();
    1190        }
    1191        if (count > 1) {
    1192            TimeSeriesDataItem item = getRawDataItem(count - 1);
    1193            result = 29 * result + item.hashCode();
    1194        }
    1195        if (count > 2) {
    1196            TimeSeriesDataItem item = getRawDataItem(count / 2);
    1197            result = 29 * result + item.hashCode();
    1198        }
    1199        result = 29 * result + this.maximumItemCount;
    1200        result = 29 * result + (int) this.maximumItemAge;
    1201        return result;
    1202    }
    1203
    1204    /**
    1205     * Updates the cached values for the minimum and maximum data values.
    1206     *
    1207     * @param item  the item added (<code>null</code> not permitted).
    1208     *
    1209     * @since 1.0.14
    1210     */
    1211    private void updateBoundsForAddedItem(TimeSeriesDataItem item) {
    1212        Number yN = item.getValue();
    1213        if (item.getValue() != null) {
    1214            double y = yN.doubleValue();
    1215            this.minY = minIgnoreNaN(this.minY, y);
    1216            this.maxY = maxIgnoreNaN(this.maxY, y);
    1217        }
    1218    }
    1219
    1220    /**
    1221     * Updates the cached values for the minimum and maximum data values on
    1222     * the basis that the specified item has just been removed.
    1223     *
    1224     * @param item  the item added (<code>null</code> not permitted).
    1225     *
    1226     * @since 1.0.14
    1227     */
    1228    private void updateBoundsForRemovedItem(TimeSeriesDataItem item) {
    1229        Number yN = item.getValue();
    1230        if (yN != null) {
    1231            double y = yN.doubleValue();
    1232            if (!Double.isNaN(y)) {
    1233                if (y <= this.minY || y >= this.maxY) {
    1234                    findBoundsByIteration();
    1235                }
    1236            }
    1237        }
    1238    }
    1239
    1240    /**
    1241     * Finds the bounds of the x and y values for the series, by iterating
    1242     * through all the data items.
    1243     *
    1244     * @since 1.0.14
    1245     */
    1246    private void findBoundsByIteration() {
    1247        this.minY = Double.NaN;
    1248        this.maxY = Double.NaN;
    1249        Iterator iterator = this.data.iterator();
    1250        while (iterator.hasNext()) {
    1251            TimeSeriesDataItem item = (TimeSeriesDataItem) iterator.next();
    1252            updateBoundsForAddedItem(item);
    1253        }
    1254    }
    1255
    1256    /**
    1257     * A function to find the minimum of two values, but ignoring any
    1258     * Double.NaN values.
    1259     *
    1260     * @param a  the first value.
    1261     * @param b  the second value.
    1262     *
    1263     * @return The minimum of the two values.
    1264     */
    1265    private double minIgnoreNaN(double a, double b) {
    1266        if (Double.isNaN(a)) {
    1267            return b;
    1268        }
    1269        if (Double.isNaN(b)) {
    1270            return a;
    1271        }
    1272        return Math.min(a, b);
    1273    }
    1274
    1275    /**
    1276     * A function to find the maximum of two values, but ignoring any
    1277     * Double.NaN values.
    1278     *
    1279     * @param a  the first value.
    1280     * @param b  the second value.
    1281     *
    1282     * @return The maximum of the two values.
    1283     */
    1284    private double maxIgnoreNaN(double a, double b) {
    1285        if (Double.isNaN(a)) {
    1286            return b;
    1287        }
    1288        if (Double.isNaN(b)) {
    1289            return a;
    1290        }
    1291        else {
    1292            return Math.max(a, b);
    1293        }
    1294    }
    1295
    1296
    1297    /**
    1298     * Creates a new (empty) time series with the specified name and class
    1299     * of {@link RegularTimePeriod}.
    1300     *
    1301     * @param name  the series name (<code>null</code> not permitted).
    1302     * @param timePeriodClass  the type of time period (<code>null</code> not
    1303     *                         permitted).
    1304     *
    1305     * @deprecated As of 1.0.13, it is not necessary to specify the
    1306     *     <code>timePeriodClass</code> as this will be inferred when the
    1307     *     first data item is added to the dataset.
    1308     */
    1309    public TimeSeries(Comparable name, Class timePeriodClass) {
    1310        this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION,
    1311                timePeriodClass);
    1312    }
    1313
    1314    /**
    1315     * Creates a new time series that contains no data.
    1316     * <P>
    1317     * Descriptions can be specified for the domain and range.  One situation
    1318     * where this is helpful is when generating a chart for the time series -
    1319     * axis labels can be taken from the domain and range description.
    1320     *
    1321     * @param name  the name of the series (<code>null</code> not permitted).
    1322     * @param domain  the domain description (<code>null</code> permitted).
    1323     * @param range  the range description (<code>null</code> permitted).
    1324     * @param timePeriodClass  the type of time period (<code>null</code> not
    1325     *                         permitted).
    1326     *
    1327     * @deprecated As of 1.0.13, it is not necessary to specify the
    1328     *     <code>timePeriodClass</code> as this will be inferred when the
    1329     *     first data item is added to the dataset.
    1330     */
    1331    public TimeSeries(Comparable name, String domain, String range,
    1332                      Class timePeriodClass) {
    1333        super(name);
    1334        this.domain = domain;
    1335        this.range = range;
    1336        this.timePeriodClass = timePeriodClass;
    1337        this.data = new java.util.ArrayList();
    1338        this.maximumItemCount = Integer.MAX_VALUE;
    1339        this.maximumItemAge = Long.MAX_VALUE;
    1340        this.minY = Double.NaN;
    1341        this.maxY = Double.NaN;
    1342    }
    1343
    1344}