Skip to main content

optionstratlib_math/curves/
types.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 26/8/24
5******************************************************************************/
6use crate::error::CurveError;
7use crate::geometrics::HasX;
8use num_traits::FromPrimitive;
9use optionstratlib_core::model::decimal::decimal_to_f64;
10use optionstratlib_core::model::is_positive;
11use rust_decimal::Decimal;
12use serde::{Deserialize, Serialize};
13use std::cmp::Ordering;
14use std::fmt::Display;
15use std::hash::{Hash, Hasher};
16
17/// Represents a point in two-dimensional space with `x` and `y` coordinates.
18///
19/// # Overview
20/// The `Point2D` struct is used to define a point in a 2D Cartesian coordinate system.
21/// Both coordinates are stored as `Decimal` values to provide high precision,
22/// making it suitable for applications requiring accurate numerical calculations.
23///
24/// # Usage
25/// This structure serves as a fundamental data type in various geometric operations:
26/// - Defining positions in curve plotting and interpolation
27/// - Representing intersections between curves
28/// - Serving as input/output for mathematical transformations
29/// - Supporting coordinate-based algorithms in the library
30///
31/// # Examples
32///
33/// ```rust
34/// use rust_decimal_macros::dec;
35/// use optionstratlib_math::curves::Point2D;
36///
37/// // Create a point at coordinates (3.5, -2.25)
38/// let point = Point2D {
39///     x: dec!(3.5),
40///     y: dec!(-2.25)
41/// };
42/// ```
43///
44/// # Derivable Traits
45/// - `Debug`: Enables formatted debugging output
46/// - `Clone` and `Copy`: Allow efficient duplication of point values
47/// - `Serialize` and `Deserialize`: Support for serialization frameworks
48///
49/// This struct is primarily used in conjunction with the `Curve` and `Curvable` types
50/// to represent mathematical curves and perform geometric operations.
51#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
52#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
53pub struct Point2D {
54    /// The x-coordinate in the Cartesian plane, represented as a high-precision `Decimal`
55    /// value to ensure accuracy in mathematical operations.
56    pub x: Decimal,
57
58    /// The y-coordinate in the Cartesian plane, represented as a high-precision `Decimal`
59    /// value to ensure accuracy in mathematical operations.
60    pub y: Decimal,
61}
62
63impl Display for Point2D {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        write!(f, "(x: {}, y: {})", self.x, self.y)
66    }
67}
68
69/// Two points are equal when both coordinates are equal.
70///
71/// `PartialEq`, `Ord` and `Hash` all read the full `(x, y)` pair, so
72/// `a == b` holds exactly when `a.cmp(&b)` is [`Ordering::Equal`], and equal
73/// points hash alike. `Ord` requires that agreement.
74///
75/// Comparing on `x` alone would also break `Point2D` in its second role: it
76/// is the *index* type of a surface (`AxisOperations<Point3D, Point2D>`),
77/// where it names a coordinate in the xy-plane and `z` is the dependent
78/// value. An x-only `Eq`/`Hash` collapses every column of a surface grid
79/// onto one cell whenever those indices pass through a `HashSet`.
80///
81/// The "one ordinate per abscissa" rule is a property of a *curve*, not of a
82/// point, and it is a container-level invariant that nothing enforces:
83/// [`crate::curves::Curve::new`] stores a repeated abscissa unchanged, and
84/// `points` is a `pub` field, so no constructor could. The rule and what
85/// each consumer does when it is broken are documented on
86/// [`crate::curves::Curve::new`]. A genuinely multi-valued projection is not
87/// a curve at all: [`crate::surfaces::Surface::project_onto`] returns a
88/// `Vec<Point2D>`.
89impl PartialEq for Point2D {
90    fn eq(&self, other: &Self) -> bool {
91        self.x == other.x && self.y == other.y
92    }
93}
94
95impl Eq for Point2D {}
96
97impl Hash for Point2D {
98    fn hash<H: Hasher>(&self, state: &mut H) {
99        // Delegate to `Decimal`'s own `Hash`, which normalizes first. Hashing
100        // the raw mantissa and scale would give `dec!(1.0)` and `dec!(1.00)`
101        // different hashes even though they compare equal.
102        self.x.hash(state);
103        self.y.hash(state);
104    }
105}
106
107impl PartialOrd for Point2D {
108    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
109        Some(self.cmp(other))
110    }
111}
112
113/// Orders on `(x, y)`, lexicographically: the same pair [`PartialEq`] reads,
114/// so `cmp` returns [`Ordering::Equal`] exactly when the points are equal.
115impl Ord for Point2D {
116    fn cmp(&self, other: &Self) -> Ordering {
117        match self.x.cmp(&other.x) {
118            Ordering::Equal => self.y.cmp(&other.y),
119            x_ordering => x_ordering,
120        }
121    }
122}
123
124impl Point2D {
125    /// Creates a new instance of `Point2D` using the specified `x` and `y` coordinates.
126    ///
127    /// # Parameters
128    /// - `x`: The x-coordinate of the point, which implements `Into<Decimal>`.
129    /// - `y`: The y-coordinate of the point, which implements `Into<Decimal>`.
130    ///
131    /// # Returns
132    /// A `Point2D` instance with the provided `x` and `y` coordinates, converted into `Decimal`.
133    ///
134    /// # Usage
135    /// This function is used when creating a `Point2D` object from any type that can be converted
136    /// into `Decimal`, allowing flexibility in input types (e.g., `f64`, `i32`, etc.).
137    pub fn new<T: Into<Decimal>, U: Into<Decimal>>(x: T, y: U) -> Self {
138        Self {
139            x: x.into(),
140            y: y.into(),
141        }
142    }
143
144    /// Converts the `Point2D` instance into a tuple `(T, U)`.
145    ///
146    /// # Parameters
147    /// - `T`: The type for the x-coordinate, which must implement `From<Decimal>` and have a 'static lifetime.
148    /// - `U`: The type for the y-coordinate, which must implement `From<Decimal>` and have a 'static lifetime.
149    ///
150    /// # Returns
151    /// - `Ok`: A tuple `(T, U)` containing the converted `x` and `y` values.
152    /// - `Err`: A `CurvesError` if conversion constraints are violated:
153    ///   - `x` must be positive if `T` is the `Positive` type.
154    ///   - `y` must be positive if `U` is the `Positive` type.
155    ///
156    /// # Errors
157    /// This function returns an error if the positivity constraints are violated or if
158    /// conversions fail due to invalid type requirements.
159    pub fn to_tuple<T: TryFrom<Decimal> + 'static, U: TryFrom<Decimal> + 'static>(
160        &self,
161    ) -> Result<(T, U), CurveError> {
162        if is_positive::<T>() && self.x <= Decimal::ZERO {
163            return Err(CurveError::Point2DError {
164                reason: "x must be positive for type T",
165            });
166        }
167
168        if is_positive::<U>() && self.y <= Decimal::ZERO {
169            return Err(CurveError::Point2DError {
170                reason: "y must be positive for type U",
171            });
172        }
173
174        let x = T::try_from(self.x).map_err(|_| CurveError::Point2DError {
175            reason: "failed to convert x",
176        })?;
177        let y = U::try_from(self.y).map_err(|_| CurveError::Point2DError {
178            reason: "failed to convert y",
179        })?;
180
181        Ok((x, y))
182    }
183
184    /// Creates a new `Point2D` instance from a tuple containing `x` and `y` values.
185    ///
186    /// # Parameters
187    /// - `x`: The x-coordinate, which implements `Into<Decimal>`.
188    /// - `y`: The y-coordinate, which implements `Into<Decimal>`.
189    ///
190    /// # Returns
191    /// - `Ok`: A new `Point2D` instance with the given `x` and `y` coordinates.
192    /// - `Err`: A `CurvesError` if coordinate creation fails.
193    ///
194    /// # Usage
195    /// This function allows constructing a `Point2D` directly from a tuple representation.
196    ///
197    /// # Errors
198    ///
199    /// Currently infallible for the blanket `Into<Decimal>` bounds;
200    /// the `Result` signature is retained so future implementations
201    /// that can reject non-finite or out-of-range conversions (e.g.
202    /// via `TryFrom<f64>`) can surface
203    /// [`CurveError::ConstructionError`] without a breaking change.
204    pub fn from_tuple<T: Into<Decimal>, U: Into<Decimal>>(x: T, y: U) -> Result<Self, CurveError> {
205        Ok(Self::new(x, y))
206    }
207
208    /// Converts the `Point2D` instance into a tuple of `(f64, f64)`.
209    ///
210    /// # Returns
211    /// - `Ok`: A tuple `(f64, f64)` containing the `x` and `y` values.
212    /// - `Err`: A `CurvesError` if either `x` or `y` cannot be converted from
213    ///   `Decimal` to `f64` (e.g., out-of-range value).
214    ///
215    /// # Errors
216    /// Returns a `CurvesError::Point2DError` with a reason explaining the failure.
217    pub fn to_f64_tuple(&self) -> Result<(f64, f64), CurveError> {
218        let x = decimal_to_f64(self.x).ok();
219        let y = decimal_to_f64(self.y).ok();
220
221        match (x, y) {
222            (Some(x), Some(y)) => Ok((x, y)),
223            _ => Err(CurveError::Point2DError {
224                reason: "Error converting Decimal to f64",
225            }),
226        }
227    }
228
229    /// Creates a new `Point2D` instance from a tuple of `(f64, f64)` values.
230    ///
231    /// # Parameters
232    /// - `x`: The x-coordinate of the point as a `f64`.
233    /// - `y`: The y-coordinate of the point as a `f64`.
234    ///
235    /// # Returns
236    /// - `Ok`: A new `Point2D` instance if both `x` and `y` values can be successfully
237    ///   converted from `f64` to `Decimal`.
238    /// - `Err`: A `CurvesError` if the conversion fails (e.g., invalid precision).
239    ///
240    /// # Errors
241    /// Returns a `CurvesError::Point2DError` with a reason if either `x` or `y` could not be
242    /// converted from `f64`.
243    pub fn from_f64_tuple(x: f64, y: f64) -> Result<Self, CurveError> {
244        let x = Decimal::from_f64(x);
245        let y = Decimal::from_f64(y);
246        match (x, y) {
247            (Some(x), Some(y)) => Ok(Self::new(x, y)),
248            _ => Err(CurveError::Point2DError {
249                reason: "Error converting f64 to Decimal",
250            }),
251        }
252    }
253}
254
255impl From<&Point2D> for Point2D {
256    fn from(point: &Point2D) -> Self {
257        *point
258    }
259}
260
261impl HasX for Point2D {
262    fn get_x(&self) -> Decimal {
263        self.x
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use optionstratlib_core::model::is_positive;
271    use rust_decimal::Decimal;
272    use rust_decimal_macros::dec;
273
274    #[test]
275    fn test_is_positive_x_must_be_positive() {
276        let point = Point2D {
277            x: Decimal::ZERO,
278            y: dec!(1.0),
279        };
280
281        let result = if is_positive::<Decimal>() && point.x <= Decimal::ZERO {
282            Err(CurveError::Point2DError {
283                reason: "x must be positive for type T",
284            })
285        } else {
286            Ok(())
287        };
288
289        assert!(result.is_ok());
290    }
291
292    #[test]
293    fn test_is_positive_y_must_be_positive() {
294        let point = Point2D {
295            x: dec!(1.0),
296            y: Decimal::ZERO,
297        };
298
299        let result = if is_positive::<Decimal>() && point.y <= Decimal::ZERO {
300            Err(CurveError::Point2DError {
301                reason: "y must be positive for type U",
302            })
303        } else {
304            Ok(())
305        };
306
307        assert!(result.is_ok());
308    }
309
310    #[test]
311    fn test_to_f64_tuple_success() {
312        let point = Point2D {
313            x: dec!(1.0),
314            y: dec!(2.0),
315        };
316        let result = point.to_f64_tuple();
317        assert!(result.is_ok());
318    }
319
320    #[test]
321    fn test_from_f64_tuple_success() {
322        let result = Point2D::from_f64_tuple(1.0, 2.0);
323        assert!(result.is_ok());
324        let point = result.unwrap();
325        assert_eq!(point.x, dec!(1.0));
326        assert_eq!(point.y, dec!(2.0));
327    }
328
329    #[test]
330    fn test_from_f64_tuple_error() {
331        let result = Point2D::from_f64_tuple(f64::INFINITY, 2.0);
332        assert!(result.is_err());
333        match result {
334            Err(CurveError::Point2DError { reason }) => {
335                assert_eq!(reason, "Error converting f64 to Decimal");
336            }
337            _ => panic!("Unexpected error type"),
338        }
339    }
340
341    /// Equality reads both coordinates.
342    ///
343    /// This test used to assert `p1 == p3` and `p1 == p4` — that points
344    /// sharing an abscissa were equal whatever their ordinate. That is the
345    /// contract this type no longer has: `test_comparison_operators` below
346    /// asserts `p1 < p3` on the very same two values, and `Ord` requires
347    /// `a == b` exactly when `a.cmp(&b)` is `Equal`. Only one of the two
348    /// could survive, and the ordering is the one the rest of the crate
349    /// relies on (`BTreeSet<Point2D>`, and `Point2D` as a surface index).
350    #[test]
351    fn test_equal() {
352        let p1 = Point2D::from_f64_tuple(1.0, 2.0).unwrap();
353        let p2 = Point2D::from_f64_tuple(1.0, 2.0).unwrap();
354        let p3 = Point2D::from_f64_tuple(1.0, 3.0).unwrap();
355        let p4 = Point2D::from_f64_tuple(1.0, 4.0).unwrap();
356        let p5 = Point2D::from_f64_tuple(2.0, 2.0).unwrap();
357
358        // Both coordinates equal.
359        assert_eq!(p1, p2);
360        // Same abscissa, different ordinate: distinct points.
361        assert_ne!(p1, p3);
362        assert_ne!(p1, p4);
363        // Different abscissa.
364        assert_ne!(p1, p5);
365    }
366
367    /// `Eq` and `Ord` agree, which is what `Ord` requires of an implementor.
368    #[test]
369    fn test_eq_agrees_with_ord() {
370        let p1 = Point2D::from_f64_tuple(1.0, 2.0).unwrap();
371        let p2 = Point2D::from_f64_tuple(1.0, 2.0).unwrap();
372        let p3 = Point2D::from_f64_tuple(1.0, 3.0).unwrap();
373
374        assert_eq!(p1.cmp(&p2), Ordering::Equal);
375        assert_eq!(p1 == p2, p1.cmp(&p2) == Ordering::Equal);
376        assert_eq!(p1 == p3, p1.cmp(&p3) == Ordering::Equal);
377    }
378
379    /// `Hash` agrees with `Eq`, including across `Decimal` scales: `1.0` and
380    /// `1.00` compare equal, so they must hash alike. The previous
381    /// implementation hashed the raw mantissa and scale and failed this.
382    #[test]
383    fn test_hash_agrees_with_eq() {
384        use std::collections::HashSet;
385
386        let p1 = Point2D::new(dec!(1.0), dec!(2.0));
387        let scaled = Point2D::new(dec!(1.00), dec!(2.000));
388        let other_y = Point2D::new(dec!(1.0), dec!(3.0));
389
390        assert_eq!(p1, scaled);
391        assert_ne!(p1, other_y);
392
393        let set: HashSet<Point2D> = [p1, scaled, other_y].into_iter().collect();
394        // `p1` and `scaled` are one entry; `other_y` is a second.
395        assert_eq!(set.len(), 2);
396        assert!(set.contains(&p1));
397        assert!(set.contains(&other_y));
398    }
399}
400
401#[cfg(test)]
402mod tests_point2d_serde {
403    use super::*;
404    use rust_decimal_macros::dec;
405    use serde_json::Value;
406
407    #[test]
408    fn test_basic_serialization() {
409        let point = Point2D {
410            x: dec!(1.5),
411            y: dec!(2.5),
412        };
413
414        let serialized = serde_json::to_string(&point).unwrap();
415        let deserialized: Point2D = serde_json::from_str(&serialized).unwrap();
416
417        assert_eq!(point.x, deserialized.x);
418        assert_eq!(point.y, deserialized.y);
419    }
420
421    #[test]
422    fn test_zero_values() {
423        let point = Point2D {
424            x: dec!(0.0),
425            y: dec!(0.0),
426        };
427
428        let serialized = serde_json::to_string(&point).unwrap();
429        let deserialized: Point2D = serde_json::from_str(&serialized).unwrap();
430
431        assert_eq!(point, deserialized);
432    }
433
434    #[test]
435    fn test_negative_values() {
436        let point = Point2D {
437            x: dec!(-1.5),
438            y: dec!(-2.5),
439        };
440
441        let serialized = serde_json::to_string(&point).unwrap();
442        let deserialized: Point2D = serde_json::from_str(&serialized).unwrap();
443
444        assert_eq!(point, deserialized);
445    }
446
447    #[test]
448    fn test_high_precision_values() {
449        let point = Point2D {
450            x: dec!(1.12345678901234567890),
451            y: dec!(2.12345678901234567890),
452        };
453
454        let serialized = serde_json::to_string(&point).unwrap();
455        let deserialized: Point2D = serde_json::from_str(&serialized).unwrap();
456
457        assert_eq!(point.x, deserialized.x);
458        assert_eq!(point.y, deserialized.y);
459    }
460
461    #[test]
462    fn test_json_structure() {
463        let point = Point2D {
464            x: dec!(1.5),
465            y: dec!(2.5),
466        };
467
468        let serialized = serde_json::to_string(&point).unwrap();
469        let json_value: Value = serde_json::from_str(&serialized).unwrap();
470
471        // Verify JSON structure
472        assert!(json_value.is_object());
473        assert_eq!(json_value.as_object().unwrap().len(), 2);
474        assert!(json_value.get("x").is_some());
475        assert!(json_value.get("y").is_some());
476    }
477
478    #[test]
479    fn test_pretty_print() {
480        let point = Point2D {
481            x: dec!(1.5),
482            y: dec!(2.5),
483        };
484
485        let serialized = serde_json::to_string_pretty(&point).unwrap();
486
487        // Verify pretty print format
488        assert!(serialized.contains('\n'));
489        assert!(serialized.contains("  "));
490
491        // Verify we can still deserialize pretty-printed JSON
492        let deserialized: Point2D = serde_json::from_str(&serialized).unwrap();
493        assert_eq!(point, deserialized);
494    }
495
496    #[test]
497    fn test_deserialize_from_integers() {
498        let json_str = r#"{"x": 1, "y": 2}"#;
499        let point: Point2D = serde_json::from_str(json_str).unwrap();
500
501        assert_eq!(point.x, dec!(1.0));
502        assert_eq!(point.y, dec!(2.0));
503    }
504
505    #[test]
506    fn test_deserialize_from_strings() {
507        let json_str = r#"{"x": "1.5", "y": "2.5"}"#;
508        let point: Point2D = serde_json::from_str(json_str).unwrap();
509
510        assert_eq!(point.x, dec!(1.5));
511        assert_eq!(point.y, dec!(2.5));
512    }
513
514    #[test]
515    fn test_invalid_json() {
516        // Missing field
517        let json_str = r#"{"x": 1.5}"#;
518        let result = serde_json::from_str::<Point2D>(json_str);
519        assert!(result.is_err());
520
521        // Invalid number format
522        let json_str = r#"{"x": "invalid", "y": 2.5}"#;
523        let result = serde_json::from_str::<Point2D>(json_str);
524        assert!(result.is_err());
525
526        // Wrong data type
527        let json_str = r#"{"x": true, "y": 2.5}"#;
528        let result = serde_json::from_str::<Point2D>(json_str);
529        assert!(result.is_err());
530    }
531
532    #[test]
533    fn test_max_values() {
534        let point = Point2D {
535            x: Decimal::MAX,
536            y: Decimal::MAX,
537        };
538
539        let serialized = serde_json::to_string(&point).unwrap();
540        let deserialized: Point2D = serde_json::from_str(&serialized).unwrap();
541
542        assert_eq!(point, deserialized);
543    }
544
545    #[test]
546    fn test_min_values() {
547        let point = Point2D {
548            x: Decimal::MIN,
549            y: Decimal::MIN,
550        };
551
552        let serialized = serde_json::to_string(&point).unwrap();
553        let deserialized: Point2D = serde_json::from_str(&serialized).unwrap();
554
555        assert_eq!(point, deserialized);
556    }
557
558    #[test]
559    fn test_json_to_vec() {
560        let points = vec![
561            Point2D {
562                x: dec!(1.0),
563                y: dec!(2.0),
564            },
565            Point2D {
566                x: dec!(3.0),
567                y: dec!(4.0),
568            },
569        ];
570
571        let serialized = serde_json::to_string(&points).unwrap();
572        let deserialized: Vec<Point2D> = serde_json::from_str(&serialized).unwrap();
573
574        assert_eq!(points, deserialized);
575    }
576
577    #[test]
578    fn test_duplicate_fields() {
579        let json_str = r#"{"x": 1.5, "y": 2.5, "x": 3.5}"#;
580        let result = serde_json::from_str::<Point2D>(json_str);
581        assert!(result.is_err());
582    }
583
584    #[test]
585    fn test_extra_fields() {
586        let json_str = r#"{"x": 1.5, "y": 2.5, "z": 3.5}"#;
587        let result = serde_json::from_str::<Point2D>(json_str);
588        assert!(result.is_ok());
589    }
590
591    #[test]
592    fn test_unknown_fields() {
593        let json_str = r#"{"x": 1.5, "r": 2.5, "z": 3.5}"#;
594        let result = serde_json::from_str::<Point2D>(json_str);
595        assert!(result.is_err());
596    }
597
598    #[test]
599    fn test_array() {
600        let json_str = "[1.5, 2.5]";
601        let result = serde_json::from_str::<Point2D>(json_str);
602        assert!(result.is_ok());
603    }
604}
605
606#[cfg(test)]
607mod tests_edge_cases {
608    use super::*;
609    use optionstratlib_core::model::Positive;
610    use rust_decimal_macros::dec;
611
612    #[test]
613    fn test_to_tuple_positive_constraint_x() {
614        // Create a point with negative x
615        let point = Point2D::new(dec!(-1.0), dec!(2.0));
616
617        // Try to convert to a tuple where T must be positive
618        let result: Result<(Positive, Decimal), _> = point.to_tuple();
619
620        // Should fail with appropriate error
621        assert!(result.is_err());
622        match result {
623            Err(CurveError::Point2DError { reason }) => {
624                assert_eq!(reason, "x must be positive for type T");
625            }
626            _ => panic!("Unexpected error type"),
627        }
628    }
629
630    #[test]
631    fn test_to_tuple_positive_constraint_y() {
632        // Create a point with negative y
633        let point = Point2D::new(dec!(1.0), dec!(-2.0));
634
635        // Try to convert to a tuple where U must be positive
636        let result: Result<(Decimal, Positive), _> = point.to_tuple();
637
638        // Should fail with appropriate error
639        assert!(result.is_err());
640        match result {
641            Err(CurveError::Point2DError { reason }) => {
642                assert_eq!(reason, "y must be positive for type U");
643            }
644            _ => panic!("Unexpected error type"),
645        }
646    }
647
648    #[test]
649    fn test_to_tuple_positive_constraint_both_pass() {
650        // Create a point with positive x and y
651        let point = Point2D::new(dec!(1.0), dec!(2.0));
652
653        // Try to convert to a tuple where both T and U must be positive
654        let result: Result<(Positive, Positive), _> = point.to_tuple();
655
656        // Should succeed
657        assert!(result.is_ok());
658        let (x, y) = result.unwrap();
659        assert_eq!(x.value(), dec!(1.0));
660        assert_eq!(y.value(), dec!(2.0));
661    }
662
663    #[test]
664    fn test_debug_output() {
665        let point = Point2D::new(dec!(3.5), dec!(-2.25));
666        let debug_str = format!("{point:?}");
667
668        // Debug output should contain both x and y values
669        assert!(debug_str.contains("3.5"));
670        assert!(debug_str.contains("-2.25"));
671    }
672
673    /// Ordering is lexicographic on `(x, y)`.
674    ///
675    /// The `p1 < p3` block below is unchanged, but it used to contradict
676    /// `test_equal`, which asserted `p1 == p3` on the same two values: a
677    /// point could be both equal to and less than another. `PartialEq` now
678    /// reads `y` as well, so the two tests agree.
679    #[test]
680    fn test_comparison_operators() {
681        let p1 = Point2D::new(dec!(1.0), dec!(2.0));
682        let p2 = Point2D::new(dec!(2.0), dec!(1.0));
683        let p3 = Point2D::new(dec!(1.0), dec!(3.0));
684
685        // Test comparison operators
686        assert!(p1 < p2);
687        assert!(p1 <= p2);
688        assert!(p2 > p1);
689        assert!(p2 >= p1);
690
691        // Same x, different y: ordered by y, and not equal.
692        assert!(p1 < p3);
693        assert!(p1 <= p3);
694        assert!(p3 > p1);
695        assert!(p3 >= p1);
696        assert_ne!(p1, p3);
697    }
698
699    /// Sorting keeps both coordinates: by `x` first, then by `y`.
700    ///
701    /// This test is unchanged and is the reason an x-only `Ord` was not an
702    /// option: the two points at `x = 1` must sort by their ordinate, not
703    /// land in whatever order the input happened to have.
704    #[test]
705    fn test_ordering_consistency() {
706        let points = vec![
707            Point2D::new(dec!(3.0), dec!(1.0)),
708            Point2D::new(dec!(1.0), dec!(3.0)),
709            Point2D::new(dec!(2.0), dec!(2.0)),
710            Point2D::new(dec!(1.0), dec!(1.0)),
711        ];
712
713        let mut sorted = points.clone();
714        sorted.sort();
715
716        // Expect points to be sorted by x-coordinate first, then by y
717        assert_eq!(sorted[0].x, dec!(1.0));
718        assert_eq!(sorted[0].y, dec!(1.0));
719
720        assert_eq!(sorted[1].x, dec!(1.0));
721        assert_eq!(sorted[1].y, dec!(3.0));
722
723        assert_eq!(sorted[2].x, dec!(2.0));
724        assert_eq!(sorted[2].y, dec!(2.0));
725
726        assert_eq!(sorted[3].x, dec!(3.0));
727        assert_eq!(sorted[3].y, dec!(1.0));
728    }
729}
730
731#[cfg(test)]
732mod tests_performance {
733    use super::*;
734    use std::time::{Duration, Instant};
735
736    #[test]
737    #[ignore]
738    fn test_creation_performance() {
739        let start = Instant::now();
740
741        for i in 0..10000 {
742            let x = Decimal::from(i);
743            let y = Decimal::from(i * 2);
744            let _ = Point2D::new(x, y);
745        }
746
747        let duration = start.elapsed();
748        // Ensure creation is reasonably fast (adjust threshold as needed)
749        assert!(duration < Duration::from_millis(100));
750    }
751
752    #[test]
753    #[ignore]
754    fn test_to_tuple_performance() {
755        let points: Vec<Point2D> = (0..10000)
756            .map(|i| Point2D::new(Decimal::from(i), Decimal::from(i * 2)))
757            .collect();
758
759        let start = Instant::now();
760
761        for point in &points {
762            let _: Result<(Decimal, Decimal), _> = point.to_tuple();
763        }
764
765        let duration = start.elapsed();
766        // Ensure conversion is reasonably fast (adjust threshold as needed)
767        assert!(duration < Duration::from_millis(100));
768    }
769
770    #[test]
771    #[ignore]
772    fn test_comparison_performance() {
773        let points: Vec<Point2D> = (0..10000)
774            .map(|i| Point2D::new(Decimal::from(i % 100), Decimal::from(i / 100)))
775            .collect();
776
777        let start = Instant::now();
778
779        // Sort points (exercises comparison operators)
780        let mut sorted = points.clone();
781        sorted.sort();
782
783        let duration = start.elapsed();
784        // Ensure sorting is reasonably fast (adjust threshold as needed)
785        assert!(duration < Duration::from_millis(200));
786    }
787
788    #[test]
789    #[ignore]
790    fn test_from_f64_tuple_performance() {
791        let start = Instant::now();
792
793        for i in 0..10000 {
794            let _ = Point2D::from_f64_tuple(i as f64, (i * 2) as f64);
795        }
796
797        let duration = start.elapsed();
798        // Ensure conversion is reasonably fast (adjust threshold as needed)
799        assert!(duration < Duration::from_millis(200));
800    }
801
802    #[test]
803    #[ignore]
804    fn test_to_f64_tuple_performance() {
805        let points: Vec<Point2D> = (0..10000)
806            .map(|i| Point2D::new(Decimal::from(i), Decimal::from(i * 2)))
807            .collect();
808
809        let start = Instant::now();
810
811        for point in &points {
812            let _ = point.to_f64_tuple();
813        }
814
815        let duration = start.elapsed();
816        // Ensure conversion is reasonably fast (adjust threshold as needed)
817        assert!(duration < Duration::from_millis(200));
818    }
819}
820
821#[cfg(test)]
822mod tests_point2d_specific_cases {
823    use super::*;
824    use optionstratlib_core::model::Positive;
825
826    use crate::error::CurveError;
827    use rust_decimal_macros::dec;
828
829    #[test]
830    fn test_to_tuple_positive_constraints() {
831        // Create a point with non-positive x (x = 0)
832        let point = Point2D::new(dec!(0.0), dec!(1.0));
833
834        // Test conversion where T must be positive
835        let result: Result<(Positive, Decimal), _> = point.to_tuple();
836
837        // Should fail with appropriate error
838        assert!(result.is_err());
839        match result {
840            Err(CurveError::Point2DError { reason }) => {
841                assert_eq!(reason, "x must be positive for type T");
842            }
843            _ => panic!("Expected Point2DError"),
844        }
845
846        // Create a point with non-positive y (y = 0)
847        let point = Point2D::new(dec!(1.0), dec!(0.0));
848
849        // Test conversion where U must be positive
850        let result: Result<(Decimal, Positive), _> = point.to_tuple();
851
852        // Should fail with appropriate error
853        assert!(result.is_err());
854        match result {
855            Err(CurveError::Point2DError { reason }) => {
856                assert_eq!(reason, "y must be positive for type U");
857            }
858            _ => panic!("Expected Point2DError"),
859        }
860    }
861
862    #[test]
863    fn test_from_f64_tuple_error_handling() {
864        // Test with invalid f64 values (infinity)
865        let result = Point2D::from_f64_tuple(f64::INFINITY, 1.0);
866        assert!(result.is_err());
867        match result {
868            Err(CurveError::Point2DError { reason }) => {
869                assert_eq!(reason, "Error converting f64 to Decimal");
870            }
871            _ => panic!("Expected Point2DError"),
872        }
873
874        // Test with NaN
875        let result = Point2D::from_f64_tuple(1.0, f64::NAN);
876        assert!(result.is_err());
877        match result {
878            Err(CurveError::Point2DError { reason }) => {
879                assert_eq!(reason, "Error converting f64 to Decimal");
880            }
881            _ => panic!("Expected Point2DError"),
882        }
883    }
884}