Skip to main content

optionstratlib_math/surfaces/
types.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 20/1/25
5******************************************************************************/
6use crate::curves::Point2D;
7use crate::error::SurfaceError;
8use crate::geometrics::HasX;
9use num_traits::FromPrimitive;
10use optionstratlib_core::model::decimal::decimal_to_f64;
11use optionstratlib_core::model::is_positive;
12use rust_decimal::Decimal;
13use serde::{Deserialize, Serialize};
14use std::cmp::Ordering;
15use std::fmt::Display;
16use std::hash::{Hash, Hasher};
17
18/// Represents a point in three-dimensional space with `x`, `y` and `z` coordinates.
19///
20/// # Overview
21/// The `Point3D` struct defines a point in a 3D Cartesian coordinate system.
22/// All coordinates (`x`, `y`, and `z`) are stored as `Decimal` values to provide high precision,
23/// making it suitable for applications requiring accurate numerical calculations.
24///
25/// # Fields
26/// - **x**: The x-coordinate of the point, represented as a `Decimal`
27/// - **y**: The y-coordinate of the point, represented as a `Decimal`
28/// - **z**: The z-coordinate of the point, represented as a `Decimal`
29///
30/// # Examples
31/// ```rust
32/// use rust_decimal_macros::dec;
33/// use optionstratlib_math::surfaces::Point3D;
34///
35/// // Create a new 3D point
36/// let point = Point3D {
37///     x: dec!(1.5),
38///     y: dec!(2.0),
39///     z: dec!(-3.25),
40/// };
41/// ```
42///
43/// # Usage
44/// `Point3D` is primarily used within the surface module to represent vertices
45/// of 3D surfaces and for various geometric calculations. The high-precision `Decimal`
46/// type ensures accuracy in scientific and engineering applications.
47#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
48#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
49#[serde(deny_unknown_fields)]
50pub struct Point3D {
51    /// The x-coordinate in the Cartesian system
52    pub x: Decimal,
53    /// The y-coordinate in the Cartesian system
54    pub y: Decimal,
55    /// The z-coordinate in the Cartesian system
56    pub z: Decimal,
57}
58
59impl Display for Point3D {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(f, "(x: {}, y: {}, z: {})", self.x, self.y, self.z)
62    }
63}
64
65/// Two points are equal when all three coordinates are equal.
66///
67/// `PartialEq`, `Ord` and `Hash` all read the full `(x, y, z)` triple, so
68/// `a == b` holds exactly when `a.cmp(&b)` is [`Ordering::Equal`], and equal
69/// points hash alike, as `Ord` requires.
70///
71/// The "one `z` per `(x, y)`" rule is a property of a *surface*, not of a
72/// point, and it is a container-level invariant that nothing enforces:
73/// [`crate::surfaces::Surface::new`] retains a repeated `(x, y)` coordinate,
74/// and `points` is a `pub` field, so no constructor could. The rule and what
75/// each consumer does when it is broken are documented on
76/// [`crate::surfaces::Surface::new`].
77impl PartialEq for Point3D {
78    fn eq(&self, other: &Self) -> bool {
79        self.x == other.x && self.y == other.y && self.z == other.z
80    }
81}
82
83impl Eq for Point3D {}
84
85impl Hash for Point3D {
86    fn hash<H: Hasher>(&self, state: &mut H) {
87        // Delegate to `Decimal`'s own `Hash`, which normalizes first, so that
88        // `dec!(1.0)` and `dec!(1.00)` — which compare equal — hash alike.
89        self.x.hash(state);
90        self.y.hash(state);
91        self.z.hash(state);
92    }
93}
94
95impl PartialOrd for Point3D {
96    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
97        Some(self.cmp(other))
98    }
99}
100
101/// Orders on `(x, y, z)`, lexicographically: the same triple [`PartialEq`]
102/// reads, so `cmp` returns [`Ordering::Equal`] exactly when the points are
103/// equal.
104impl Ord for Point3D {
105    fn cmp(&self, other: &Self) -> Ordering {
106        match self.x.cmp(&other.x) {
107            Ordering::Equal => match self.y.cmp(&other.y) {
108                Ordering::Equal => self.z.cmp(&other.z),
109                y_ordering => y_ordering,
110            },
111            x_ordering => x_ordering,
112        }
113    }
114}
115
116impl Point3D {
117    /// Creates a new instance of `Point3D` using the specified coordinates.
118    ///
119    /// # Parameters
120    /// - `x`: The x-coordinate, which implements `Into<Decimal>`
121    /// - `y`: The y-coordinate, which implements `Into<Decimal>`
122    /// - `z`: The z-coordinate, which implements `Into<Decimal>`
123    pub fn new<T: Into<Decimal>, U: Into<Decimal>, V: Into<Decimal>>(x: T, y: U, z: V) -> Self {
124        Self {
125            x: x.into(),
126            y: y.into(),
127            z: z.into(),
128        }
129    }
130
131    /// Converts the Point3D to a tuple of three values.
132    ///
133    /// # Type Parameters
134    /// - `T`: Type for x-coordinate
135    /// - `U`: Type for y-coordinate
136    /// - `V`: Type for z-coordinate
137    ///
138    /// # Errors
139    ///
140    /// Returns `SurfaceError::Point3DError` in three situations:
141    ///
142    /// - when one of the target types is `Positive`-like
143    ///   (detected via the `is_positive::<T>()` / `is_positive::<U>()`
144    ///   / `is_positive::<V>()` checks) and the corresponding
145    ///   coordinate is less than or equal to zero.
146    /// - when one of the `Decimal` coordinates cannot be converted
147    ///   into the target numeric type (e.g. out-of-range for `f32`,
148    ///   overflow for `u32`).
149    pub fn to_tuple<
150        T: TryFrom<Decimal> + 'static,
151        U: TryFrom<Decimal> + 'static,
152        V: TryFrom<Decimal> + 'static,
153    >(
154        &self,
155    ) -> Result<(T, U, V), SurfaceError> {
156        if is_positive::<T>() && self.x <= Decimal::ZERO {
157            return Err(SurfaceError::Point3DError {
158                reason: "x must be positive for type T",
159            });
160        }
161
162        if is_positive::<U>() && self.y <= Decimal::ZERO {
163            return Err(SurfaceError::Point3DError {
164                reason: "y must be positive for type U",
165            });
166        }
167
168        if is_positive::<V>() && self.z <= Decimal::ZERO {
169            return Err(SurfaceError::Point3DError {
170                reason: "z must be positive for type V",
171            });
172        }
173
174        let x = T::try_from(self.x).map_err(|_| SurfaceError::Point3DError {
175            reason: "failed to convert x",
176        })?;
177        let y = U::try_from(self.y).map_err(|_| SurfaceError::Point3DError {
178            reason: "failed to convert y",
179        })?;
180        let z = V::try_from(self.z).map_err(|_| SurfaceError::Point3DError {
181            reason: "failed to convert z",
182        })?;
183
184        Ok((x, y, z))
185    }
186
187    /// Creates a Point3D from a tuple of three values.
188    ///
189    /// # Errors
190    ///
191    /// Currently infallible for the blanket `Into<Decimal>`
192    /// bounds; the `Result` signature is retained so future
193    /// implementations that can reject non-finite or out-of-range
194    /// inputs (e.g. via `TryFrom<f64>`) can surface
195    /// [`SurfaceError::Point3DError`] without a breaking change.
196    pub fn from_tuple<T: Into<Decimal>, U: Into<Decimal>, V: Into<Decimal>>(
197        x: T,
198        y: U,
199        z: V,
200    ) -> Result<Self, SurfaceError> {
201        Ok(Self::new(x, y, z))
202    }
203
204    /// Converts the Point3D to a tuple of f64 values.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`SurfaceError::Point3DError`] when any coordinate
209    /// cannot be represented as an `f64` (typically a `Decimal`
210    /// magnitude that exceeds the `f64` range).
211    pub fn to_f64_tuple(&self) -> Result<(f64, f64, f64), SurfaceError> {
212        let x = decimal_to_f64(self.x).ok();
213        let y = decimal_to_f64(self.y).ok();
214        let z = decimal_to_f64(self.z).ok();
215
216        match (x, y, z) {
217            (Some(x), Some(y), Some(z)) => Ok((x, y, z)),
218            _ => Err(SurfaceError::Point3DError {
219                reason: "Error converting Decimal to f64",
220            }),
221        }
222    }
223
224    /// Creates a Point3D from a tuple of f64 values.
225    ///
226    /// # Errors
227    ///
228    /// Returns [`SurfaceError::Point3DError`] when any of `x`,
229    /// `y` or `z` is `NaN` or infinite and therefore not
230    /// representable as a `Decimal`.
231    pub fn from_f64_tuple(x: f64, y: f64, z: f64) -> Result<Self, SurfaceError> {
232        let x = Decimal::from_f64(x);
233        let y = Decimal::from_f64(y);
234        let z = Decimal::from_f64(z);
235
236        match (x, y, z) {
237            (Some(x), Some(y), Some(z)) => Ok(Self::new(x, y, z)),
238            _ => Err(SurfaceError::Point3DError {
239                reason: "Error converting f64 to Decimal",
240            }),
241        }
242    }
243
244    /// Converts this `Point3D` instance to a `Point2D` by projecting onto the XY plane.
245    ///
246    /// # Overview
247    /// This method creates a new `Point2D` instance using only the `x` and `y` coordinates
248    /// of the current `Point3D` object, effectively projecting the 3D point onto the XY plane.
249    /// The `z` coordinate is discarded in this operation.
250    ///
251    /// # Returns
252    /// A heap-allocated `Point2D` instance (`Box<Point2D>`) containing the `x` and `y` coordinates
253    /// from this `Point3D` object.
254    ///
255    /// # Example
256    /// ```
257    /// use rust_decimal_macros::dec;
258    /// use optionstratlib_math::surfaces::Point3D;
259    ///
260    /// let point3d = Point3D { x: dec!(1.5), y: dec!(2.0), z: dec!(3.5) };
261    /// let point2d = point3d.point2d();
262    /// assert_eq!(point2d.x, dec!(1.5));
263    /// assert_eq!(point2d.y, dec!(2.0));
264    /// ```
265    ///
266    /// # Use Cases
267    /// This method is useful when:
268    /// - Working with visualization that requires 2D projections of 3D points
269    /// - Analyzing specific planes within a 3D model
270    /// - Converting between coordinate systems from 3D to 2D
271    #[must_use]
272    pub fn point2d(&self) -> Box<Point2D> {
273        Box::new(Point2D::new(self.x, self.y))
274    }
275}
276
277impl From<&Point3D> for Point3D {
278    fn from(point: &Point3D) -> Self {
279        *point
280    }
281}
282
283impl HasX for Point3D {
284    fn get_x(&self) -> Decimal {
285        self.x
286    }
287}
288
289/// Represents the three possible axes in a 3D space.
290///
291/// This enumeration is commonly used to define or manipulate directions
292/// or dimensions within a 3D coordinate system.
293///
294/// ## Variants
295/// - `X`: The axis representing the horizontal direction.
296/// - `Y`: The axis representing the vertical direction or elevation.
297/// - `Z`: The axis representing depth or the third dimension.
298#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
299#[repr(u8)]
300pub enum Axis {
301    X,
302    Y,
303    Z,
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    use crate::surfaces::Surface;
311    use optionstratlib_core::{model::Positive, pos_or_panic};
312    use rust_decimal_macros::dec;
313    use std::collections::BTreeSet;
314
315    #[test]
316    fn test_point3d_new() {
317        let point = Point3D::new(dec!(1.0), dec!(2.0), dec!(3.0));
318        assert_eq!(point.x, dec!(1.0));
319        assert_eq!(point.y, dec!(2.0));
320        assert_eq!(point.z, dec!(3.0));
321
322        let display = format!("{point}");
323        assert_eq!(display, "(x: 1.0, y: 2.0, z: 3.0)");
324
325        let point2d = point.point2d();
326        assert_eq!(point2d.x, dec!(1.0));
327        assert_eq!(point2d.y, dec!(2.0));
328
329        let has_x = point.get_x();
330        assert_eq!(has_x, dec!(1.0));
331    }
332
333    #[test]
334    fn test_point3d_negative() {
335        let point = Point3D::new(dec!(-1.0), dec!(-2.0), dec!(-3.0));
336        assert_eq!(point.x, dec!(-1.0));
337        assert_eq!(point.y, dec!(-2.0));
338        assert_eq!(point.z, dec!(-3.0));
339
340        let display = format!("{point}");
341        assert_eq!(display, "(x: -1.0, y: -2.0, z: -3.0)");
342
343        let tuple = point.to_tuple::<Positive, Decimal, Decimal>();
344        assert!(tuple.is_err());
345        let tuple = point.to_tuple::<Decimal, Positive, Decimal>();
346        assert!(tuple.is_err());
347        let tuple = point.to_tuple::<Decimal, Decimal, Positive>();
348        assert!(tuple.is_err());
349    }
350
351    #[test]
352    fn test_point3d_to_tuple() {
353        let result = Point3D::from_f64_tuple(1.0, 2.0, 3.0);
354        assert!(result.is_ok());
355        let point = result.unwrap();
356        let result: Result<(Decimal, Decimal, Decimal), _> = point.to_tuple();
357        assert!(result.is_ok());
358        let (x, y, z) = result.unwrap();
359        assert_eq!(x, dec!(1.0));
360        assert_eq!(y, dec!(2.0));
361        assert_eq!(z, dec!(3.0));
362    }
363
364    #[test]
365    fn test_point3d_from_tuple() {
366        let result = Point3D::from_tuple(dec!(1.0), Positive::TWO, pos_or_panic!(3.0));
367        assert!(result.is_ok());
368        let point = result.unwrap();
369        assert_eq!(point.x, dec!(1.0));
370        assert_eq!(point.y, dec!(2.0));
371        assert_eq!(point.z, dec!(3.0));
372    }
373
374    #[test]
375    fn test_point3d_to_f64_tuple() {
376        let result = Point3D::from_f64_tuple(1.0, 2.0, 3.0);
377        assert!(result.is_ok());
378        let point = result.unwrap();
379        let result = point.to_f64_tuple();
380        assert!(result.is_ok());
381        let (x, y, z) = result.unwrap();
382        assert_eq!(x, 1.0);
383        assert_eq!(y, 2.0);
384        assert_eq!(z, 3.0);
385    }
386
387    #[test]
388    fn test_point3d_from_f64_tuple() {
389        let result = Point3D::from_f64_tuple(1.0, 2.0, 3.0);
390        assert!(result.is_ok());
391        let point = result.unwrap();
392        assert_eq!(point.x, dec!(1.0));
393        assert_eq!(point.y, dec!(2.0));
394        assert_eq!(point.z, dec!(3.0));
395    }
396
397    /// Ordering is lexicographic on `(x, y, z)`.
398    ///
399    /// The assertions are unchanged; `p1 < p2` used to contradict
400    /// `test_equal`, which asserted `p1 == p2` on those same values. Only
401    /// `PartialEq` moved.
402    #[test]
403    fn test_point3d_ordering() {
404        let p1 = Point3D::from_f64_tuple(1.0, 2.0, 3.0).unwrap();
405        let p2 = Point3D::from_f64_tuple(1.0, 2.0, 4.0).unwrap();
406        let p3 = Point3D::from_f64_tuple(1.0, 3.0, 1.0).unwrap();
407        let p4 = Point3D::from_f64_tuple(2.0, 1.0, 1.0).unwrap();
408
409        assert!(p1 < p2); // Same x,y, different z
410        assert!(p1 < p3); // Same x, different y
411        assert!(p1 < p4); // Different x
412
413        // ... and none of them is equal, which is what `Ord` demands of a
414        // strict ordering.
415        assert_ne!(p1, p2);
416        assert_ne!(p1, p3);
417        assert_ne!(p1, p4);
418    }
419
420    #[test]
421    fn test_surface_new() {
422        let points = BTreeSet::from_iter(vec![
423            Point3D::from_f64_tuple(0.0, 0.0, 0.0).unwrap(),
424            Point3D::from_f64_tuple(1.0, 1.0, 1.0).unwrap(),
425        ]);
426        let surface = Surface::new(points.clone());
427
428        assert_eq!(surface.points, points);
429    }
430
431    #[test]
432    fn test_error_handling_invalid_f64() {
433        let result = Point3D::from_f64_tuple(f64::INFINITY, 2.0, 3.0);
434        assert!(result.is_err());
435        match result {
436            Err(SurfaceError::Point3DError { reason }) => {
437                assert_eq!(reason, "Error converting f64 to Decimal");
438            }
439            _ => panic!("Unexpected error type"),
440        }
441    }
442
443    /// Equality reads all three coordinates.
444    ///
445    /// This test used to assert `p1 == p3` — that two points sharing an
446    /// xy-coordinate were equal whatever their height. `test_point3d_ordering`
447    /// asserts `p1 < p3` on those same two values, so a point was both equal
448    /// to and less than another, which `Ord` forbids. `PartialEq` now reads
449    /// `z` as well and the two tests agree.
450    #[test]
451    fn test_equal() {
452        let p1 = Point3D::from_f64_tuple(1.0, 2.0, 3.0).unwrap();
453        let p2 = Point3D::from_f64_tuple(1.0, 2.0, 3.0).unwrap();
454        let p3 = Point3D::from_f64_tuple(1.0, 2.0, 4.0).unwrap();
455        let p4 = Point3D::from_f64_tuple(1.0, 3.0, 3.0).unwrap();
456        let p5 = Point3D::from_f64_tuple(2.0, 2.0, 3.0).unwrap();
457
458        // All three coordinates equal.
459        assert_eq!(p1, p2);
460        // Same xy-coordinate, different height: distinct points.
461        assert_ne!(p1, p3);
462        assert_ne!(p1, p4);
463        assert_ne!(p1, p5);
464    }
465
466    /// `Eq` and `Ord` agree, which is what `Ord` requires of an implementor.
467    #[test]
468    fn test_eq_agrees_with_ord() {
469        let p1 = Point3D::from_f64_tuple(1.0, 2.0, 3.0).unwrap();
470        let p2 = Point3D::from_f64_tuple(1.0, 2.0, 3.0).unwrap();
471        let p3 = Point3D::from_f64_tuple(1.0, 2.0, 4.0).unwrap();
472
473        assert_eq!(p1.cmp(&p2), Ordering::Equal);
474        assert_eq!(p1 == p2, p1.cmp(&p2) == Ordering::Equal);
475        assert_eq!(p1 == p3, p1.cmp(&p3) == Ordering::Equal);
476    }
477
478    /// `Hash` agrees with `Eq`, including across `Decimal` scales. `Point3D`
479    /// had no `Hash` impl at all before, so it could not be used as a hash
480    /// key; this pins the contract now that it can.
481    #[test]
482    fn test_hash_agrees_with_eq() {
483        use std::collections::HashSet;
484
485        let p1 = Point3D::new(dec!(1.0), dec!(2.0), dec!(3.0));
486        let scaled = Point3D::new(dec!(1.00), dec!(2.000), dec!(3.0000));
487        let other_z = Point3D::new(dec!(1.0), dec!(2.0), dec!(4.0));
488
489        assert_eq!(p1, scaled);
490        assert_ne!(p1, other_z);
491
492        let set: HashSet<Point3D> = [p1, scaled, other_z].into_iter().collect();
493        // `p1` and `scaled` are one entry; `other_z` is a second.
494        assert_eq!(set.len(), 2);
495        assert!(set.contains(&p1));
496        assert!(set.contains(&other_z));
497    }
498}
499
500#[cfg(test)]
501mod tests_point3d_serde {
502    use super::*;
503    use rust_decimal_macros::dec;
504    use serde_json::Value;
505
506    #[test]
507    fn test_basic_serialization() {
508        let point = Point3D {
509            x: dec!(1.5),
510            y: dec!(2.5),
511            z: dec!(3.5),
512        };
513
514        let serialized = serde_json::to_string(&point).unwrap();
515        let deserialized: Point3D = serde_json::from_str(&serialized).unwrap();
516
517        assert_eq!(point.x, deserialized.x);
518        assert_eq!(point.y, deserialized.y);
519        assert_eq!(point.z, deserialized.z);
520    }
521
522    #[test]
523    fn test_zero_values() {
524        let point = Point3D {
525            x: dec!(0.0),
526            y: dec!(0.0),
527            z: dec!(0.0),
528        };
529
530        let serialized = serde_json::to_string(&point).unwrap();
531        let deserialized: Point3D = serde_json::from_str(&serialized).unwrap();
532
533        assert_eq!(point, deserialized);
534    }
535
536    #[test]
537    fn test_negative_values() {
538        let point = Point3D {
539            x: dec!(-1.5),
540            y: dec!(-2.5),
541            z: dec!(-3.5),
542        };
543
544        let serialized = serde_json::to_string(&point).unwrap();
545        let deserialized: Point3D = serde_json::from_str(&serialized).unwrap();
546
547        assert_eq!(point, deserialized);
548    }
549
550    #[test]
551    fn test_high_precision_values() {
552        let point = Point3D {
553            x: dec!(1.12345678901234567890),
554            y: dec!(2.12345678901234567890),
555            z: dec!(3.12345678901234567890),
556        };
557
558        let serialized = serde_json::to_string(&point).unwrap();
559        let deserialized: Point3D = serde_json::from_str(&serialized).unwrap();
560
561        assert_eq!(point.x, deserialized.x);
562        assert_eq!(point.y, deserialized.y);
563        assert_eq!(point.z, deserialized.z);
564    }
565
566    #[test]
567    fn test_json_structure() {
568        let point = Point3D {
569            x: dec!(1.5),
570            y: dec!(2.5),
571            z: dec!(3.5),
572        };
573
574        let serialized = serde_json::to_string(&point).unwrap();
575        let json_value: Value = serde_json::from_str(&serialized).unwrap();
576
577        // Verify JSON structure
578        assert!(json_value.is_object());
579        assert_eq!(json_value.as_object().unwrap().len(), 3);
580        assert!(json_value.get("x").is_some());
581        assert!(json_value.get("y").is_some());
582        assert!(json_value.get("z").is_some());
583    }
584
585    #[test]
586    fn test_pretty_print() {
587        let point = Point3D {
588            x: dec!(1.5),
589            y: dec!(2.5),
590            z: dec!(3.5),
591        };
592
593        let serialized = serde_json::to_string_pretty(&point).unwrap();
594
595        // Verify pretty print format
596        assert!(serialized.contains('\n'));
597        assert!(serialized.contains("  "));
598
599        // Verify we can still deserialize pretty-printed JSON
600        let deserialized: Point3D = serde_json::from_str(&serialized).unwrap();
601        assert_eq!(point, deserialized);
602    }
603
604    #[test]
605    fn test_deserialize_from_integers() {
606        let json_str = r#"{"x": 1, "y": 2, "z": 3}"#;
607        let point: Point3D = serde_json::from_str(json_str).unwrap();
608
609        assert_eq!(point.x, dec!(1.0));
610        assert_eq!(point.y, dec!(2.0));
611        assert_eq!(point.z, dec!(3.0));
612    }
613
614    #[test]
615    fn test_deserialize_from_strings() {
616        let json_str = r#"{"x": "1.5", "y": "2.5", "z": "3.5"}"#;
617        let point: Point3D = serde_json::from_str(json_str).unwrap();
618
619        assert_eq!(point.x, dec!(1.5));
620        assert_eq!(point.y, dec!(2.5));
621        assert_eq!(point.z, dec!(3.5));
622    }
623
624    #[test]
625    fn test_invalid_json() {
626        // Missing field
627        let json_str = r#"{"x": 1.5, "y": 2.5}"#;
628        let result = serde_json::from_str::<Point3D>(json_str);
629        assert!(result.is_err());
630
631        // Invalid number format
632        let json_str = r#"{"x": "invalid", "y": 2.5, "z": 3.5}"#;
633        let result = serde_json::from_str::<Point3D>(json_str);
634        assert!(result.is_err());
635
636        // Wrong data type
637        let json_str = r#"{"x": true, "y": 2.5, "z": 3.5}"#;
638        let result = serde_json::from_str::<Point3D>(json_str);
639        assert!(result.is_err());
640    }
641
642    #[test]
643    fn test_max_values() {
644        let point = Point3D {
645            x: Decimal::MAX,
646            y: Decimal::MAX,
647            z: Decimal::MAX,
648        };
649
650        let serialized = serde_json::to_string(&point).unwrap();
651        let deserialized: Point3D = serde_json::from_str(&serialized).unwrap();
652
653        assert_eq!(point, deserialized);
654    }
655
656    #[test]
657    fn test_min_values() {
658        let point = Point3D {
659            x: Decimal::MIN,
660            y: Decimal::MIN,
661            z: Decimal::MIN,
662        };
663
664        let serialized = serde_json::to_string(&point).unwrap();
665        let deserialized: Point3D = serde_json::from_str(&serialized).unwrap();
666
667        assert_eq!(point, deserialized);
668    }
669
670    #[test]
671    fn test_json_to_vec() {
672        let points = vec![
673            Point3D {
674                x: dec!(1.0),
675                y: dec!(2.0),
676                z: dec!(3.0),
677            },
678            Point3D {
679                x: dec!(4.0),
680                y: dec!(5.0),
681                z: dec!(6.0),
682            },
683        ];
684
685        let serialized = serde_json::to_string(&points).unwrap();
686        let deserialized: Vec<Point3D> = serde_json::from_str(&serialized).unwrap();
687
688        assert_eq!(points, deserialized);
689    }
690
691    #[test]
692    fn test_array() {
693        let json_str = "[1.5,  2.5, 3.5]";
694        let result = serde_json::from_str::<Point3D>(json_str);
695        assert!(result.is_ok());
696    }
697}