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