LotSizingDataModel.Core 2.0.1
Core domain model, shared abstractions and XML-serializable entities.
Loading...
Searching...
No Matches
DoubleTimeSeries.cs
Go to the documentation of this file.
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Linq;
5using System.Xml.Serialization;
6
8
9/// <summary>
10/// Represents a sequence of finite double values indexed by planning period.
11///
12/// Period numbers are one-based:
13/// period 1 corresponds to the first value,
14/// period 2 to the second value, and so on.
15/// </summary>
16[Serializable]
17[XmlType(TypeName = "doubleTimeSeries")]
18public sealed class DoubleTimeSeries :
20 IEnumerable<double>
21{
22 private double[] _values = Array.Empty<double>();
23
24 /// <summary>
25 /// Initializes an empty time series.
26 ///
27 /// This constructor is required by XmlSerializer.
28 /// </summary>
30 {
31 }
32
33 /// <summary>
34 /// Initializes a time series with the specified number of periods.
35 /// </summary>
36 /// <param name="periodCount">
37 /// Number of periods in the planning horizon.
38 /// </param>
39 /// <param name="defaultValue">
40 /// Initial value assigned to every period.
41 /// </param>
43 int periodCount,
44 double defaultValue = 0.0)
45 {
46 Resize(periodCount, defaultValue);
47 }
48
49 /// <summary>
50 /// Gets or replaces all values of the time series.
51 ///
52 /// Each XML element corresponds to one planning period.
53 /// The first value corresponds to period 1.
54 /// </summary>
55 [XmlElement("value")]
56 public double[] Values
57 {
58 get => (double[])_values.Clone();
59 set => ReplaceValues(value ?? Array.Empty<double>());
60 }
61
62 /// <summary>
63 /// Gets the number of periods currently represented.
64 /// </summary>
65 [XmlIgnore]
66 public int PeriodCount => _values.Length;
67
68 /// <summary>
69 /// Gets or sets the value for a planning period.
70 ///
71 /// The period number starts at 1, not 0.
72 /// </summary>
73 /// <param name="period">
74 /// Planning period between 1 and <see cref="PeriodCount"/>.
75 /// </param>
76 [XmlIgnore]
77 public double this[int period]
78 {
79 get
80 {
81 int index = ConvertPeriodToIndex(period);
82 return _values[index];
83 }
84 set
85 {
86 ValidateFiniteValue(value, nameof(value));
87
88 int index = ConvertPeriodToIndex(period);
89
90 // Avoid unnecessary notifications if the value does not change
91 if (_values[index].Equals(value))
92 {
93 return;
94 }
95
96 _values[index] = value;
97
99 OnPropertyChanged("Item[]");
100 }
101 }
102
103 /// <summary>
104 /// Resizes the time series.
105 ///
106 /// Existing values are preserved. When the horizon grows,
107 /// new periods receive <paramref name="defaultValue"/>.
108 /// When the horizon shrinks, values beyond the new horizon
109 /// are removed.
110 /// </summary>
111 /// <param name="periodCount">
112 /// New number of periods.
113 /// </param>
114 /// <param name="defaultValue">
115 /// Value assigned to newly created periods.
116 /// </param>
117 public void Resize(
118 int periodCount,
119 double defaultValue = 0.0)
120 {
121 if (periodCount < 0)
122 {
123 throw new ArgumentOutOfRangeException(
124 nameof(periodCount),
125 periodCount,
126 "The number of periods cannot be negative.");
127 }
128
129 ValidateFiniteValue(defaultValue, nameof(defaultValue));
130
131 // No change necessary if the size is the same
132 if (periodCount == _values.Length)
133 {
134 return;
135 }
136
137 int previousPeriodCount = _values.Length;
138 var resizedValues = new double[periodCount];
139
140 // Copy existing values (the minimum between old and new size)
141 int copiedValueCount = Math.Min(
142 previousPeriodCount,
143 periodCount);
144
145 if (copiedValueCount > 0)
146 {
147 Array.Copy(
148 _values,
149 resizedValues,
150 copiedValueCount);
151 }
152
153 // Initialize new periods with the default value when expanding
154 if (periodCount > previousPeriodCount)
155 {
156 Array.Fill(
157 resizedValues,
158 defaultValue,
159 previousPeriodCount,
160 periodCount - previousPeriodCount);
161 }
162
163 _values = resizedValues;
164
165 OnPropertyChanged(nameof(Values));
167 OnPropertyChanged("Item[]");
168 }
169
170 /// <summary>
171 /// Assigns the same value to every planning period.
172 /// </summary>
173 public void Fill(double value)
174 {
175 ValidateFiniteValue(value, nameof(value));
176
177 // Avoid unnecessary notifications if all values are already equal
178 if (_values.All(currentValue => currentValue.Equals(value)))
179 {
180 return;
181 }
182
183 Array.Fill(_values, value);
184
185 OnPropertyChanged(nameof(Values));
186 OnPropertyChanged("Item[]");
187 }
188
189 /// <summary>
190 /// Gets the value associated with a planning period.
191 /// </summary>
192 public double GetValue(int period)
193 {
194 return this[period];
195 }
196
197 /// <summary>
198 /// Sets the value associated with a planning period.
199 /// </summary>
200 public void SetValue(int period, double value)
201 {
202 this[period] = value;
203 }
204
205 /// <summary>
206 /// Appends a value to the end of the time series.
207 /// </summary>
208 /// <param name="value">
209 /// Finite value to append.
210 /// </param>
211 /// <remarks>
212 /// This public method is required by <see cref="XmlSerializer"/>
213 /// because this type implements <see cref="IEnumerable{T}"/>.
214 /// During XML deserialization, the serializer calls this method
215 /// once for each serialized period value.
216 /// </remarks>
217 public void Add(double value)
218 {
219 ValidateFiniteValue(value, nameof(value));
220
221 int previousPeriodCount = _values.Length;
222
223 Array.Resize(
224 ref _values,
225 previousPeriodCount + 1);
226
227 _values[previousPeriodCount] = value;
228
229 OnPropertyChanged(nameof(Values));
231 OnPropertyChanged("Item[]");
232 }
233
234 /// <summary>
235 /// Removes all values from the time series.
236 /// </summary>
237 public void Clear()
238 {
239 if (_values.Length == 0)
240 {
241 return;
242 }
243
244 _values = Array.Empty<double>();
245
246 OnPropertyChanged(nameof(Values));
248 OnPropertyChanged("Item[]");
249 }
250
251 /// <summary>
252 /// Creates an independent copy of this time series.
253 /// </summary>
255 {
256 return new DoubleTimeSeries
257 {
258 Values = Values
259 };
260 }
261
262 /// <summary>
263 /// Returns an enumerator over the period values.
264 /// </summary>
265 public IEnumerator<double> GetEnumerator()
266 {
267 return ((IEnumerable<double>)_values).GetEnumerator();
268 }
269
270 IEnumerator IEnumerable.GetEnumerator()
271 {
272 return GetEnumerator();
273 }
274
275 /// <summary>
276 /// Replaces the internal values of the time series with a new array.
277 /// Validates all values before assignment.
278 /// </summary>
279 /// <param name="values">The new array of values to assign.</param>
280 private void ReplaceValues(double[] values)
281 {
282 // Validate that all values are finite (neither NaN nor Infinity)
283 foreach (double value in values)
284 {
285 ValidateFiniteValue(value, nameof(values));
286 }
287
288 // Avoid unnecessary notifications if the values are identical
289 if (_values.SequenceEqual(values))
290 {
291 return;
292 }
293
294 // Detect if the period count changes to notify PropertyChanged
295 bool periodCountChanged =
296 _values.Length != values.Length;
297
298 // Clone the array to ensure encapsulation
299 _values = (double[])values.Clone();
300
301 OnPropertyChanged(nameof(Values));
302 OnPropertyChanged("Item[]");
303
304 if (periodCountChanged)
305 {
307 }
308 }
309
310 /// <summary>
311 /// Converts a period number (1-based) to an array index (0-based).
312 /// </summary>
313 /// <param name="period">The period number (starts at 1).</param>
314 /// <returns>The corresponding index in the array (starts at 0).</returns>
315 /// <exception cref="ArgumentOutOfRangeException">
316 /// If the period is outside the valid range [1, PeriodCount].
317 /// </exception>
318 private int ConvertPeriodToIndex(int period)
319 {
320 if (period < 1 || period > _values.Length)
321 {
322 throw new ArgumentOutOfRangeException(
323 nameof(period),
324 period,
325 $"The period must be between 1 and {_values.Length}.");
326 }
327
328 // Conversion: period 1 -> index 0, period 2 -> index 1, etc.
329 return period - 1;
330 }
331
332 /// <summary>
333 /// Validates that a double value is finite (neither NaN nor infinite).
334 /// </summary>
335 /// <param name="value">The value to validate.</param>
336 /// <param name="parameterName">The parameter name for the error message.</param>
337 /// <exception cref="ArgumentOutOfRangeException">
338 /// If the value is NaN or infinite.
339 /// </exception>
340 private static void ValidateFiniteValue(
341 double value,
342 string parameterName)
343 {
344 if (double.IsNaN(value) ||
345 double.IsInfinity(value))
346 {
347 throw new ArgumentOutOfRangeException(
348 parameterName,
349 value,
350 "A time-series value must be a finite number.");
351 }
352 }
353}
void Clear()
Removes all values from the time series.
double GetValue(int period)
Gets the value associated with a planning period.
void Resize(int periodCount, double defaultValue=0.0)
Resizes the time series.
double[] Values
Gets or replaces all values of the time series.
IEnumerator< double > GetEnumerator()
Returns an enumerator over the period values.
DoubleTimeSeries Clone()
Creates an independent copy of this time series.
DoubleTimeSeries(int periodCount, double defaultValue=0.0)
Initializes a time series with the specified number of periods.
int PeriodCount
Gets the number of periods currently represented.
void SetValue(int period, double value)
Sets the value associated with a planning period.
DoubleTimeSeries()
Initializes an empty time series.
void Add(double value)
Appends a value to the end of the time series.
void Fill(double value)
Assigns the same value to every planning period.
Base class for model objects that notify listeners when one of their properties changes.
virtual void OnPropertyChanged([CallerMemberName] string? propertyName=null)
Raises the PropertyChanged event.