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