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