001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2011, 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     * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025     * Other names may be trademarks of their respective owners.]
026     *
027     * ---------------
028     * TimeSeries.java
029     * ---------------
030     * (C) Copyright 2001-2011, 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     * 26-May-2009 : Added cache for minY and maxY values (DG);
080     * 09-Jun-2009 : Ensure that TimeSeriesDataItem objects used in underlying
081     *               storage are cloned to keep series isolated from external
082     *               changes (DG);
083     * 10-Jun-2009 : Added addOrUpdate(TimeSeriesDataItem) method (DG);
084     * 31-Aug-2009 : Clear minY and maxY cache values in createCopy (DG);
085     * 
086     */
087    
088    package org.jfree.data.time;
089    
090    import java.io.Serializable;
091    import java.lang.reflect.InvocationTargetException;
092    import java.lang.reflect.Method;
093    import java.util.Collection;
094    import java.util.Collections;
095    import java.util.Date;
096    import java.util.Iterator;
097    import java.util.List;
098    import java.util.TimeZone;
099    
100    import org.jfree.data.general.Series;
101    import org.jfree.data.general.SeriesChangeEvent;
102    import org.jfree.data.general.SeriesException;
103    import org.jfree.util.ObjectUtilities;
104    
105    /**
106     * Represents a sequence of zero or more data items in the form (period, value)
107     * where 'period' is some instance of a subclass of {@link RegularTimePeriod}.
108     * The time series will ensure that (a) all data items have the same type of
109     * period (for example, {@link Day}) and (b) that each period appears at
110     * most one time in the series.
111     */
112    public class TimeSeries extends Series implements Cloneable, Serializable {
113    
114        /** For serialization. */
115        private static final long serialVersionUID = -5032960206869675528L;
116    
117        /** Default value for the domain description. */
118        protected static final String DEFAULT_DOMAIN_DESCRIPTION = "Time";
119    
120        /** Default value for the range description. */
121        protected static final String DEFAULT_RANGE_DESCRIPTION = "Value";
122    
123        /** A description of the domain. */
124        private String domain;
125    
126        /** A description of the range. */
127        private String range;
128    
129        /** The type of period for the data. */
130        protected Class timePeriodClass;
131    
132        /** The list of data items in the series. */
133        protected List data;
134    
135        /** The maximum number of items for the series. */
136        private int maximumItemCount;
137    
138        /**
139         * The maximum age of items for the series, specified as a number of
140         * time periods.
141         */
142        private long maximumItemAge;
143    
144        /**
145         * The minimum y-value in the series.
146         * 
147         * @since 1.0.14
148         */
149        private double minY;
150    
151        /**
152         * The maximum y-value in the series.
153         *
154         * @since 1.0.14
155         */
156        private double maxY;
157    
158        /**
159         * Creates a new (empty) time series.  By default, a daily time series is
160         * created.  Use one of the other constructors if you require a different
161         * time period.
162         *
163         * @param name  the series name (<code>null</code> not permitted).
164         */
165        public TimeSeries(Comparable name) {
166            this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION);
167        }
168    
169        /**
170         * Creates a new time series that contains no data.
171         * <P>
172         * Descriptions can be specified for the domain and range.  One situation
173         * where this is helpful is when generating a chart for the time series -
174         * axis labels can be taken from the domain and range description.
175         *
176         * @param name  the name of the series (<code>null</code> not permitted).
177         * @param domain  the domain description (<code>null</code> permitted).
178         * @param range  the range description (<code>null</code> permitted).
179         *
180         * @since 1.0.13
181         */
182        public TimeSeries(Comparable name, String domain, String range) {
183            super(name);
184            this.domain = domain;
185            this.range = range;
186            this.timePeriodClass = null;
187            this.data = new java.util.ArrayList();
188            this.maximumItemCount = Integer.MAX_VALUE;
189            this.maximumItemAge = Long.MAX_VALUE;
190            this.minY = Double.NaN;
191            this.maxY = Double.NaN;
192        }
193    
194        /**
195         * Returns the domain description.
196         *
197         * @return The domain description (possibly <code>null</code>).
198         *
199         * @see #setDomainDescription(String)
200         */
201        public String getDomainDescription() {
202            return this.domain;
203        }
204    
205        /**
206         * Sets the domain description and sends a <code>PropertyChangeEvent</code>
207         * (with the property name <code>Domain</code>) to all registered
208         * property change listeners.
209         *
210         * @param description  the description (<code>null</code> permitted).
211         *
212         * @see #getDomainDescription()
213         */
214        public void setDomainDescription(String description) {
215            String old = this.domain;
216            this.domain = description;
217            firePropertyChange("Domain", old, description);
218        }
219    
220        /**
221         * Returns the range description.
222         *
223         * @return The range description (possibly <code>null</code>).
224         *
225         * @see #setRangeDescription(String)
226         */
227        public String getRangeDescription() {
228            return this.range;
229        }
230    
231        /**
232         * Sets the range description and sends a <code>PropertyChangeEvent</code>
233         * (with the property name <code>Range</code>) to all registered listeners.
234         *
235         * @param description  the description (<code>null</code> permitted).
236         *
237         * @see #getRangeDescription()
238         */
239        public void setRangeDescription(String description) {
240            String old = this.range;
241            this.range = description;
242            firePropertyChange("Range", old, description);
243        }
244    
245        /**
246         * Returns the number of items in the series.
247         *
248         * @return The item count.
249         */
250        public int getItemCount() {
251            return this.data.size();
252        }
253    
254        /**
255         * Returns the list of data items for the series (the list contains
256         * {@link TimeSeriesDataItem} objects and is unmodifiable).
257         *
258         * @return The list of data items.
259         */
260        public List getItems() {
261            // FIXME: perhaps we should clone the data list
262            return Collections.unmodifiableList(this.data);
263        }
264    
265        /**
266         * Returns the maximum number of items that will be retained in the series.
267         * The default value is <code>Integer.MAX_VALUE</code>.
268         *
269         * @return The maximum item count.
270         *
271         * @see #setMaximumItemCount(int)
272         */
273        public int getMaximumItemCount() {
274            return this.maximumItemCount;
275        }
276    
277        /**
278         * Sets the maximum number of items that will be retained in the series.
279         * If you add a new item to the series such that the number of items will
280         * exceed the maximum item count, then the FIRST element in the series is
281         * automatically removed, ensuring that the maximum item count is not
282         * exceeded.
283         *
284         * @param maximum  the maximum (requires >= 0).
285         *
286         * @see #getMaximumItemCount()
287         */
288        public void setMaximumItemCount(int maximum) {
289            if (maximum < 0) {
290                throw new IllegalArgumentException("Negative 'maximum' argument.");
291            }
292            this.maximumItemCount = maximum;
293            int count = this.data.size();
294            if (count > maximum) {
295                delete(0, count - maximum - 1);
296            }
297        }
298    
299        /**
300         * Returns the maximum item age (in time periods) for the series.
301         *
302         * @return The maximum item age.
303         *
304         * @see #setMaximumItemAge(long)
305         */
306        public long getMaximumItemAge() {
307            return this.maximumItemAge;
308        }
309    
310        /**
311         * Sets the number of time units in the 'history' for the series.  This
312         * provides one mechanism for automatically dropping old data from the
313         * time series. For example, if a series contains daily data, you might set
314         * the history count to 30.  Then, when you add a new data item, all data
315         * items more than 30 days older than the latest value are automatically
316         * dropped from the series.
317         *
318         * @param periods  the number of time periods.
319         *
320         * @see #getMaximumItemAge()
321         */
322        public void setMaximumItemAge(long periods) {
323            if (periods < 0) {
324                throw new IllegalArgumentException("Negative 'periods' argument.");
325            }
326            this.maximumItemAge = periods;
327            removeAgedItems(true);  // remove old items and notify if necessary
328        }
329    
330        /**
331         * Returns the smallest y-value in the series, ignoring any null and
332         * Double.NaN values.  This method returns Double.NaN if there is no
333         * smallest y-value (for example, when the series is empty).
334         *
335         * @return The smallest y-value.
336         *
337         * @see #getMaxY()
338         *
339         * @since 1.0.14
340         */
341        public double getMinY() {
342            return this.minY;
343        }
344    
345        /**
346         * Returns the largest y-value in the series, ignoring any Double.NaN
347         * values.  This method returns Double.NaN if there is no largest y-value
348         * (for example, when the series is empty).
349         *
350         * @return The largest y-value.
351         *
352         * @see #getMinY()
353         *
354         * @since 1.0.14
355         */
356        public double getMaxY() {
357            return this.maxY;
358        }
359    
360        /**
361         * Returns the time period class for this series.
362         * <p>
363         * Only one time period class can be used within a single series (enforced).
364         * If you add a data item with a {@link Year} for the time period, then all
365         * subsequent data items must also have a {@link Year} for the time period.
366         *
367         * @return The time period class (may be <code>null</code> but only for
368         *     an empty series).
369         */
370        public Class getTimePeriodClass() {
371            return this.timePeriodClass;
372        }
373    
374        /**
375         * Returns a data item from the dataset.  Note that the returned object
376         * is a clone of the item in the series, so modifying it will have no 
377         * effect on the data series.
378         * 
379         * @param index  the item index.
380         * 
381         * @return The data item.
382         */
383        public TimeSeriesDataItem getDataItem(int index) {
384            TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(index);
385            return (TimeSeriesDataItem) item.clone();
386        }
387    
388        /**
389         * Returns the data item for a specific period.  Note that the returned
390         * object is a clone of the item in the series, so modifying it will have
391         * no effect on the data series.
392         *
393         * @param period  the period of interest (<code>null</code> not allowed).
394         *
395         * @return The data item matching the specified period (or
396         *         <code>null</code> if there is no match).
397         *
398         * @see #getDataItem(int)
399         */
400        public TimeSeriesDataItem getDataItem(RegularTimePeriod period) {
401            int index = getIndex(period);
402            if (index >= 0) {
403                return getDataItem(index);
404            }
405            return null;
406        }
407    
408        /**
409         * Returns a data item for the series.  This method returns the object
410         * that is used for the underlying storage - you should not modify the
411         * contents of the returned value unless you know what you are doing.
412         *
413         * @param index  the item index (zero-based).
414         *
415         * @return The data item.
416         *
417         * @see #getDataItem(int)
418         *
419         * @since 1.0.14
420         */
421        TimeSeriesDataItem getRawDataItem(int index) {
422            return (TimeSeriesDataItem) this.data.get(index);
423        }
424    
425        /**
426         * Returns a data item for the series.  This method returns the object
427         * that is used for the underlying storage - you should not modify the
428         * contents of the returned value unless you know what you are doing.
429         *
430         * @param period  the item index (zero-based).
431         *
432         * @return The data item.
433         *
434         * @see #getDataItem(RegularTimePeriod)
435         *
436         * @since 1.0.14
437         */
438        TimeSeriesDataItem getRawDataItem(RegularTimePeriod period) {
439            int index = getIndex(period);
440            if (index >= 0) {
441                return (TimeSeriesDataItem) this.data.get(index);
442            }
443            return null;
444        }
445    
446        /**
447         * Returns the time period at the specified index.
448         *
449         * @param index  the index of the data item.
450         *
451         * @return The time period.
452         */
453        public RegularTimePeriod getTimePeriod(int index) {
454            return getRawDataItem(index).getPeriod();
455        }
456    
457        /**
458         * Returns a time period that would be the next in sequence on the end of
459         * the time series.
460         *
461         * @return The next time period.
462         */
463        public RegularTimePeriod getNextTimePeriod() {
464            RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
465            return last.next();
466        }
467    
468        /**
469         * Returns a collection of all the time periods in the time series.
470         *
471         * @return A collection of all the time periods.
472         */
473        public Collection getTimePeriods() {
474            Collection result = new java.util.ArrayList();
475            for (int i = 0; i < getItemCount(); i++) {
476                result.add(getTimePeriod(i));
477            }
478            return result;
479        }
480    
481        /**
482         * Returns a collection of time periods in the specified series, but not in
483         * this series, and therefore unique to the specified series.
484         *
485         * @param series  the series to check against this one.
486         *
487         * @return The unique time periods.
488         */
489        public Collection getTimePeriodsUniqueToOtherSeries(TimeSeries series) {
490            Collection result = new java.util.ArrayList();
491            for (int i = 0; i < series.getItemCount(); i++) {
492                RegularTimePeriod period = series.getTimePeriod(i);
493                int index = getIndex(period);
494                if (index < 0) {
495                    result.add(period);
496                }
497            }
498            return result;
499        }
500    
501        /**
502         * Returns the index for the item (if any) that corresponds to a time
503         * period.
504         *
505         * @param period  the time period (<code>null</code> not permitted).
506         *
507         * @return The index.
508         */
509        public int getIndex(RegularTimePeriod period) {
510            if (period == null) {
511                throw new IllegalArgumentException("Null 'period' argument.");
512            }
513            TimeSeriesDataItem dummy = new TimeSeriesDataItem(
514                  period, Integer.MIN_VALUE);
515            return Collections.binarySearch(this.data, dummy);
516        }
517    
518        /**
519         * Returns the value at the specified index.
520         *
521         * @param index  index of a value.
522         *
523         * @return The value (possibly <code>null</code>).
524         */
525        public Number getValue(int index) {
526            return getRawDataItem(index).getValue();
527        }
528    
529        /**
530         * Returns the value for a time period.  If there is no data item with the
531         * specified period, this method will return <code>null</code>.
532         *
533         * @param period  time period (<code>null</code> not permitted).
534         *
535         * @return The value (possibly <code>null</code>).
536         */
537        public Number getValue(RegularTimePeriod period) {
538            int index = getIndex(period);
539            if (index >= 0) {
540                return getValue(index);
541            }
542            return null;
543        }
544    
545        /**
546         * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
547         * all registered listeners.
548         *
549         * @param item  the (timeperiod, value) pair (<code>null</code> not
550         *              permitted).
551         */
552        public void add(TimeSeriesDataItem item) {
553            add(item, true);
554        }
555    
556        /**
557         * Adds a data item to the series and sends a {@link SeriesChangeEvent} to
558         * all registered listeners.
559         *
560         * @param item  the (timeperiod, value) pair (<code>null</code> not
561         *              permitted).
562         * @param notify  notify listeners?
563         */
564        public void add(TimeSeriesDataItem item, boolean notify) {
565            if (item == null) {
566                throw new IllegalArgumentException("Null 'item' argument.");
567            }
568            item = (TimeSeriesDataItem) item.clone();
569            Class c = item.getPeriod().getClass();
570            if (this.timePeriodClass == null) {
571                this.timePeriodClass = c;
572            }
573            else if (!this.timePeriodClass.equals(c)) {
574                StringBuffer b = new StringBuffer();
575                b.append("You are trying to add data where the time period class ");
576                b.append("is ");
577                b.append(item.getPeriod().getClass().getName());
578                b.append(", but the TimeSeries is expecting an instance of ");
579                b.append(this.timePeriodClass.getName());
580                b.append(".");
581                throw new SeriesException(b.toString());
582            }
583    
584            // make the change (if it's not a duplicate time period)...
585            boolean added = false;
586            int count = getItemCount();
587            if (count == 0) {
588                this.data.add(item);
589                added = true;
590            }
591            else {
592                RegularTimePeriod last = getTimePeriod(getItemCount() - 1);
593                if (item.getPeriod().compareTo(last) > 0) {
594                    this.data.add(item);
595                    added = true;
596                }
597                else {
598                    int index = Collections.binarySearch(this.data, item);
599                    if (index < 0) {
600                        this.data.add(-index - 1, item);
601                        added = true;
602                    }
603                    else {
604                        StringBuffer b = new StringBuffer();
605                        b.append("You are attempting to add an observation for ");
606                        b.append("the time period ");
607                        b.append(item.getPeriod().toString());
608                        b.append(" but the series already contains an observation");
609                        b.append(" for that time period. Duplicates are not ");
610                        b.append("permitted.  Try using the addOrUpdate() method.");
611                        throw new SeriesException(b.toString());
612                    }
613                }
614            }
615            if (added) {
616                updateBoundsForAddedItem(item);
617                // check if this addition will exceed the maximum item count...
618                if (getItemCount() > this.maximumItemCount) {
619                    TimeSeriesDataItem d = (TimeSeriesDataItem) this.data.remove(0);
620                    updateBoundsForRemovedItem(d);
621                }
622    
623                removeAgedItems(false);  // remove old items if necessary, but
624                                         // don't notify anyone, because that
625                                         // happens next anyway...
626                if (notify) {
627                    fireSeriesChanged();
628                }
629            }
630    
631        }
632    
633        /**
634         * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
635         * to all registered listeners.
636         *
637         * @param period  the time period (<code>null</code> not permitted).
638         * @param value  the value.
639         */
640        public void add(RegularTimePeriod period, double value) {
641            // defer argument checking...
642            add(period, value, true);
643        }
644    
645        /**
646         * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
647         * to all registered listeners.
648         *
649         * @param period  the time period (<code>null</code> not permitted).
650         * @param value  the value.
651         * @param notify  notify listeners?
652         */
653        public void add(RegularTimePeriod period, double value, boolean notify) {
654            // defer argument checking...
655            TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
656            add(item, notify);
657        }
658    
659        /**
660         * Adds a new data item to the series and sends
661         * a {@link org.jfree.data.general.SeriesChangeEvent} to all registered
662         * listeners.
663         *
664         * @param period  the time period (<code>null</code> not permitted).
665         * @param value  the value (<code>null</code> permitted).
666         */
667        public void add(RegularTimePeriod period, Number value) {
668            // defer argument checking...
669            add(period, value, true);
670        }
671    
672        /**
673         * Adds a new data item to the series and sends a {@link SeriesChangeEvent}
674         * to all registered listeners.
675         *
676         * @param period  the time period (<code>null</code> not permitted).
677         * @param value  the value (<code>null</code> permitted).
678         * @param notify  notify listeners?
679         */
680        public void add(RegularTimePeriod period, Number value, boolean notify) {
681            // defer argument checking...
682            TimeSeriesDataItem item = new TimeSeriesDataItem(period, value);
683            add(item, notify);
684        }
685    
686        /**
687         * Updates (changes) the value for a time period.  Throws a
688         * {@link SeriesException} if the period does not exist.
689         *
690         * @param period  the period (<code>null</code> not permitted).
691         * @param value  the value.
692         * 
693         * @since 1.0.14
694         */
695        public void update(RegularTimePeriod period, double value) {
696          update(period, new Double(value));
697        }
698    
699        /**
700         * Updates (changes) the value for a time period.  Throws a
701         * {@link SeriesException} if the period does not exist.
702         *
703         * @param period  the period (<code>null</code> not permitted).
704         * @param value  the value (<code>null</code> permitted).
705         */
706        public void update(RegularTimePeriod period, Number value) {
707            TimeSeriesDataItem temp = new TimeSeriesDataItem(period, value);
708            int index = Collections.binarySearch(this.data, temp);
709            if (index < 0) {
710                throw new SeriesException("There is no existing value for the "
711                        + "specified 'period'.");
712            }
713            update(index, value);
714        }
715    
716        /**
717         * Updates (changes) the value of a data item.
718         *
719         * @param index  the index of the data item.
720         * @param value  the new value (<code>null</code> permitted).
721         */
722        public void update(int index, Number value) {
723            TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.get(index);
724            boolean iterate = false;
725            Number oldYN = item.getValue();
726            if (oldYN != null) {
727                double oldY = oldYN.doubleValue();
728                if (!Double.isNaN(oldY)) {
729                    iterate = oldY <= this.minY || oldY >= this.maxY;
730                }
731            }
732            item.setValue(value);
733            if (iterate) {
734                findBoundsByIteration();
735            }
736            else if (value != null) {
737                double yy = value.doubleValue();
738                this.minY = minIgnoreNaN(this.minY, yy);
739                this.maxY = maxIgnoreNaN(this.maxY, yy);
740            }
741            fireSeriesChanged();
742        }
743    
744        /**
745         * Adds or updates data from one series to another.  Returns another series
746         * containing the values that were overwritten.
747         *
748         * @param series  the series to merge with this.
749         *
750         * @return A series containing the values that were overwritten.
751         */
752        public TimeSeries addAndOrUpdate(TimeSeries series) {
753            TimeSeries overwritten = new TimeSeries("Overwritten values from: "
754                    + getKey());
755            for (int i = 0; i < series.getItemCount(); i++) {
756                TimeSeriesDataItem item = series.getRawDataItem(i);
757                TimeSeriesDataItem oldItem = addOrUpdate(item.getPeriod(),
758                        item.getValue());
759                if (oldItem != null) {
760                    overwritten.add(oldItem);
761                }
762            }
763            return overwritten;
764        }
765    
766        /**
767         * Adds or updates an item in the times series and sends a
768         * {@link SeriesChangeEvent} to all registered listeners.
769         *
770         * @param period  the time period to add/update (<code>null</code> not
771         *                permitted).
772         * @param value  the new value.
773         *
774         * @return A copy of the overwritten data item, or <code>null</code> if no
775         *         item was overwritten.
776         */
777        public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
778                                              double value) {
779            return addOrUpdate(period, new Double(value));
780        }
781    
782        /**
783         * Adds or updates an item in the times series and sends a
784         * {@link SeriesChangeEvent} to all registered listeners.
785         *
786         * @param period  the time period to add/update (<code>null</code> not
787         *                permitted).
788         * @param value  the new value (<code>null</code> permitted).
789         *
790         * @return A copy of the overwritten data item, or <code>null</code> if no
791         *         item was overwritten.
792         */
793        public TimeSeriesDataItem addOrUpdate(RegularTimePeriod period,
794                                              Number value) {
795            return addOrUpdate(new TimeSeriesDataItem(period, value));
796        }
797    
798        /**
799         * Adds or updates an item in the times series and sends a
800         * {@link SeriesChangeEvent} to all registered listeners.
801         *
802         * @param item  the data item (<code>null</code> not permitted).
803         *
804         * @return A copy of the overwritten data item, or <code>null</code> if no
805         *         item was overwritten.
806         *
807         * @since 1.0.14
808         */
809        public TimeSeriesDataItem addOrUpdate(TimeSeriesDataItem item) {
810    
811            if (item == null) {
812                throw new IllegalArgumentException("Null 'period' argument.");
813            }
814            Class periodClass = item.getPeriod().getClass();
815            if (this.timePeriodClass == null) {
816                this.timePeriodClass = periodClass;
817            }
818            else if (!this.timePeriodClass.equals(periodClass)) {
819                String msg = "You are trying to add data where the time "
820                        + "period class is " + periodClass.getName()
821                        + ", but the TimeSeries is expecting an instance of "
822                        + this.timePeriodClass.getName() + ".";
823                throw new SeriesException(msg);
824            }
825            TimeSeriesDataItem overwritten = null;
826            int index = Collections.binarySearch(this.data, item);
827            if (index >= 0) {
828                TimeSeriesDataItem existing
829                        = (TimeSeriesDataItem) this.data.get(index);
830                overwritten = (TimeSeriesDataItem) existing.clone();
831                // figure out if we need to iterate through all the y-values
832                // to find the revised minY / maxY
833                boolean iterate = false;
834                Number oldYN = existing.getValue();
835                double oldY = oldYN != null ? oldYN.doubleValue() : Double.NaN;
836                if (!Double.isNaN(oldY)) {
837                    iterate = oldY <= this.minY || oldY >= this.maxY;
838                }
839                existing.setValue(item.getValue());
840                if (iterate) {
841                    findBoundsByIteration();
842                }
843                else if (item.getValue() != null) {
844                    double yy = item.getValue().doubleValue();
845                    this.minY = minIgnoreNaN(this.minY, yy);
846                    this.maxY = minIgnoreNaN(this.maxY, yy);
847                }
848            }
849            else {
850                item = (TimeSeriesDataItem) item.clone();
851                this.data.add(-index - 1, item);
852                updateBoundsForAddedItem(item);
853    
854                // check if this addition will exceed the maximum item count...
855                if (getItemCount() > this.maximumItemCount) {
856                    TimeSeriesDataItem d = (TimeSeriesDataItem) this.data.remove(0);
857                    updateBoundsForRemovedItem(d);
858                }
859            }
860            removeAgedItems(false);  // remove old items if necessary, but
861                                     // don't notify anyone, because that
862                                     // happens next anyway...
863            fireSeriesChanged();
864            return overwritten;
865    
866        }
867    
868        /**
869         * Age items in the series.  Ensure that the timespan from the youngest to
870         * the oldest record in the series does not exceed maximumItemAge time
871         * periods.  Oldest items will be removed if required.
872         *
873         * @param notify  controls whether or not a {@link SeriesChangeEvent} is
874         *                sent to registered listeners IF any items are removed.
875         */
876        public void removeAgedItems(boolean notify) {
877            // check if there are any values earlier than specified by the history
878            // count...
879            if (getItemCount() > 1) {
880                long latest = getTimePeriod(getItemCount() - 1).getSerialIndex();
881                boolean removed = false;
882                while ((latest - getTimePeriod(0).getSerialIndex())
883                        > this.maximumItemAge) {
884                    this.data.remove(0);
885                    removed = true;
886                }
887                if (removed) {
888                    findBoundsByIteration();
889                    if (notify) {
890                        fireSeriesChanged();
891                    }
892                }
893            }
894        }
895    
896        /**
897         * Age items in the series.  Ensure that the timespan from the supplied
898         * time to the oldest record in the series does not exceed history count.
899         * oldest items will be removed if required.
900         *
901         * @param latest  the time to be compared against when aging data
902         *     (specified in milliseconds).
903         * @param notify  controls whether or not a {@link SeriesChangeEvent} is
904         *                sent to registered listeners IF any items are removed.
905         */
906        public void removeAgedItems(long latest, boolean notify) {
907            if (this.data.isEmpty()) {
908                return;  // nothing to do
909            }
910            // find the serial index of the period specified by 'latest'
911            long index = Long.MAX_VALUE;
912            try {
913                Method m = RegularTimePeriod.class.getDeclaredMethod(
914                        "createInstance", new Class[] {Class.class, Date.class,
915                        TimeZone.class});
916                RegularTimePeriod newest = (RegularTimePeriod) m.invoke(
917                        this.timePeriodClass, new Object[] {this.timePeriodClass,
918                                new Date(latest), TimeZone.getDefault()});
919                index = newest.getSerialIndex();
920            }
921            catch (NoSuchMethodException e) {
922                e.printStackTrace();
923            }
924            catch (IllegalAccessException e) {
925                e.printStackTrace();
926            }
927            catch (InvocationTargetException e) {
928                e.printStackTrace();
929            }
930    
931            // check if there are any values earlier than specified by the history
932            // count...
933            boolean removed = false;
934            while (getItemCount() > 0 && (index
935                    - getTimePeriod(0).getSerialIndex()) > this.maximumItemAge) {
936                this.data.remove(0);
937                removed = true;
938            }
939            if (removed) {
940                findBoundsByIteration();
941                if (notify) {
942                    fireSeriesChanged();
943                }
944            }
945        }
946    
947        /**
948         * Removes all data items from the series and sends a
949         * {@link SeriesChangeEvent} to all registered listeners.
950         */
951        public void clear() {
952            if (this.data.size() > 0) {
953                this.data.clear();
954                this.timePeriodClass = null;
955                this.minY = Double.NaN;
956                this.maxY = Double.NaN;
957                fireSeriesChanged();
958            }
959        }
960    
961        /**
962         * Deletes the data item for the given time period and sends a
963         * {@link SeriesChangeEvent} to all registered listeners.  If there is no
964         * item with the specified time period, this method does nothing.
965         *
966         * @param period  the period of the item to delete (<code>null</code> not
967         *                permitted).
968         */
969        public void delete(RegularTimePeriod period) {
970            int index = getIndex(period);
971            if (index >= 0) {
972                TimeSeriesDataItem item = (TimeSeriesDataItem) this.data.remove(
973                        index);
974                updateBoundsForRemovedItem(item);
975                if (this.data.isEmpty()) {
976                    this.timePeriodClass = null;
977                }
978                fireSeriesChanged();
979            }
980        }
981    
982        /**
983         * Deletes data from start until end index (end inclusive).
984         *
985         * @param start  the index of the first period to delete.
986         * @param end  the index of the last period to delete.
987         */
988        public void delete(int start, int end) {
989            delete(start, end, true);
990        }
991    
992        /**
993         * Deletes data from start until end index (end inclusive).
994         *
995         * @param start  the index of the first period to delete.
996         * @param end  the index of the last period to delete.
997         * @param notify  notify listeners?
998         *
999         * @since 1.0.14
1000         */
1001        public void delete(int start, int end, boolean notify) {
1002            if (end < start) {
1003                throw new IllegalArgumentException("Requires start <= end.");
1004            }
1005            for (int i = 0; i <= (end - start); i++) {
1006                this.data.remove(start);
1007            }
1008            findBoundsByIteration();
1009            if (this.data.isEmpty()) {
1010                this.timePeriodClass = null;
1011            }
1012            if (notify) {
1013                fireSeriesChanged();
1014            }
1015        }
1016    
1017        /**
1018         * Returns a clone of the time series.
1019         * <P>
1020         * Notes:
1021         * <ul>
1022         *   <li>no need to clone the domain and range descriptions, since String
1023         *     object is immutable;</li>
1024         *   <li>we pass over to the more general method clone(start, end).</li>
1025         * </ul>
1026         *
1027         * @return A clone of the time series.
1028         *
1029         * @throws CloneNotSupportedException not thrown by this class, but
1030         *         subclasses may differ.
1031         */
1032        public Object clone() throws CloneNotSupportedException {
1033            TimeSeries clone = (TimeSeries) super.clone();
1034            clone.data = (List) ObjectUtilities.deepClone(this.data);
1035            return clone;
1036        }
1037    
1038        /**
1039         * Creates a new timeseries by copying a subset of the data in this time
1040         * series.
1041         *
1042         * @param start  the index of the first time period to copy.
1043         * @param end  the index of the last time period to copy.
1044         *
1045         * @return A series containing a copy of this times series from start until
1046         *         end.
1047         *
1048         * @throws CloneNotSupportedException if there is a cloning problem.
1049         */
1050        public TimeSeries createCopy(int start, int end)
1051                throws CloneNotSupportedException {
1052            if (start < 0) {
1053                throw new IllegalArgumentException("Requires start >= 0.");
1054            }
1055            if (end < start) {
1056                throw new IllegalArgumentException("Requires start <= end.");
1057            }
1058            TimeSeries copy = (TimeSeries) super.clone();
1059            copy.minY = Double.NaN;
1060            copy.maxY = Double.NaN;
1061            copy.data = new java.util.ArrayList();
1062            if (this.data.size() > 0) {
1063                for (int index = start; index <= end; index++) {
1064                    TimeSeriesDataItem item
1065                            = (TimeSeriesDataItem) this.data.get(index);
1066                    TimeSeriesDataItem clone = (TimeSeriesDataItem) item.clone();
1067                    try {
1068                        copy.add(clone);
1069                    }
1070                    catch (SeriesException e) {
1071                        e.printStackTrace();
1072                    }
1073                }
1074            }
1075            return copy;
1076        }
1077    
1078        /**
1079         * Creates a new timeseries by copying a subset of the data in this time
1080         * series.
1081         *
1082         * @param start  the first time period to copy (<code>null</code> not
1083         *         permitted).
1084         * @param end  the last time period to copy (<code>null</code> not
1085         *         permitted).
1086         *
1087         * @return A time series containing a copy of this time series from start
1088         *         until end.
1089         *
1090         * @throws CloneNotSupportedException if there is a cloning problem.
1091         */
1092        public TimeSeries createCopy(RegularTimePeriod start, RegularTimePeriod end)
1093            throws CloneNotSupportedException {
1094    
1095            if (start == null) {
1096                throw new IllegalArgumentException("Null 'start' argument.");
1097            }
1098            if (end == null) {
1099                throw new IllegalArgumentException("Null 'end' argument.");
1100            }
1101            if (start.compareTo(end) > 0) {
1102                throw new IllegalArgumentException(
1103                        "Requires start on or before end.");
1104            }
1105            boolean emptyRange = false;
1106            int startIndex = getIndex(start);
1107            if (startIndex < 0) {
1108                startIndex = -(startIndex + 1);
1109                if (startIndex == this.data.size()) {
1110                    emptyRange = true;  // start is after last data item
1111                }
1112            }
1113            int endIndex = getIndex(end);
1114            if (endIndex < 0) {             // end period is not in original series
1115                endIndex = -(endIndex + 1); // this is first item AFTER end period
1116                endIndex = endIndex - 1;    // so this is last item BEFORE end
1117            }
1118            if ((endIndex < 0)  || (endIndex < startIndex)) {
1119                emptyRange = true;
1120            }
1121            if (emptyRange) {
1122                TimeSeries copy = (TimeSeries) super.clone();
1123                copy.data = new java.util.ArrayList();
1124                return copy;
1125            }
1126            return createCopy(startIndex, endIndex);
1127        }
1128    
1129        /**
1130         * Tests the series for equality with an arbitrary object.
1131         *
1132         * @param obj  the object to test against (<code>null</code> permitted).
1133         *
1134         * @return A boolean.
1135         */
1136        public boolean equals(Object obj) {
1137            if (obj == this) {
1138                return true;
1139            }
1140            if (!(obj instanceof TimeSeries)) {
1141                return false;
1142            }
1143            TimeSeries that = (TimeSeries) obj;
1144            if (!ObjectUtilities.equal(getDomainDescription(),
1145                    that.getDomainDescription())) {
1146                return false;
1147            }
1148            if (!ObjectUtilities.equal(getRangeDescription(),
1149                    that.getRangeDescription())) {
1150                return false;
1151            }
1152            if (!ObjectUtilities.equal(this.timePeriodClass,
1153                    that.timePeriodClass)) {
1154                return false;
1155            }
1156            if (getMaximumItemAge() != that.getMaximumItemAge()) {
1157                return false;
1158            }
1159            if (getMaximumItemCount() != that.getMaximumItemCount()) {
1160                return false;
1161            }
1162            int count = getItemCount();
1163            if (count != that.getItemCount()) {
1164                return false;
1165            }
1166            if (!ObjectUtilities.equal(this.data, that.data)) {
1167                return false;
1168            }
1169            return super.equals(obj);
1170        }
1171    
1172        /**
1173         * Returns a hash code value for the object.
1174         *
1175         * @return The hashcode
1176         */
1177        public int hashCode() {
1178            int result = super.hashCode();
1179            result = 29 * result + (this.domain != null ? this.domain.hashCode()
1180                    : 0);
1181            result = 29 * result + (this.range != null ? this.range.hashCode() : 0);
1182            result = 29 * result + (this.timePeriodClass != null
1183                    ? this.timePeriodClass.hashCode() : 0);
1184            // it is too slow to look at every data item, so let's just look at
1185            // the first, middle and last items...
1186            int count = getItemCount();
1187            if (count > 0) {
1188                TimeSeriesDataItem item = getRawDataItem(0);
1189                result = 29 * result + item.hashCode();
1190            }
1191            if (count > 1) {
1192                TimeSeriesDataItem item = getRawDataItem(count - 1);
1193                result = 29 * result + item.hashCode();
1194            }
1195            if (count > 2) {
1196                TimeSeriesDataItem item = getRawDataItem(count / 2);
1197                result = 29 * result + item.hashCode();
1198            }
1199            result = 29 * result + this.maximumItemCount;
1200            result = 29 * result + (int) this.maximumItemAge;
1201            return result;
1202        }
1203    
1204        /**
1205         * Updates the cached values for the minimum and maximum data values.
1206         *
1207         * @param item  the item added (<code>null</code> not permitted).
1208         *
1209         * @since 1.0.14
1210         */
1211        private void updateBoundsForAddedItem(TimeSeriesDataItem item) {
1212            Number yN = item.getValue();
1213            if (item.getValue() != null) {
1214                double y = yN.doubleValue();
1215                this.minY = minIgnoreNaN(this.minY, y);
1216                this.maxY = maxIgnoreNaN(this.maxY, y);
1217            }
1218        }
1219        
1220        /**
1221         * Updates the cached values for the minimum and maximum data values on
1222         * the basis that the specified item has just been removed.
1223         *
1224         * @param item  the item added (<code>null</code> not permitted).
1225         *
1226         * @since 1.0.14
1227         */
1228        private void updateBoundsForRemovedItem(TimeSeriesDataItem item) {
1229            Number yN = item.getValue();
1230            if (yN != null) {
1231                double y = yN.doubleValue();
1232                if (!Double.isNaN(y)) {
1233                    if (y <= this.minY || y >= this.maxY) {
1234                        findBoundsByIteration();
1235                    }
1236                }
1237            }
1238        }
1239    
1240        /**
1241         * Finds the bounds of the x and y values for the series, by iterating
1242         * through all the data items.
1243         *
1244         * @since 1.0.14
1245         */
1246        private void findBoundsByIteration() {
1247            this.minY = Double.NaN;
1248            this.maxY = Double.NaN;
1249            Iterator iterator = this.data.iterator();
1250            while (iterator.hasNext()) {
1251                TimeSeriesDataItem item = (TimeSeriesDataItem) iterator.next();
1252                updateBoundsForAddedItem(item);
1253            }
1254        }
1255    
1256        /**
1257         * A function to find the minimum of two values, but ignoring any
1258         * Double.NaN values.
1259         *
1260         * @param a  the first value.
1261         * @param b  the second value.
1262         *
1263         * @return The minimum of the two values.
1264         */
1265        private double minIgnoreNaN(double a, double b) {
1266            if (Double.isNaN(a)) {
1267                return b;
1268            }
1269            if (Double.isNaN(b)) {
1270                return a;
1271            }
1272            return Math.min(a, b);
1273        }
1274    
1275        /**
1276         * A function to find the maximum of two values, but ignoring any
1277         * Double.NaN values.
1278         *
1279         * @param a  the first value.
1280         * @param b  the second value.
1281         *
1282         * @return The maximum of the two values.
1283         */
1284        private double maxIgnoreNaN(double a, double b) {
1285            if (Double.isNaN(a)) {
1286                return b;
1287            }
1288            if (Double.isNaN(b)) {
1289                return a;
1290            }
1291            else {
1292                return Math.max(a, b);
1293            }
1294        }
1295    
1296    
1297        /**
1298         * Creates a new (empty) time series with the specified name and class
1299         * of {@link RegularTimePeriod}.
1300         *
1301         * @param name  the series name (<code>null</code> not permitted).
1302         * @param timePeriodClass  the type of time period (<code>null</code> not
1303         *                         permitted).
1304         *
1305         * @deprecated As of 1.0.13, it is not necessary to specify the
1306         *     <code>timePeriodClass</code> as this will be inferred when the
1307         *     first data item is added to the dataset.
1308         */
1309        public TimeSeries(Comparable name, Class timePeriodClass) {
1310            this(name, DEFAULT_DOMAIN_DESCRIPTION, DEFAULT_RANGE_DESCRIPTION,
1311                    timePeriodClass);
1312        }
1313    
1314        /**
1315         * Creates a new time series that contains no data.
1316         * <P>
1317         * Descriptions can be specified for the domain and range.  One situation
1318         * where this is helpful is when generating a chart for the time series -
1319         * axis labels can be taken from the domain and range description.
1320         *
1321         * @param name  the name of the series (<code>null</code> not permitted).
1322         * @param domain  the domain description (<code>null</code> permitted).
1323         * @param range  the range description (<code>null</code> permitted).
1324         * @param timePeriodClass  the type of time period (<code>null</code> not
1325         *                         permitted).
1326         *
1327         * @deprecated As of 1.0.13, it is not necessary to specify the
1328         *     <code>timePeriodClass</code> as this will be inferred when the
1329         *     first data item is added to the dataset.
1330         */
1331        public TimeSeries(Comparable name, String domain, String range,
1332                          Class timePeriodClass) {
1333            super(name);
1334            this.domain = domain;
1335            this.range = range;
1336            this.timePeriodClass = timePeriodClass;
1337            this.data = new java.util.ArrayList();
1338            this.maximumItemCount = Integer.MAX_VALUE;
1339            this.maximumItemAge = Long.MAX_VALUE;
1340            this.minY = Double.NaN;
1341            this.maxY = Double.NaN;
1342        }
1343    
1344    }