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    }