View Javadoc
1   package org.djutils.stats.summarizers;
2   
3   import java.util.Calendar;
4   
5   import org.djutils.exceptions.Throw;
6   
7   /**
8    * The TimestampWeightedTally class defines a time-weighted tally based on timestamped data. The difference with a normal
9    * time-weighed tally is that the weight of a value is only known at the occurrence of the next timestamp. Furthermore, a last
10   * timestamp needs to be specified to determine the weight of the last value. Timestamps can be Number based or Calendar based.
11   * <p>
12   * Copyright (c) 2020-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
13   * for project information <a href="https://simulation.tudelft.nl/" target="_blank"> https://simulation.tudelft.nl</a>. The DSOL
14   * project is distributed under a three-clause BSD-style license, which can be found at
15   * <a href="https://simulation.tudelft.nl/dsol/3.0/license.html" target="_blank">
16   * https://simulation.tudelft.nl/dsol/3.0/license.html</a>.
17   * </p>
18   * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank"> Alexander Verbraeck</a>
19   */
20  public class TimestampWeightedTally extends WeightedTally
21  {
22      /** */
23      private static final long serialVersionUID = 20200228L;
24  
25      /** startTime defines the time of the first observation. Often equals to 0.0, but can also have other value. */
26      private double startTime;
27  
28      /** lastTimestamp stores the time of the last observation. Stored separately to avoid ulp rounding errors and allow ==. */
29      private double lastTimestamp;
30  
31      /** lastValue tracks the last value. */
32      private double lastValue;
33  
34      /** indicate whether the statistic is active or not (false before first event and after last event). */
35      private boolean active;
36  
37      /**
38       * constructs a new TimestampWeightedTally with a description.
39       * @param description String; the description of this TimestampWeightedTally
40       */
41      public TimestampWeightedTally(final String description)
42      {
43          super(description);
44      }
45  
46      @Override
47      public void initialize()
48      {
49          synchronized (super.semaphore)
50          {
51              super.initialize();
52              this.startTime = Double.NaN;
53              this.lastTimestamp = Double.NaN;
54              this.lastValue = 0.0;
55              this.active = true;
56          }
57      }
58  
59      /**
60       * Return whether the statistic is active (accepting observations) or not.
61       * @return boolean; whether the statistic is active (accepting observations) or not
62       */
63      public boolean isActive()
64      {
65          return this.active;
66      }
67  
68      /**
69       * End the observations and closes the last interval of observations. After ending, no more observations will be accepted.
70       * Calling this method will create an extra observation, and corresponding events for the EventBased implementations of this
71       * interface will be called.
72       * @param timestamp Number; the Number object representing the final timestamp
73       */
74      public void endObservations(final Number timestamp)
75      {
76          register(timestamp, this.lastValue);
77          this.active = false;
78      }
79  
80      /**
81       * End the observations and closes the last interval of observations. After ending, no more observations will be accepted.
82       * Calling this method will create an extra observation, and corresponding events for the EventBased implementations of this
83       * interface will be called.
84       * @param timestamp Calendar; the Calendar object representing the final timestamp
85       */
86      public void endObservations(final Calendar timestamp)
87      {
88          register(timestamp, this.lastValue);
89          this.active = false;
90      }
91  
92      /**
93       * Return the last observed value.
94       * @return double; the last observed value
95       */
96      public double getLastValue()
97      {
98          return this.lastValue;
99      }
100 
101     /**
102      * Process one observed Calender-based value. The time used will be the Calendar's time in milliseconds. Silently ignore
103      * when a value is registered, but tally is not active, i.e. when endObservations() has been called.
104      * @param timestamp Calendar; the Calendar object representing the timestamp
105      * @param value double; the value to process
106      * @return double; the value
107      * @throws NullPointerException when timestamp is null
108      * @throws IllegalArgumentException when value is NaN
109      * @throws IllegalArgumentException when given timestamp is before last timestamp
110      */
111     public double register(final Calendar timestamp, final double value)
112     {
113         Throw.whenNull(timestamp, "timestamp object may not be null");
114         return registerValue(timestamp.getTimeInMillis(), value);
115     }
116 
117     /**
118      * Process one observed Number-based value. Silently ignore when a value is registered, but tally is not active, i.e. when
119      * endObservations() has been called.
120      * @param timestamp Number; the object representing the timestamp
121      * @param value double; the value to process
122      * @return double; the value
123      * @throws NullPointerException when timestamp is null
124      * @throws IllegalArgumentException when value is NaN or timestamp is NaN
125      * @throws IllegalArgumentException when given timestamp is before last timestamp
126      */
127     public double register(final Number timestamp, final double value)
128     {
129         return registerValue(timestamp, value);
130     }
131 
132     /**
133      * Explicit;y override the double value method signature of WeightedTally to call the right method.<br>
134      * Process one observed double value. Silently ignore when a value is registered, but tally is not active, i.e. when
135      * endObservations() has been called.
136      * @param timestamp Number; the object representing the timestamp
137      * @param value double; the value to process
138      * @return double; the value
139      * @throws NullPointerException when timestamp is null
140      * @throws IllegalArgumentException when value is NaN or timestamp is NaN
141      * @throws IllegalArgumentException when given timestamp is before last timestamp
142      */
143     @Override
144     public double register(final double timestamp, final double value)
145     {
146         return registerValue(timestamp, value);
147     }
148 
149     /**
150      * Process one observed Number-based value. Silently ignore when a value is registered, but tally is not active, i.e. when
151      * endObservations() has been called.
152      * @param timestamp Number; the object representing the timestamp
153      * @param value double; the value to process
154      * @return double; the value
155      * @throws NullPointerException when timestamp is null
156      * @throws IllegalArgumentException when value is NaN or timestamp is NaN
157      * @throws IllegalArgumentException when given timestamp is before last timestamp
158      */
159     protected double registerValue(final Number timestamp, final double value)
160     {
161         Throw.whenNull(timestamp, "timestamp object may not be null");
162         Throw.when(Double.isNaN(value), IllegalArgumentException.class, "value may not be NaN");
163         double timestampDouble = timestamp.doubleValue();
164         Throw.when(Double.isNaN(timestampDouble), IllegalArgumentException.class, "timestamp may not be NaN");
165         Throw.when(timestampDouble < this.lastTimestamp, IllegalArgumentException.class,
166                 "times not offered in ascending order. Last time was " + this.lastTimestamp + ", new timestamp was "
167                         + timestampDouble);
168 
169         synchronized (super.semaphore)
170         {
171             // only calculate anything when the time interval is larger than 0, and when the TimestampWeightedTally is active
172             if ((Double.isNaN(this.lastTimestamp) || timestampDouble > this.lastTimestamp) && this.active)
173             {
174                 if (Double.isNaN(this.startTime))
175                 {
176                     this.startTime = timestampDouble;
177                 }
178                 else
179                 {
180                     double deltaTime = Math.max(0.0, timestampDouble - this.lastTimestamp);
181                     super.register(deltaTime, this.lastValue);
182                 }
183                 this.lastTimestamp = timestampDouble;
184             }
185             this.lastValue = value;
186             return value;
187         }
188     }
189 
190     /**
191      * Return a string representing a header for a textual table with a monospaced font that can contain multiple statistics.
192      * @return String; header for the textual table.
193      */
194     public static String reportHeader()
195     {
196         return "-".repeat(126)
197                 + String.format("%n| %-48.48s | %6.6s | %10.10s | %10.10s | %10.10s | %10.10s | %10.10s |%n",
198                         "Timestamp-based weighted Tally name", "n", "interval", "w.mean", "w.st.dev", "min obs", "max obs")
199                 + "-".repeat(126);
200     }
201 
202     @Override
203     public String reportLine()
204     {
205         return String.format("| %-48.48s | %6d | %s | %s | %s | %s | %s |", getDescription(), getN(),
206                 formatFixed(this.lastTimestamp - this.startTime, 10), formatFixed(getWeightedPopulationMean(), 10),
207                 formatFixed(getWeightedPopulationStDev(), 10), formatFixed(getMin(), 10), formatFixed(getMax(), 10));
208     }
209 
210     /**
211      * Return a string representing a footer for a textual table with a monospaced font that can contain multiple statistics.
212      * @return String; footer for the textual table
213      */
214     public static String reportFooter()
215     {
216         return "-".repeat(126);
217     }
218 
219     @Override
220     public String toString()
221     {
222         return "TimestampWeightedTally" + super.toString().substring("WeightedTally".length());
223     }
224 
225 }