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    }