Skip to main content

optionstratlib_simulation/simulation/steps/
y.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 24/3/25
5******************************************************************************/
6
7use crate::error::SimulationError;
8use optionstratlib_core::model::Positive;
9use serde::{Serialize, Serializer};
10use std::convert::TryInto;
11use std::fmt::{Display, Formatter};
12
13/// A step entity in a Y-axis progression with an associated numeric value.
14///
15/// `Ystep` represents a discrete point in a sequence, maintaining both an integer index
16/// and an associated positive numeric value. This structure is designed to be used in
17/// financial calculations, visualizations, or any context where tracking incremental
18/// steps with corresponding values is needed.
19///
20/// The generic parameter `T` allows flexibility in the type of value stored, as long as
21/// it can be converted into a `Positive` type, ensuring all values are valid non-negative numbers.
22///
23/// # Type Parameters
24///
25/// * `T` - A type that can be converted into a `Positive` value.
26///   This ensures that all values stored in `Ystep` are non-negative.
27///
28/// # Fields
29///
30/// * `index` - An integer representing the step's position in a sequence.
31///
32/// * `value` - A positive numeric value associated with this step.
33///
34#[derive(Debug, Clone)]
35pub struct Ystep<T>
36where
37    T: TryInto<Positive> + Display + Clone,
38{
39    /// An integer index representing the step's position in a sequence
40    index: i32,
41
42    /// The positive numeric value associated with this step
43    value: T,
44}
45
46/// A step value holder for simulation values that must be positive.
47///
48/// `Ystep<T>` maintains an index counter and a value of type `T`, where `T`
49/// must be `TryInto<Positive>` + Display and convertible to a `Positive` value.
50///
51/// This struct is typically used in financial simulations where values need
52/// to be tracked across simulation steps while ensuring they remain positive.
53///
54/// # Type Parameters
55///
56/// * `T` - The value type that must be convertible to `Positive`
57///
58/// # Examples
59///
60/// ```rust
61/// use rust_decimal_macros::dec;
62/// use optionstratlib_simulation::simulation::steps::Ystep;
63///
64/// // Create a new step with initial value
65/// let step = Ystep::new(1, dec!(10.5));
66///
67/// // Access the current value
68/// assert_eq!(*step.value(), dec!(10.5));
69/// ```
70impl<T> Ystep<T>
71where
72    T: TryInto<Positive> + Display + Clone,
73{
74    /// Creates a new `Ystep` instance with the specified value.
75    ///
76    /// The index is initialized to 0 and the provided value is stored.
77    ///
78    /// # Parameters
79    ///
80    /// * `value` - The initial value to store
81    ///
82    /// # Returns
83    ///
84    /// A new `Ystep<T>` instance
85    pub fn new(index: i32, value: T) -> Self {
86        Self { index, value }
87    }
88
89    /// Creates a new step with an incremented index and the provided value.
90    ///
91    /// This method produces a new step instance that represents the next step in a sequence,
92    /// with an index one higher than the current step and storing the provided value.
93    ///
94    /// # Parameters
95    ///
96    /// * `value` - The value to store in the new step
97    ///
98    /// # Returns
99    ///
100    /// A new `Ystep<T>` instance with incremented index
101    ///
102    /// # Errors
103    ///
104    /// Returns [`SimulationError::StepError`] when the index is already
105    /// `i32::MAX`. The raw `+ 1` used here aborted in debug builds and wrapped
106    /// to `i32::MIN` in release builds (#788).
107    pub fn next(&self, value: T) -> Result<Self, SimulationError> {
108        let index = self.index.checked_add(1).ok_or_else(|| {
109            SimulationError::step_error("step index overflowed advancing the walk")
110        })?;
111        Ok(Self { index, value })
112    }
113
114    /// Returns an immutable reference to the stored value.
115    ///
116    /// # Returns
117    ///
118    /// A reference to the stored value of type `T`
119    pub fn value(&self) -> &T {
120        &self.value
121    }
122
123    /// Returns an immutable reference to the stored value.
124    ///
125    /// # Returns
126    ///
127    /// A reference to the stored value of type `T`
128    ///
129    /// # Errors
130    ///
131    /// Returns [`SimulationError::PositiveError`] when the stored
132    /// `y` value cannot be converted to a `Positive` (NaN, negative,
133    /// or otherwise invalid for the `Positive` invariant).
134    pub fn positive(&self) -> Result<Positive, SimulationError> {
135        self.value
136            .clone()
137            .try_into()
138            .map_err(|_| SimulationError::step_error("Failed to convert value to Positive"))
139    }
140
141    /// Returns an immutable reference to the index of this step.
142    ///
143    /// The index represents the step's position in a sequence, typically
144    /// indicating how many steps have occurred since initialization.
145    ///
146    /// # Returns
147    ///
148    /// A reference to the index as an `i32` value
149    pub fn index(&self) -> &i32 {
150        &self.index
151    }
152
153    /// Returns a mutable reference to the stored value.
154    ///
155    /// This allows modifying the value while maintaining the `Ystep` structure.
156    ///
157    /// # Returns
158    ///
159    /// A mutable reference to the stored value of type `T`
160    pub fn value_mut(&mut self) -> &mut T {
161        &mut self.value
162    }
163}
164
165impl<T> Display for Ystep<T>
166where
167    T: TryInto<Positive> + Display + Clone,
168{
169    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
170        let positive_value: Positive = self.positive().unwrap_or(Positive::ZERO);
171        write!(
172            f,
173            "Ystep {{ index: {}, value: {} }}",
174            self.index,
175            // Rounding the `Decimal` prints what `Positive::round_to(3)` did,
176            // without its `unwrap_or_panic` (#788).
177            positive_value.to_dec().round_dp(3).normalize()
178        )
179    }
180}
181
182impl<T> Serialize for Ystep<T>
183where
184    T: TryInto<Positive> + Display + Serialize + Clone,
185{
186    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
187    where
188        S: Serializer,
189    {
190        // Convert value to Positive for consistent serialization
191        let value: Positive = self.positive().unwrap_or(Positive::ZERO);
192
193        // Use a struct with 2 fields to represent Ystep
194        use serde::ser::SerializeStruct;
195        let mut state = serializer.serialize_struct("Ystep", 2)?;
196        state.serialize_field("index", &self.index)?;
197        state.serialize_field("value", &value)?;
198        state.end()
199    }
200}
201
202impl<T> TryFrom<Ystep<T>> for Positive
203where
204    T: TryInto<Positive> + Display + Clone,
205{
206    type Error = SimulationError;
207
208    fn try_from(step: Ystep<T>) -> Result<Self, Self::Error> {
209        step.positive()
210    }
211}
212
213#[cfg(test)]
214mod tests_ystep {
215    use super::*;
216
217    #[test]
218    fn test_ystep_new() {
219        // Test creation with a simple value
220        let value = 42.5;
221        let step = Ystep::new(0, value);
222
223        assert_eq!(step.index, 0);
224        assert_eq!(step.value, 42.5);
225    }
226}
227
228#[cfg(test)]
229mod tests_serialize {
230    use super::*;
231    use optionstratlib_core::pos_or_panic;
232
233    use rust_decimal_macros::dec;
234    use serde_json::{Value, json};
235
236    #[test]
237    fn test_basic_serialization() {
238        let step = Ystep::new(5, 10.5f64);
239        let serialized = serde_json::to_string(&step).unwrap();
240        let parsed: Value = serde_json::from_str(&serialized).unwrap();
241
242        assert_eq!(parsed["index"], 5);
243        assert_eq!(parsed["value"], "10.5");
244    }
245
246    #[test]
247    fn test_serialized_structure() {
248        let step = Ystep::new(42, 15.75f64);
249        let serialized = serde_json::to_string(&step).unwrap();
250        let parsed: Value = serde_json::from_str(&serialized).unwrap();
251
252        // Check structure
253        assert!(parsed.is_object());
254        assert_eq!(parsed.as_object().unwrap().len(), 2);
255        assert!(parsed.get("index").unwrap().is_i64());
256        assert!(parsed.get("value").unwrap().is_string());
257
258        // Check values
259        assert_eq!(parsed["index"], json!(42));
260        assert_eq!(parsed["value"], json!("15.75"));
261    }
262
263    #[test]
264    fn test_type_conversion() {
265        // Test with different types but same values
266        let step_f64 = Ystep::new(1, 2.5f64);
267        let step_decimal = Ystep::new(1, dec!(2.5));
268        let step_positive = Ystep::new(1, pos_or_panic!(2.5));
269
270        // Serialize all
271        let json_f64 = serde_json::to_string(&step_f64).unwrap();
272        let json_decimal = serde_json::to_string(&step_decimal).unwrap();
273        let json_positive = serde_json::to_string(&step_positive).unwrap();
274
275        // Parse to compare values
276        let parsed_f64: Value = serde_json::from_str(&json_f64).unwrap();
277        let parsed_decimal: Value = serde_json::from_str(&json_decimal).unwrap();
278        let parsed_positive: Value = serde_json::from_str(&json_positive).unwrap();
279
280        // All should have the same value representation
281        assert_eq!(parsed_f64["value"], json!("2.5"));
282        assert_eq!(parsed_decimal["value"], json!("2.5"));
283        assert_eq!(parsed_positive["value"], json!("2.5"));
284    }
285
286    #[test]
287    fn test_json_format_identity() {
288        // Test with different types but same values
289        let step_f64 = Ystep::new(3, 4.01f64);
290        let step_decimal = Ystep::new(3, dec!(4.01));
291        let step_positive = Ystep::new(3, pos_or_panic!(4.01));
292
293        // Serialize all
294        let json_f64 = serde_json::to_string(&step_f64).unwrap();
295        let json_decimal = serde_json::to_string(&step_decimal).unwrap();
296        let json_positive = serde_json::to_string(&step_positive).unwrap();
297
298        // All should serialize to identical JSON
299        assert_eq!(json_f64, json_decimal);
300        assert_eq!(json_decimal, json_positive);
301    }
302
303    #[test]
304    fn test_edge_cases() {
305        // Test with zero
306        let step_zero = Ystep::new(0, 0.1f64);
307        let json_zero = serde_json::to_string(&step_zero).unwrap();
308        let parsed_zero: Value = serde_json::from_str(&json_zero).unwrap();
309        assert_eq!(parsed_zero["value"], json!("0.1"));
310
311        // Test with very small value
312        let step_small = Ystep::new(1, 0.000001f64);
313        let json_small = serde_json::to_string(&step_small).unwrap();
314        let parsed_small: Value = serde_json::from_str(&json_small).unwrap();
315        assert!(
316            parsed_small["value"]
317                .as_str()
318                .unwrap()
319                .parse::<f64>()
320                .unwrap()
321                > 0.0
322        );
323        assert!(
324            parsed_small["value"]
325                .as_str()
326                .unwrap()
327                .parse::<f64>()
328                .unwrap()
329                < 0.0001
330        );
331
332        // Test with large value
333        let step_large = Ystep::new(2, 1_000_000.01f64);
334        let json_large = serde_json::to_string(&step_large).unwrap();
335        let parsed_large: Value = serde_json::from_str(&json_large).unwrap();
336        assert_eq!(parsed_large["value"], json!("1000000.01"));
337    }
338
339    #[test]
340    fn test_decimal_precision() {
341        let step = Ystep::new(1, 1.23456789f64);
342        let serialized = serde_json::to_string(&step).unwrap();
343        let parsed: Value = serde_json::from_str(&serialized).unwrap();
344
345        // Check precision is maintained (to reasonable float precision)
346        let value = parsed["value"].as_str().unwrap().parse::<f64>().unwrap();
347        assert!((value - 1.23456789).abs() < 0.0000001);
348    }
349
350    #[test]
351    fn test_next_serialization() {
352        let step = Ystep::new(1, 5.0f64);
353        let next_step = step.next(10.0f64).unwrap();
354
355        let serialized = serde_json::to_string(&next_step).unwrap();
356        let parsed: Value = serde_json::from_str(&serialized).unwrap();
357
358        // Check next step has incremented index
359        assert_eq!(parsed["index"], 2);
360        assert_eq!(parsed["value"], "10");
361    }
362
363    #[test]
364    fn test_pretty_serialization() {
365        let step = Ystep::new(7, 15.25f64);
366        let serialized = serde_json::to_string_pretty(&step).unwrap();
367
368        // Check it contains appropriate formatting
369        assert!(serialized.contains("\n"));
370        assert!(serialized.contains("  "));
371
372        // Ensure it can be parsed back
373        let parsed: Value = serde_json::from_str(&serialized).unwrap();
374        assert_eq!(parsed["index"], 7);
375        assert_eq!(parsed["value"], "15.25");
376    }
377}
378
379/// `Ystep::next` formed `index + 1` with the raw operator: an abort in debug
380/// builds and a silent wrap to `i32::MIN` in release builds (#788).
381#[cfg(test)]
382mod tests_next_panic_freedom {
383    use super::*;
384
385    #[test]
386    fn test_next_at_max_index_returns_error() {
387        let step = Ystep::new(i32::MAX, 1.0f64);
388        assert!(matches!(
389            step.next(2.0f64),
390            Err(SimulationError::StepError { .. })
391        ));
392    }
393
394    #[test]
395    fn test_next_increments_index() -> Result<(), SimulationError> {
396        let step = Ystep::new(i32::MAX - 1, 1.0f64).next(2.0f64)?;
397        assert_eq!(*step.index(), i32::MAX);
398        Ok(())
399    }
400}