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