1use 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#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
52#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
53pub struct Point2D {
54 pub x: Decimal,
57
58 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
69impl 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 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
113impl 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 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 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 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 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 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 #[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 assert_eq!(p1, p2);
360 assert_ne!(p1, p3);
362 assert_ne!(p1, p4);
363 assert_ne!(p1, p5);
365 }
366
367 #[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 #[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 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 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 assert!(serialized.contains('\n'));
489 assert!(serialized.contains(" "));
490
491 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 let json_str = r#"{"x": 1.5}"#;
518 let result = serde_json::from_str::<Point2D>(json_str);
519 assert!(result.is_err());
520
521 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 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 let point = Point2D::new(dec!(-1.0), dec!(2.0));
616
617 let result: Result<(Positive, Decimal), _> = point.to_tuple();
619
620 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 let point = Point2D::new(dec!(1.0), dec!(-2.0));
634
635 let result: Result<(Decimal, Positive), _> = point.to_tuple();
637
638 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 let point = Point2D::new(dec!(1.0), dec!(2.0));
652
653 let result: Result<(Positive, Positive), _> = point.to_tuple();
655
656 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 assert!(debug_str.contains("3.5"));
670 assert!(debug_str.contains("-2.25"));
671 }
672
673 #[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 assert!(p1 < p2);
687 assert!(p1 <= p2);
688 assert!(p2 > p1);
689 assert!(p2 >= p1);
690
691 assert!(p1 < p3);
693 assert!(p1 <= p3);
694 assert!(p3 > p1);
695 assert!(p3 >= p1);
696 assert_ne!(p1, p3);
697 }
698
699 #[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 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 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 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 let mut sorted = points.clone();
781 sorted.sort();
782
783 let duration = start.elapsed();
784 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 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 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 let point = Point2D::new(dec!(0.0), dec!(1.0));
833
834 let result: Result<(Positive, Decimal), _> = point.to_tuple();
836
837 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 let point = Point2D::new(dec!(1.0), dec!(0.0));
848
849 let result: Result<(Decimal, Positive), _> = point.to_tuple();
851
852 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 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 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}