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