    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2008, 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     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    026     *
    027     * ---------------
    028     * TimeSeries.java
    029     * ---------------
    030     * (C) Copyright 2001-2008, 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     *
    078     */
    079
    080    package org.jfree.data.time;
    081
    082    import java.io.Serializable;
    083    import java.lang.reflect.InvocationTargetException;
    084    import java.lang.reflect.Method;
    085    import java.util.Collection;
    086    import java.util.Collections;
    087    import java.util.Date;
    088    import java.util.List;
    089    import java.util.TimeZone;
    090
    091    import org.jfree.data.general.Series;
    092    import org.jfree.data.general.SeriesChangeEvent;
    093    import org.jfree.data.general.SeriesException;
    094    import org.jfree.util.ObjectUtilities;
    095
    096    /**
    097     * Represents a sequence of zero or more data items in the form (period, value).
    098     */
    099    public class TimeSeries extends Series implements Cloneable, Serializable {
    100
    101        /** For serialization. */
    102        private static final long serialVersionUID = -5032960206869675528L;
    103
    104        /** Default value for the domain description. */
    105        protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time";
    106
    107        /** Default value for the range description. */
    108        protected static final String DEFAULT_RANGE_DESCRIPTION = "Value";
    109
    110        /** A description of the domain. */
    111        private String domain;
    112
    113        /** A description of the range. */
    114        private String range;
    115
    116        /** The type of period for the data. */
    117        protected Class timePeriodClass;
    118
    119        /** The list of data items in the series. */
    120        protected List data;
    121
    122        /** The maximum number of items for the series. */
    123        private int maximumItemCount;
    124
    125        /**
    126         * The maximum age of items for the series, specified as a number of
    127         * time periods.
    128         */
    129        private long maximumItemAge;
    130
    131        /**
    132         * Creates a new (empty) time series.  By default, a daily time series is
    133         * created.  Use one of the other constructors if you require a different
    134         * time period.
    135         *
    136         * @param name  the series name (<code>null</code> not permitted).
    137         */
    138        public TimeSeries(Comparable name) {
    139            this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION,
    140                    Day.class);
    141        }
    142
    143        /**
    144         * Creates a new (empty) time series with the specified name and class
    145         * of {@link RegularTimePeriod}.
    146         *
    147         * @param name  the series name (<code>null</code> not permitted).
    148         * @param timePeriodClass  the type of time period (<code>null</code> not
    149         *                         permitted).
    150         */
    151        public TimeSeries(Comparable name, Class timePeriodClass) {
    152            this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION,
    153                    timePeriodClass);
    154        }
    155
    156        /**
    157         * Creates a new time series that contains no data.
    158         * <P>
    159         * Descriptions can be specified for the domain and range.  One situation
    160         * where this is helpful is when generating a chart for the time series -
    161         * axis labels can be taken from the domain and range description.
    162         *
    163         * @param name  the name of the series (<code>null</code> not permitted).
    164         * @param domain  the domain description (<code>null</code> permitted).
    165         * @param range  the range description (<code>null</code> permitted).
    166         * @param timePeriodClass  the type of time period (<code>null</code> not
    167         *                         permitted).
    168         */
    169        public TimeSeries(Comparable name, String domain, String range,
    170                          Class timePeriodClass) {
    171            super(name);
    172            this.domain = domain;
    173            this.range = range;
    174            this.timePeriodClass = timePeriodClass;
    175            this.data = new java.util.ArrayList();
    176            this.maximumItemCount = Integer.MAX_VALUE;
    177            this.maximumItemAge = Long.MAX_VALUE;
    178        }
    179
    180        /**
    181         * Returns the domain description.
    182         *
    183         * @return The domain description (possibly <code>null</code>).
    184         *
    185         * @see #setDomainDescription(String)
    186         */
    187        public String getDomainDescription() {
    188            return this.domain;
    189        }
    190
    191        /**
    192         * Sets the domain description and sends a <code>PropertyChangeEvent</code>
    193         * (with the property name <code>Domain</code>) to all registered
    194         * property change listeners.
    195         *
    196         * @param description  the description (<code>null</code> permitted).
    197         *
    198         * @see #getDomainDescription()
    199         */
    200        public void setDomainDescription(String description) {
    201            String old = this.domain;
    202            this.domain = description;
    203            firePropertyChange("Domain", old, description);
    204        }
    205
    206        /**
    207         * Returns the range description.
    208         *
    209         * @return The range description (possibly <code>null</code>).
    210         *
    211         * @see #setRangeDescription(String)
    212         */
    213        public String getRangeDescription() {
    214            return this.range;
    215        }
    216
    217        /**
    218         * Sets the range description and sends a <code>PropertyChangeEvent</code>
    219         * (with the property name <code>Range</code>) to all registered listeners.
    220         *
    221         * @param description  the description (<code>null</code> permitted).
    222         *
    223         * @see #getRangeDescription()
    224         */
    225        public void setRangeDescription(String description) {
    226            String old = this.range;
    227            this.range = description;
    228            firePropertyChange("Range", old, description);
    229        }
    230
    231        /**
    232         * Returns the number of items in the series.
    233         *
    234         * @return The item count.
    235         */
    236        public int getItemCount() {
    237            return this.data.size();
    238        }
    239
    240        /**
    241         * Returns the list of data items for the series (the list contains
    242         * {@link TimeSeriesDataItem} objects and is unmodifiable).
    243         *
    244         * @return The list of data items.
    245         */
    246        public List getItems() {
    247            return Collections.unmodifiableList(this.data);
    248        }
    249
    250        /**
    251         * Returns the maximum number of items that will be retained in the series.
    252         * The default value is <code>Integer.MAX_VALUE</code>.
    253         *
    254         * @return The maximum item count.
    255         *
    256         * @see #setMaximumItemCount(int)
    257         */
    258        public int getMaximumItemCount() {
    259            return this.maximumItemCount;
    260        }
    261
    262        /**
    263         * Sets the maximum number of items that will be retained in the series.
    264         * If you add a new item to the series such that the number of items will
    265         * exceed the maximum item count, then the FIRST element in the series is
    266         * automatically removed, ensuring that the maximum item count is not
    267         * exceeded.
    268         *
    269         * @param maximum  the maximum (requires >= 0).
    270         *
    271         * @see #getMaximumItemCount()
    272         */
    273        public void setMaximumItemCount(int maximum) {
    274            if (maximum < 0) {
    275                throw new IllegalArgumentException("Negative 'maximum' argument.");
    276            }
    277            this.maximumItemCount = maximum;
    278            int count = this.data.size();
    279            if (count > maximum) {
    280                delete(0, count - maximum - 1);
    281            }
    282        }
    283
    284        /**
    285         * Returns the maximum item age (in time periods) for the series.
    286         *
    287         * @return The maximum item age.
    288         *
    289         * @see #setMaximumItemAge(long)
    290         */
    291        public long getMaximumItemAge() {
    292            return this.maximumItemAge;
    293        }
    294
    295        /**
    296         * Sets the number of time units in the 'history' for the series.  This
    297         * provides one mechanism for automatically dropping old data from the
    298         * time series. For example, if a series contains daily data, you might set
    299         * the history count to 30.  Then, when you add a new data item, all data
    300         * items more than 30 days older than the latest value are automatically
    301         * dropped from the series.
    302         *
    303         * @param periods  the number of time periods.
    304         *
    305         * @see #getMaximumItemAge()
    306         */
    307        public void setMaximumItemAge(long periods) {
    308            if (periods < 0) {
    309                throw new IllegalArgumentException("Negative 'periods' argument.");
    310            }
    311            this.maximumItemAge = periods;
    312            removeAgedItems(true);  // remove old items and notify if necessary
    313        }
    314
    315        /**
    316         * Returns the time period class for this series.
    317         * <p>
    318         * Only one time period class can be used within a single series (enforced).
    319         * If you add a data item with a {@link Year} for the time period, then all
    320         * subsequent data items must also have a {@link Year} for the time period.
    321         *
    322         * @return The time period class (never <code>null</code>).
    323         */
    324        public Class getTimePeriodClass() {
    325            return this.timePeriodClass;
    326        }
    327
    328        /**
    329         * Returns a data item for the series.
    330         *
    331         * @param index  the item index (zero-based).
    332         *
    333         * @return The data item.
    334         *
    335         * @see #getDataItem(RegularTimePeriod)
    336         */
    337        public TimeSeriesDataItem getDataItem(int index) {
    338            return (TimeSeriesDataItem) this.data.get(index);
    339        }
    340
    341        /**
    342         * Returns the data item for a specific period.
    343         *
    344         * @param period  the period of interest (<code>null</code> not allowed).
    345         *
    346         * @return The data item matching the specified period (or
    347         *         <code>null</code> if there is no match).
    348         *
    349         * @see #getDataItem(int)
    350         */
    351        public TimeSeriesDataItem getDataItem(RegularTimePeriod period) {
    352            int index = getIndex(period);
    353            if (index >= 0) {
    354                return (TimeSeriesDataItem) this.data.get(index);
    355            }
    356            else {
    357                return null;
    358            }
    359        }
    360
    361        /**
    362         * Returns the time period at the specified index.
    363         *
    364         * @param index  the index of the data item.
    365         *
    366         * @return The time period.
    367         */
    368        public RegularTimePeriod getTimePeriod(int index) {
    369            return getDataItem(index).getPeriod();
    370        }
    371
    372        /**
    373         * Returns a time period that would be the next in sequence on the end of
    374         * the time series.
    375         *
    376         * @return The next time period.
    377         */
    378        public RegularTimePeriod getNextTimePeriod() {
    379            RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
    380            return last.next();
    381        }
    382
    383        /**
    384         * Returns a collection of all the time periods in the time series.
    385         *
    386         * @return A collection of all the time periods.
    387         */
    388        public Collection getTimePeriods() {
    389            Collection result = new java.util.ArrayList();
    390            for (int i = 0; i < getItemCount(); i++) {
    391                result.add(getTimePeriod(i));
    392            }
    393            return result;
    394        }
    395
    396        /**
    397         * Returns a collection of time periods in the specified series, but not in
    398         * this series, and therefore unique to the specified series.
    399         *
    400         * @param series  the series to check against this one.
    401         *
    402         * @return The unique time periods.
    403         */
    404        public Collection getTimePeriodsUniqueToOtherSeries(TimeSeries series) {
    405
    406            Collection result = new java.util.ArrayList();
    407            for (int i = 0; i < series.getItemCount(); i++) {
    408                RegularTimePeriod period = series.getTimePeriod(i);
    409                int index = getIndex(period);
    410                if (index < 0) {
    411                    result.add(period);
    412                }
    413            }
    414            return result;
    415
    416        }
    417
    418        /**
    419         * Returns the index for the item (if any) that corresponds to a time
    420         * period.
    421         *
    422         * @param period  the time period (<code>null</code> not permitted).
    423         *
    424         * @return The index.
    425         */
    426        public int getIndex(RegularTimePeriod period) {
    427            if (period == null) {
    428                throw new IllegalArgumentException("Null 'period' argument.");
    429            }
    430            TimeSeriesDataItem dummy = new TimeSeriesDataItem(
    431                  period, Integer.MIN_VALUE);
    432            return Collections.binarySearch(this.data, dummy);
    433        }
    434
    435        /**
    436         * Returns the value at the specified index.
    437         *
    438         * @param index  index of a value.
    439         *
    440         * @return The value (possibly <code>null</code>).
    441         */
    442        public Number getValue(int index) {
    443            return getDataItem(index).getValue();
    444        }
    445
    446        /**
    447         * Returns the value for a time period.  If there is no data item with the
    448         * specified period, this method will return <code>null</code>.
    449         *
    450         * @param period  time period (<code>null</code> not permitted).
    451         *
    452         * @return The value (possibly <code>null</code>).
    453         */
    454        public Number getValue(RegularTimePeriod period) {
    455
    456            int index = getIndex(period);
    457            if (index >= 0) {
    458                return getValue(index);
    459            }
    460            else {
    461                return null;
    462            }
    463
    464        }
    465
    466        /**
    467         * Adds a data item to the series and sends a
    468         * {@link org.jfree.data.general.SeriesChangeEvent} to all registered
    469         * listeners.
    470         *
    471         * @param item  the (timeperiod, value) pair (<code>null</code> not
    472         *              permitted).
    473         */
    474        public void add(TimeSeriesDataItem item) {
    475            add(item, true);
    476        }
    477
    478        /**
    479         * Adds a data item to the series and sends a
    480         * {@link org.jfree.data.general.SeriesChangeEvent} to all registered
    481         * listeners.
    482         *
    483         * @param item  the (timeperiod, value) pair (<code>null</code> not
    484         *              permitted).
    485         * @param notify  notify listeners?
    486         */
    487        public void add(TimeSeriesDataItem item, boolean notify) {
    488            if (item == null) {
    489                throw new IllegalArgumentException("Null 'item' argument.");
    490            }
    491            if (!item.getPeriod().getClass().equals(this.timePeriodClass)) {
    492                StringBuffer b = new StringBuffer();
    493                b.append("You are trying to add data where the time period class ");
    494                b.append("is ");
    495                b.append(item.getPeriod().getClass().getName());
    496                b.append(", but the TimeSeries is expecting an instance of ");
    497                b.append(this.timePeriodClass.getName());
    498                b.append(".");
    499                throw new SeriesException(b.toString());
    500            }
    501
    502            // make the change (if it's not a duplicate time period)...
    503            boolean added = false;
    504            int count = getItemCount();
    505            if (count == 0) {
    506                this.data.add(item);
    507                added = true;
    508            }
    509            else {
    510                RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
    511                if (item.getPeriod().compareTo(last) > 0) {
    512                    this.data.add(item);
    513                    added = true;
    514                }
    515                else {
    516                    int index = Collections.binarySearch(this.data, item);
    517                    if (index < 0) {
    518                        this.data.add(-index - 1, item);
    519                        added = true;
    520                    }
    521                    else {
    522                        StringBuffer b = new StringBuffer();
    523                        b.append("You are attempting to add an observation for ");
    524                        b.append("the time period ");
    525                        b.append(item.getPeriod().toString());
    526                        b.append(" but the series already contains an observation");
    527                        b.append(" for that time period. Duplicates are not ");
    528                        b.append("permitted.  Try using the addOrUpdate() method.");
    529                        throw new SeriesException(b.toString());
    530                    }
    531                }
    532            }
    533            if (added) {
    534                // check if this addition will exceed the maximum item count...
    535                if (getItemCount() > this.maximumItemCount) {
    536                    this.data.remove(0);
    537                }
    538
    539                removeAgedItems(false);  // remove old items if necessary, but
    540                                         // don't notify anyone, because that
    541                                         // happens next anyway...
    542                if (notify) {
    543                    fireSeriesChanged();
    544                }
    545            }
    546
    547        }
    548
    549        /**
    550         * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
    551         * to all registered listeners.
    552         *
    553         * @param period  the time period (<code>null</code> not permitted).
    554         * @param value  the value.
    555         */
    556        public void add(RegularTimePeriod period, double value) {
    557            // defer argument checking...
    558            add(period, value, true);
    559        }
    560
    561        /**
    562         * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
    563         * to all registered listeners.
    564         *
    565         * @param period  the time period (<code>null</code> not permitted).
    566         * @param value  the value.
    567         * @param notify  notify listeners?
    568         */
    569        public void add(RegularTimePeriod period, double value, boolean notify) {
    570            // defer argument checking...
    571            TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
    572            add(item, notify);
    573        }
    574
    575        /**
    576         * Adds a new data item to the series and sends
    577         * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered
    578         * listeners.
    579         *
    580         * @param period  the time period (<code>null</code> not permitted).
    581         * @param value  the value (<code>null</code> permitted).
    582         */
    583        public void add(RegularTimePeriod period, Number value) {
    584            // defer argument checking...
    585            add(period, value, true);
    586        }
    587
    588        /**
    589         * Adds a new data item to the series and sends
    590         * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered
    591         * listeners.
    592         *
    593         * @param period  the time period (<code>null</code> not permitted).
    594         * @param value  the value (<code>null</code> permitted).
    595         * @param notify  notify listeners?
    596         */
    597        public void add(RegularTimePeriod period, Number value, boolean notify) {
    598            // defer argument checking...
    599            TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
    600            add(item, notify);
    601        }
    602
    603        /**
    604         * Updates (changes) the value for a time period.  Throws a
    605         * {@link SeriesException} if the period does not exist.
    606         *
    607         * @param period  the period (<code>null</code> not permitted).
    608         * @param value  the value (<code>null</code> permitted).
    609         */
    610        public void update(RegularTimePeriod period, Number value) {
    611            TimeSeriesDataItem temp = new TimeSeriesDataItem(period, value);
    612            int index = Collections.binarySearch(this.data, temp);
    613            if (index >= 0) {
    614                TimeSeriesDataItem pair = (TimeSeriesDataItem) this.data.get(index);
    615                pair.setValue(value);
    616                fireSeriesChanged();
    617            }
    618            else {
    619                throw new SeriesException(
    620                    "TimeSeries.update(TimePeriod, Number):  period does not exist."
    621                );
    622            }
    623
    624        }
    625
    626        /**
    627         * Updates (changes) the value of a data item.
    628         *
    629         * @param index  the index of the data item.
    630         * @param value  the new value (<code>null</code> permitted).
    631         */
    632        public void update(int index, Number value) {
    633            TimeSeriesDataItem item = getDataItem(index);
    634            item.setValue(value);
    635            fireSeriesChanged();
    636        }
    637
    638        /**
    639         * Adds or updates data from one series to another.  Returns another series
    640         * containing the values that were overwritten.
    641         *
    642         * @param series  the series to merge with this.
    643         *
    644         * @return A series containing the values that were overwritten.
    645         */
    646        public TimeSeries addAndOrUpdate(TimeSeries series) {
    647            TimeSeries overwritten = new TimeSeries("Overwritten values from: "
    648                    + getKey(), series.getTimePeriodClass());
    649            for (int i = 0; i < series.getItemCount(); i++) {
    650                TimeSeriesDataItem item = series.getDataItem(i);
    651                TimeSeriesDataItem oldItem = addOrUpdate(item.getPeriod(),
    652                        item.getValue());
    653                if (oldItem != null) {
    654                    overwritten.add(oldItem);
    655                }
    656            }
    657            return overwritten;
    658        }
    659
    660        /**
    661         * Adds or updates an item in the times series and sends a
    662         * {@link org.jfree.data.general.SeriesChangeEvent} to all registered
    663         * listeners.
    664         *
    665         * @param period  the time period to add/update (<code>null</code> not
    666         *                permitted).
    667         * @param value  the new value.
    668         *
    669         * @return A copy of the overwritten data item, or <code>null</code> if no
    670         *         item was overwritten.
    671         */
    672        public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
    673                                              double value) {
    674            return addOrUpdate(period, new Double(value));
    675        }
    676
    677        /**
    678         * Adds or updates an item in the times series and sends a
    679         * {@link org.jfree.data.general.SeriesChangeEvent} to all registered
    680         * listeners.
    681         *
    682         * @param period  the time period to add/update (<code>null</code> not
    683         *                permitted).
    684         * @param value  the new value (<code>null</code> permitted).
    685         *
    686         * @return A copy of the overwritten data item, or <code>null</code> if no
    687         *         item was overwritten.
    688         */
    689        public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
    690                                              Number value) {
    691
    692            if (period == null) {
    693                throw new IllegalArgumentException("Null 'period' argument.");
    694            }
    695            TimeSeriesDataItem overwritten = null;
    696
    697            TimeSeriesDataItem key = new TimeSeriesDataItem(period, value);
    698            int index = Collections.binarySearch(this.data, key);
    699            if (index >= 0) {
    700                TimeSeriesDataItem existing
    701                    = (TimeSeriesDataItem) this.data.get(index);
    702                overwritten = (TimeSeriesDataItem) existing.clone();
    703                existing.setValue(value);
    704                removeAgedItems(false);  // remove old items if necessary, but
    705                                         // don't notify anyone, because that
    706                                         // happens next anyway...
    707                fireSeriesChanged();
    708            }
    709            else {
    710                this.data.add(-index - 1, new TimeSeriesDataItem(period, value));
    711
    712                // check if this addition will exceed the maximum item count...
    713                if (getItemCount() > this.maximumItemCount) {
    714                    this.data.remove(0);
    715                }
    716
    717                removeAgedItems(false);  // remove old items if necessary, but
    718                                         // don't notify anyone, because that
    719                                         // happens next anyway...
    720                fireSeriesChanged();
    721            }
    722            return overwritten;
    723
    724        }
    725
    726        /**
    727         * Age items in the series.  Ensure that the timespan from the youngest to
    728         * the oldest record in the series does not exceed maximumItemAge time
    729         * periods.  Oldest items will be removed if required.
    730         *
    731         * @param notify  controls whether or not a {@link SeriesChangeEvent} is
    732         *                sent to registered listeners IF any items are removed.
    733         */
    734        public void removeAgedItems(boolean notify) {
    735            // check if there are any values earlier than specified by the history
    736            // count...
    737            if (getItemCount() > 1) {
    738                long latest = getTimePeriod(getItemCount() - 1).getSerialIndex();
    739                boolean removed = false;
    740                while ((latest - getTimePeriod(0).getSerialIndex())
    741                        > this.maximumItemAge) {
    742                    this.data.remove(0);
    743                    removed = true;
    744                }
    745                if (removed && notify) {
    746                    fireSeriesChanged();
    747                }
    748            }
    749        }
    750
    751        /**
    752         * Age items in the series.  Ensure that the timespan from the supplied
    753         * time to the oldest record in the series does not exceed history count.
    754         * oldest items will be removed if required.
    755         *
    756         * @param latest  the time to be compared against when aging data
    757         *     (specified in milliseconds).
    758         * @param notify  controls whether or not a {@link SeriesChangeEvent} is
    759         *                sent to registered listeners IF any items are removed.
    760         */
    761        public void removeAgedItems(long latest, boolean notify) {
    762
    763            // find the serial index of the period specified by 'latest'
    764            long index = Long.MAX_VALUE;
    765            try {
    766                Method m = RegularTimePeriod.class.getDeclaredMethod(
    767                        "createInstance", new Class[] {Class.class, Date.class,
    768                        TimeZone.class});
    769                RegularTimePeriod newest = (RegularTimePeriod) m.invoke(
    770                        this.timePeriodClass, new Object[] {this.timePeriodClass,
    771                                new Date(latest), TimeZone.getDefault()});
    772                index = newest.getSerialIndex();
    773            }
    774            catch (NoSuchMethodException e) {
    775                e.printStackTrace();
    776            }
    777            catch (IllegalAccessException e) {
    778                e.printStackTrace();
    779            }
    780            catch (InvocationTargetException e) {
    781                e.printStackTrace();
    782            }
    783
    784            // check if there are any values earlier than specified by the history
    785            // count...
    786            boolean removed = false;
    787            while (getItemCount() > 0 && (index
    788                    - getTimePeriod(0).getSerialIndex()) > this.maximumItemAge) {
    789                this.data.remove(0);
    790                removed = true;
    791            }
    792            if (removed && notify) {
    793                fireSeriesChanged();
    794            }
    795        }
    796
    797        /**
    798         * Removes all data items from the series and sends a
    799         * {@link SeriesChangeEvent} to all registered listeners.
    800         */
    801        public void clear() {
    802            if (this.data.size() > 0) {
    803                this.data.clear();
    804                fireSeriesChanged();
    805            }
    806        }
    807
    808        /**
    809         * Deletes the data item for the given time period and sends a
    810         * {@link SeriesChangeEvent} to all registered listeners.  If there is no
    811         * item with the specified time period, this method does nothing.
    812         *
    813         * @param period  the period of the item to delete (<code>null</code> not
    814         *                permitted).
    815         */
    816        public void delete(RegularTimePeriod period) {
    817            int index = getIndex(period);
    818            if (index >= 0) {
    819                this.data.remove(index);
    820                fireSeriesChanged();
    821            }
    822        }
    823
    824        /**
    825         * Deletes data from start until end index (end inclusive).
    826         *
    827         * @param start  the index of the first period to delete.
    828         * @param end  the index of the last period to delete.
    829         */
    830        public void delete(int start, int end) {
    831            if (end < start) {
    832                throw new IllegalArgumentException("Requires start <= end.");
    833            }
    834            for (int i = 0; i <= (end - start); i++) {
    835                this.data.remove(start);
    836            }
    837            fireSeriesChanged();
    838        }
    839
    840        /**
    841         * Returns a clone of the time series.
    842         * <P>
    843         * Notes:
    844         * <ul>
    845         *   <li>no need to clone the domain and range descriptions, since String
    846         *     object is immutable;</li>
    847         *   <li>we pass over to the more general method clone(start, end).</li>
    848         * </ul>
    849         *
    850         * @return A clone of the time series.
    851         *
    852         * @throws CloneNotSupportedException not thrown by this class, but
    853         *         subclasses may differ.
    854         */
    855        public Object clone() throws CloneNotSupportedException {
    856            TimeSeries clone = (TimeSeries) super.clone();
    857            clone.data = (List) ObjectUtilities.deepClone(this.data);
    858            return clone;
    859        }
    860
    861        /**
    862         * Creates a new timeseries by copying a subset of the data in this time
    863         * series.
    864         *
    865         * @param start  the index of the first time period to copy.
    866         * @param end  the index of the last time period to copy.
    867         *
    868         * @return A series containing a copy of this times series from start until
    869         *         end.
    870         *
    871         * @throws CloneNotSupportedException if there is a cloning problem.
    872         */
    873        public TimeSeries createCopy(int start, int end)
    874            throws CloneNotSupportedException {
    875
    876            if (start < 0) {
    877                throw new IllegalArgumentException("Requires start >= 0.");
    878            }
    879            if (end < start) {
    880                throw new IllegalArgumentException("Requires start <= end.");
    881            }
    882            TimeSeries copy = (TimeSeries) super.clone();
    883
    884            copy.data = new java.util.ArrayList();
    885            if (this.data.size() > 0) {
    886                for (int index = start; index <= end; index++) {
    887                    TimeSeriesDataItem item
    888                        = (TimeSeriesDataItem) this.data.get(index);
    889                    TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone();
    890                    try {
    891                        copy.add(clone);
    892                    }
    893                    catch (SeriesException e) {
    894                        e.printStackTrace();
    895                    }
    896                }
    897            }
    898            return copy;
    899        }
    900
    901        /**
    902         * Creates a new timeseries by copying a subset of the data in this time
    903         * series.
    904         *
    905         * @param start  the first time period to copy (<code>null</code> not
    906         *         permitted).
    907         * @param end  the last time period to copy (<code>null</code> not
    908         *         permitted).
    909         *
    910         * @return A time series containing a copy of this time series from start
    911         *         until end.
    912         *
    913         * @throws CloneNotSupportedException if there is a cloning problem.
    914         */
    915        public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end)
    916            throws CloneNotSupportedException {
    917
    918            if (start == null) {
    919                throw new IllegalArgumentException("Null 'start' argument.");
    920            }
    921            if (end == null) {
    922                throw new IllegalArgumentException("Null 'end' argument.");
    923            }
    924            if (start.compareTo(end) > 0) {
    925                throw new IllegalArgumentException(
    926                        "Requires start on or before end.");
    927            }
    928            boolean emptyRange = false;
    929            int startIndex = getIndex(start);
    930            if (startIndex < 0) {
    931                startIndex = -(startIndex + 1);
    932                if (startIndex == this.data.size()) {
    933                    emptyRange = true;  // start is after last data item
    934                }
    935            }
    936            int endIndex = getIndex(end);
    937            if (endIndex < 0) {             // end period is not in original series
    938                endIndex = -(endIndex + 1); // this is first item AFTER end period
    939                endIndex = endIndex - 1;    // so this is last item BEFORE end
    940            }
    941            if ((endIndex < 0)  || (endIndex < startIndex)) {
    942                emptyRange = true;
    943            }
    944            if (emptyRange) {
    945                TimeSeries copy = (TimeSeries) super.clone();
    946                copy.data = new java.util.ArrayList();
    947                return copy;
    948            }
    949            else {
    950                return createCopy(startIndex, endIndex);
    951            }
    952
    953        }
    954
    955        /**
    956         * Tests the series for equality with an arbitrary object.
    957         *
    958         * @param object  the object to test against (<code>null</code> permitted).
    959         *
    960         * @return A boolean.
    961         */
    962        public boolean equals(Object object) {
    963            if (object == this) {
    964                return true;
    965            }
    966            if (!(object instanceof TimeSeries) || !super.equals(object)) {
    967                return false;
    968            }
    969            TimeSeries s = (TimeSeries) object;
    970            if (!ObjectUtilities.equal(getDomainDescription(),
    971                    s.getDomainDescription())) {
    972                return false;
    973            }
    974
    975            if (!ObjectUtilities.equal(getRangeDescription(),
    976                    s.getRangeDescription())) {
    977                return false;
    978            }
    979
    980            if (!getClass().equals(s.getClass())) {
    981                return false;
    982            }
    983
    984            if (getMaximumItemAge() != s.getMaximumItemAge()) {
    985                return false;
    986            }
    987
    988            if (getMaximumItemCount() != s.getMaximumItemCount()) {
    989                return false;
    990            }
    991
    992            int count = getItemCount();
    993            if (count != s.getItemCount()) {
    994                return false;
    995            }
    996            for (int i = 0; i < count; i++) {
    997                if (!getDataItem(i).equals(s.getDataItem(i))) {
    998                    return false;
    999                }
    1000            }
    1001            return true;
    1002        }
    1003
    1004        /**
    1005         * Returns a hash code value for the object.
    1006         *
    1007         * @return The hashcode
    1008         */
    1009        public int hashCode() {
    1010            int result = super.hashCode();
    1011            result = 29 * result + (this.domain != null ? this.domain.hashCode()
    1012                    : 0);
    1013            result = 29 * result + (this.range != null ? this.range.hashCode() : 0);
    1014            result = 29 * result + (this.timePeriodClass != null
    1015                    ? this.timePeriodClass.hashCode() : 0);
    1016            // it is too slow to look at every data item, so let's just look at
    1017            // the first, middle and last items...
    1018            int count = getItemCount();
    1019            if (count > 0) {
    1020                TimeSeriesDataItem item = getDataItem(0);
    1021                result = 29 * result + item.hashCode();
    1022            }
    1023            if (count > 1) {
    1024                TimeSeriesDataItem item = getDataItem(count - 1);
    1025                result = 29 * result + item.hashCode();
    1026            }
    1027            if (count > 2) {
    1028                TimeSeriesDataItem item = getDataItem(count / 2);
    1029                result = 29 * result + item.hashCode();
    1030            }
    1031            result = 29 * result + this.maximumItemCount;
    1032            result = 29 * result + (int) this.maximumItemAge;
    1033            return result;
    1034        }
    1035
    1036    }