Skip to main content

optionstratlib_math/geometrics/
utils.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 21/1/25
5******************************************************************************/
6use crate::geometrics::ConstructionMethod;
7use optionstratlib_core::error::DecimalError;
8use rust_decimal::{Decimal, MathematicalOps};
9use std::collections::BTreeSet;
10
11/// Checked counterpart of [`MathematicalOps::powu`], which aborts with
12/// `Pow overflowed` on the operator path.
13///
14/// `powu` is `checked_powu` plus an `unwrap`, so routing through the checked
15/// call keeps the digits identical while turning the abort into a
16/// [`DecimalError::Overflow`] tagged with `op`.
17///
18/// # Errors
19///
20/// Returns [`DecimalError::Overflow`] when `base^exp` leaves the
21/// representable `Decimal` range.
22pub(crate) fn powu_checked(
23    base: Decimal,
24    exp: u64,
25    op: &'static str,
26) -> Result<Decimal, DecimalError> {
27    base.checked_powu(exp)
28        .ok_or_else(|| DecimalError::overflow(op, base, Decimal::from(exp)))
29}
30
31/// Defines a geometric object constructed from a set of points.
32/// Provides methods for creating, accessing, and manipulating these objects.
33pub trait GeometricObject<Point: Clone, Input> {
34    /// Type alias for any errors that might occur during the construction of the geometric object.
35    type Error;
36
37    /// Returns a `BTreeSet` containing references to the points that constitute the geometric object.
38    /// The `BTreeSet` ensures that the points are ordered and unique.
39    fn get_points(&self) -> BTreeSet<&Point>;
40
41    /// Returns a `Vec` containing references to the points that constitute the geometric object.
42    /// This method simply converts the `BTreeSet` from `get_points` into a `Vec`.
43    fn vector(&self) -> Vec<&Point> {
44        self.get_points().into_iter().collect()
45    }
46
47    /// Creates a new geometric object from a `Vec` of points.
48    ///
49    /// The generic type `T` represents the input point type, which can be converted into the `Point`
50    /// type associated with the geometric object.
51    ///
52    fn from_vector<T>(points: Vec<T>) -> Self
53    where
54        Self: Sized,
55        T: Into<Point> + Clone;
56
57    /// Constructs a geometric object using a specific construction method.
58    ///
59    /// The generic type `T` represents a type that can be converted into a `ConstructionMethod`.
60    /// The `ConstructionMethod` enum provides different strategies for building geometric objects,
61    /// such as constructing from a set of data points or from a parametric function.
62    ///
63    /// This method returns a `Result` to handle potential errors during construction.
64    ///
65    /// # Errors
66    ///
67    /// Returns the implementation's `Self::Error` when the provided
68    /// construction method cannot produce a valid geometry, for
69    /// example when a `FromData` input is empty or a parametric
70    /// function surfaces a domain violation.
71    fn construct<T>(method: T) -> Result<Self, Self::Error>
72    where
73        Self: Sized,
74        T: Into<ConstructionMethod<Point, Input, Self::Error>>;
75
76    /// Returns the points of the geometric object as a `Vec` of references.
77    /// Equivalent to calling the `vector()` method.
78    fn to_vector(&self) -> Vec<&Point> {
79        self.vector()
80    }
81
82    /// Calculates the minimum and maximum decimal values from an iterator of decimals.
83    ///
84    /// This is a utility function that can be used to find the range of values in a set of points.
85    fn calculate_range<I>(iter: I) -> (Decimal, Decimal)
86    where
87        I: Iterator<Item = Decimal>,
88    {
89        iter.fold((Decimal::MAX, Decimal::MIN), |(min, max), val| {
90            (min.min(val), max.max(val))
91        })
92    }
93}
94
95#[cfg(test)]
96mod tests_geometric_object {
97    use super::*;
98    use optionstratlib_core::utils::Len;
99    use rust_decimal_macros::dec;
100
101    // Mock Point type for testing
102    #[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
103    struct TestPoint {
104        x: Decimal,
105        y: Decimal,
106    }
107
108    impl From<(Decimal, Decimal)> for TestPoint {
109        fn from(tuple: (Decimal, Decimal)) -> Self {
110            TestPoint {
111                x: tuple.0,
112                y: tuple.1,
113            }
114        }
115    }
116
117    // Mock Geometric Object for testing
118    #[derive(Debug, Clone)]
119    struct TestGeometricObject {
120        points: BTreeSet<TestPoint>,
121    }
122
123    impl GeometricObject<TestPoint, Decimal> for TestGeometricObject {
124        type Error = String;
125
126        fn get_points(&self) -> BTreeSet<&TestPoint> {
127            self.points.iter().collect()
128        }
129
130        fn from_vector<T>(points: Vec<T>) -> Self
131        where
132            T: Into<TestPoint> + Clone,
133        {
134            let points_set: BTreeSet<TestPoint> = points.into_iter().map(|p| p.into()).collect();
135            TestGeometricObject { points: points_set }
136        }
137
138        fn construct<T>(method: T) -> Result<Self, Self::Error>
139        where
140            Self: Sized,
141            T: Into<ConstructionMethod<TestPoint, Decimal, Self::Error>>,
142        {
143            let method = method.into();
144            match method {
145                ConstructionMethod::FromData { points } => {
146                    if points.is_empty() {
147                        Err("Cannot create object from empty points".to_string())
148                    } else {
149                        Ok(TestGeometricObject { points })
150                    }
151                }
152                _ => Err("Unsupported construction method".to_string()),
153            }
154        }
155    }
156
157    impl Len for TestGeometricObject {
158        fn len(&self) -> usize {
159            self.points.len()
160        }
161    }
162
163    #[test]
164    fn test_get_points() {
165        let points = BTreeSet::from([
166            TestPoint {
167                x: dec!(1.0),
168                y: dec!(2.0),
169            },
170            TestPoint {
171                x: dec!(3.0),
172                y: dec!(4.0),
173            },
174        ]);
175
176        let obj = TestGeometricObject {
177            points: points.clone(),
178        };
179        let retrieved_points = obj.get_points();
180
181        assert_eq!(retrieved_points.len(), points.len());
182        for point in &points {
183            assert!(retrieved_points.contains(point));
184        }
185    }
186
187    #[test]
188    fn test_from_vector() {
189        let points = vec![(dec!(1.0), dec!(2.0)), (dec!(3.0), dec!(4.0))];
190        let obj = TestGeometricObject::from_vector(points);
191
192        assert_eq!(obj.points.len(), 2);
193        assert!(obj.points.contains(&TestPoint {
194            x: dec!(1.0),
195            y: dec!(2.0)
196        }));
197        assert!(obj.points.contains(&TestPoint {
198            x: dec!(3.0),
199            y: dec!(4.0)
200        }));
201    }
202
203    #[test]
204    fn test_construct_from_data() {
205        let points = BTreeSet::from([
206            TestPoint {
207                x: dec!(1.0),
208                y: dec!(2.0),
209            },
210            TestPoint {
211                x: dec!(3.0),
212                y: dec!(4.0),
213            },
214        ]);
215
216        let result = TestGeometricObject::construct(ConstructionMethod::FromData {
217            points: points.clone(),
218        });
219
220        assert!(result.is_ok());
221        let obj = result.unwrap();
222        assert_eq!(obj.points, points);
223    }
224
225    #[test]
226    fn test_construct_empty_points_fails() {
227        let points: BTreeSet<TestPoint> = BTreeSet::new();
228        let result = TestGeometricObject::construct(ConstructionMethod::FromData { points });
229
230        assert!(result.is_err());
231    }
232
233    #[test]
234    fn test_to_vector() {
235        let points = BTreeSet::from([
236            TestPoint {
237                x: dec!(1.0),
238                y: dec!(2.0),
239            },
240            TestPoint {
241                x: dec!(3.0),
242                y: dec!(4.0),
243            },
244        ]);
245
246        let obj = TestGeometricObject {
247            points: points.clone(),
248        };
249        let vector = obj.to_vector();
250
251        assert_eq!(vector.len(), points.len());
252        for point in &points {
253            assert!(vector.contains(&point));
254        }
255    }
256
257    #[test]
258    fn test_calculate_range() {
259        let values = vec![dec!(1.0), dec!(5.0), dec!(3.0), dec!(-2.0), dec!(10.0)];
260
261        let (min, max) = TestGeometricObject::calculate_range(values.into_iter());
262
263        assert_eq!(min, dec!(-2.0));
264        assert_eq!(max, dec!(10.0));
265    }
266
267    #[test]
268    fn test_len_trait() {
269        let points = BTreeSet::from([
270            TestPoint {
271                x: dec!(1.0),
272                y: dec!(2.0),
273            },
274            TestPoint {
275                x: dec!(3.0),
276                y: dec!(4.0),
277            },
278        ]);
279
280        let obj = TestGeometricObject { points };
281        assert_eq!(obj.len(), 2);
282        assert!(!obj.is_empty());
283    }
284
285    #[test]
286    fn test_len_empty() {
287        let points: BTreeSet<TestPoint> = BTreeSet::new();
288
289        let obj = TestGeometricObject { points };
290        assert_eq!(obj.len(), 0);
291        assert!(obj.is_empty());
292    }
293}