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