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