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