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