Skip to main content

scirs2_core/integration/
conversion.rs

1//! Type Conversion Traits for Consistent Data Flow
2//!
3//! This module provides traits and utilities for converting data between
4//! different module representations with consistent error handling and
5//! optional precision tracking.
6//!
7//! # Features
8//!
9//! - **Lossless Conversion**: Convert without any data loss
10//! - **Lossy Conversion**: Convert with potential precision loss
11//! - **Array Conversion**: Convert between array types
12//! - **Cross-Module Conversion**: Unified interface for module interop
13
14use std::any::TypeId;
15use std::fmt;
16use std::marker::PhantomData;
17
18use crate::error::{CoreError, CoreResult, ErrorContext, ErrorLocation};
19use num_complex::Complex;
20use num_traits::{Float, NumCast, Zero};
21
22/// Error type for conversion operations
23#[derive(Debug, Clone, PartialEq)]
24pub enum ConversionError {
25    /// Type mismatch
26    TypeMismatch { expected: String, actual: String },
27    /// Value out of range for target type
28    OutOfRange {
29        value: String,
30        min: String,
31        max: String,
32    },
33    /// Precision loss would occur
34    PrecisionLoss {
35        original: String,
36        converted: String,
37        loss: f64,
38    },
39    /// Incompatible shapes
40    ShapeMismatch {
41        source: Vec<usize>,
42        target: Vec<usize>,
43    },
44    /// Invalid conversion operation
45    InvalidOperation(String),
46    /// Generic conversion failure
47    Failed(String),
48}
49
50impl fmt::Display for ConversionError {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match self {
53            ConversionError::TypeMismatch { expected, actual } => {
54                write!(f, "Type mismatch: expected {expected}, got {actual}")
55            }
56            ConversionError::OutOfRange { value, min, max } => {
57                write!(f, "Value {value} out of range [{min}, {max}]")
58            }
59            ConversionError::PrecisionLoss {
60                original,
61                converted,
62                loss,
63            } => {
64                write!(
65                    f,
66                    "Precision loss: {original} -> {converted} (loss: {loss:.2e})"
67                )
68            }
69            ConversionError::ShapeMismatch { source, target } => {
70                write!(f, "Shape mismatch: {source:?} vs {target:?}")
71            }
72            ConversionError::InvalidOperation(msg) => {
73                write!(f, "Invalid operation: {msg}")
74            }
75            ConversionError::Failed(msg) => {
76                write!(f, "Conversion failed: {msg}")
77            }
78        }
79    }
80}
81
82impl std::error::Error for ConversionError {}
83
84impl From<ConversionError> for CoreError {
85    fn from(err: ConversionError) -> Self {
86        CoreError::ValidationError(
87            ErrorContext::new(err.to_string()).with_location(ErrorLocation::new(file!(), line!())),
88        )
89    }
90}
91
92/// Result type for conversion operations
93pub type ConversionResult<T> = Result<T, ConversionError>;
94
95/// Options for conversion operations
96#[derive(Debug, Clone, PartialEq)]
97pub struct ConversionOptions {
98    /// Allow precision loss
99    pub allow_precision_loss: bool,
100    /// Maximum allowed relative error
101    pub max_relative_error: f64,
102    /// Clamp values to target range instead of failing
103    pub clamp_to_range: bool,
104    /// Round floating point to nearest integer
105    pub round_to_nearest: bool,
106    /// Preserve NaN values
107    pub preserve_nan: bool,
108    /// Preserve infinity values
109    pub preserve_infinity: bool,
110}
111
112impl Default for ConversionOptions {
113    fn default() -> Self {
114        Self {
115            allow_precision_loss: true,
116            max_relative_error: 1e-10,
117            clamp_to_range: false,
118            round_to_nearest: true,
119            preserve_nan: true,
120            preserve_infinity: true,
121        }
122    }
123}
124
125impl ConversionOptions {
126    /// Create strict options (no precision loss allowed)
127    #[must_use]
128    pub fn strict() -> Self {
129        Self {
130            allow_precision_loss: false,
131            max_relative_error: 0.0,
132            clamp_to_range: false,
133            round_to_nearest: false,
134            preserve_nan: true,
135            preserve_infinity: true,
136        }
137    }
138
139    /// Create lenient options (allow clamping and precision loss)
140    #[must_use]
141    pub fn lenient() -> Self {
142        Self {
143            allow_precision_loss: true,
144            max_relative_error: 1e-6,
145            clamp_to_range: true,
146            round_to_nearest: true,
147            preserve_nan: true,
148            preserve_infinity: true,
149        }
150    }
151}
152
153/// Trait for lossless conversions (guaranteed no data loss)
154pub trait LosslessConvert<T> {
155    /// Convert to target type without any data loss
156    fn convert_lossless(&self) -> ConversionResult<T>;
157}
158
159/// Trait for lossy conversions (may lose precision)
160pub trait LossyConvert<T> {
161    /// Convert to target type with potential precision loss
162    fn convert_lossy(&self) -> T;
163
164    /// Convert with options controlling loss behavior
165    fn convert_with_options(&self, options: &ConversionOptions) -> ConversionResult<T>;
166}
167
168/// Trait for cross-module data conversion
169pub trait CrossModuleConvert {
170    /// The source type
171    type Source;
172    /// The target type
173    type Target;
174
175    /// Convert from source to target
176    fn convert(source: &Self::Source) -> ConversionResult<Self::Target>;
177
178    /// Convert with options
179    fn convert_with_options(
180        source: &Self::Source,
181        options: &ConversionOptions,
182    ) -> ConversionResult<Self::Target>;
183
184    /// Check if conversion is possible without actually converting
185    fn can_convert(source: &Self::Source) -> bool;
186}
187
188/// Trait for array type conversions
189pub trait ArrayConvert<T> {
190    /// Output array type
191    type Output;
192
193    /// Convert array elements to target type
194    fn convert_array(&self) -> ConversionResult<Self::Output>;
195
196    /// Convert with specified options
197    fn convert_array_with_options(
198        &self,
199        options: &ConversionOptions,
200    ) -> ConversionResult<Self::Output>;
201}
202
203// Implement LosslessConvert for common numeric types
204impl LosslessConvert<f64> for f32 {
205    fn convert_lossless(&self) -> ConversionResult<f64> {
206        Ok(*self as f64)
207    }
208}
209
210impl LosslessConvert<i64> for i32 {
211    fn convert_lossless(&self) -> ConversionResult<i64> {
212        Ok(*self as i64)
213    }
214}
215
216impl LosslessConvert<i64> for i16 {
217    fn convert_lossless(&self) -> ConversionResult<i64> {
218        Ok(*self as i64)
219    }
220}
221
222impl LosslessConvert<i64> for i8 {
223    fn convert_lossless(&self) -> ConversionResult<i64> {
224        Ok(*self as i64)
225    }
226}
227
228impl LosslessConvert<u64> for u32 {
229    fn convert_lossless(&self) -> ConversionResult<u64> {
230        Ok(*self as u64)
231    }
232}
233
234impl LosslessConvert<u64> for u16 {
235    fn convert_lossless(&self) -> ConversionResult<u64> {
236        Ok(*self as u64)
237    }
238}
239
240impl LosslessConvert<u64> for u8 {
241    fn convert_lossless(&self) -> ConversionResult<u64> {
242        Ok(*self as u64)
243    }
244}
245
246impl<T: Float> LosslessConvert<Complex<T>> for T {
247    fn convert_lossless(&self) -> ConversionResult<Complex<T>> {
248        Ok(Complex::new(*self, T::zero()))
249    }
250}
251
252// Implement LossyConvert for numeric types
253impl LossyConvert<f32> for f64 {
254    fn convert_lossy(&self) -> f32 {
255        *self as f32
256    }
257
258    fn convert_with_options(&self, options: &ConversionOptions) -> ConversionResult<f32> {
259        let converted = *self as f32;
260
261        if !options.allow_precision_loss {
262            // Check if we lost significant precision
263            let back = converted as f64;
264            let relative_error = if self.abs() > f64::EPSILON {
265                ((back - *self) / *self).abs()
266            } else {
267                (back - *self).abs()
268            };
269
270            if relative_error > options.max_relative_error {
271                return Err(ConversionError::PrecisionLoss {
272                    original: format!("{self}"),
273                    converted: format!("{converted}"),
274                    loss: relative_error,
275                });
276            }
277        }
278
279        if self.is_infinite() && !options.preserve_infinity {
280            return Err(ConversionError::OutOfRange {
281                value: format!("{self}"),
282                min: format!("{}", f32::MIN),
283                max: format!("{}", f32::MAX),
284            });
285        }
286
287        if self.is_nan() && !options.preserve_nan {
288            return Err(ConversionError::InvalidOperation(
289                "Cannot convert NaN".to_string(),
290            ));
291        }
292
293        Ok(converted)
294    }
295}
296
297impl LossyConvert<i32> for f64 {
298    fn convert_lossy(&self) -> i32 {
299        *self as i32
300    }
301
302    fn convert_with_options(&self, options: &ConversionOptions) -> ConversionResult<i32> {
303        if self.is_nan() {
304            return Err(ConversionError::InvalidOperation(
305                "Cannot convert NaN to integer".to_string(),
306            ));
307        }
308
309        if self.is_infinite() {
310            if options.clamp_to_range {
311                return Ok(if *self > 0.0 { i32::MAX } else { i32::MIN });
312            }
313            return Err(ConversionError::OutOfRange {
314                value: format!("{self}"),
315                min: format!("{}", i32::MIN),
316                max: format!("{}", i32::MAX),
317            });
318        }
319
320        let value = if options.round_to_nearest {
321            self.round()
322        } else {
323            self.trunc()
324        };
325
326        if value < i32::MIN as f64 || value > i32::MAX as f64 {
327            if options.clamp_to_range {
328                return Ok(if value < 0.0 { i32::MIN } else { i32::MAX });
329            }
330            return Err(ConversionError::OutOfRange {
331                value: format!("{self}"),
332                min: format!("{}", i32::MIN),
333                max: format!("{}", i32::MAX),
334            });
335        }
336
337        if !options.allow_precision_loss && (value - *self).abs() > f64::EPSILON {
338            return Err(ConversionError::PrecisionLoss {
339                original: format!("{self}"),
340                converted: format!("{}", value as i32),
341                loss: (value - *self).abs(),
342            });
343        }
344
345        Ok(value as i32)
346    }
347}
348
349impl LossyConvert<i64> for f64 {
350    fn convert_lossy(&self) -> i64 {
351        *self as i64
352    }
353
354    fn convert_with_options(&self, options: &ConversionOptions) -> ConversionResult<i64> {
355        if self.is_nan() {
356            return Err(ConversionError::InvalidOperation(
357                "Cannot convert NaN to integer".to_string(),
358            ));
359        }
360
361        if self.is_infinite() {
362            if options.clamp_to_range {
363                return Ok(if *self > 0.0 { i64::MAX } else { i64::MIN });
364            }
365            return Err(ConversionError::OutOfRange {
366                value: format!("{self}"),
367                min: format!("{}", i64::MIN),
368                max: format!("{}", i64::MAX),
369            });
370        }
371
372        let value = if options.round_to_nearest {
373            self.round()
374        } else {
375            self.trunc()
376        };
377
378        if value < i64::MIN as f64 || value > i64::MAX as f64 {
379            if options.clamp_to_range {
380                return Ok(if value < 0.0 { i64::MIN } else { i64::MAX });
381            }
382            return Err(ConversionError::OutOfRange {
383                value: format!("{self}"),
384                min: format!("{}", i64::MIN),
385                max: format!("{}", i64::MAX),
386            });
387        }
388
389        if !options.allow_precision_loss && (value - *self).abs() > f64::EPSILON {
390            return Err(ConversionError::PrecisionLoss {
391                original: format!("{self}"),
392                converted: format!("{}", value as i64),
393                loss: (value - *self).abs(),
394            });
395        }
396
397        Ok(value as i64)
398    }
399}
400
401/// Type adapter for converting between incompatible types
402#[derive(Debug, Clone)]
403pub struct TypeAdapter<S, T> {
404    _source: PhantomData<S>,
405    _target: PhantomData<T>,
406}
407
408impl<S, T> TypeAdapter<S, T> {
409    /// Create a new type adapter
410    #[must_use]
411    pub const fn new() -> Self {
412        Self {
413            _source: PhantomData,
414            _target: PhantomData,
415        }
416    }
417}
418
419impl<S, T> Default for TypeAdapter<S, T> {
420    fn default() -> Self {
421        Self::new()
422    }
423}
424
425impl<S, T> TypeAdapter<S, T>
426where
427    S: NumCast + Copy,
428    T: NumCast + Copy,
429{
430    /// Adapt a value from source to target type
431    pub fn adapt(&self, value: S) -> ConversionResult<T> {
432        NumCast::from(value).ok_or_else(|| {
433            ConversionError::Failed(format!(
434                "Cannot convert {} to {}",
435                std::any::type_name::<S>(),
436                std::any::type_name::<T>()
437            ))
438        })
439    }
440
441    /// Adapt a slice of values
442    pub fn adapt_slice(&self, values: &[S]) -> ConversionResult<Vec<T>> {
443        values.iter().map(|&v| self.adapt(v)).collect()
444    }
445}
446
447/// Data flow converter for pipeline operations
448pub struct DataFlowConverter {
449    /// Conversion options
450    options: ConversionOptions,
451    /// Type registry for custom conversions
452    custom_conversions: Vec<(
453        TypeId,
454        TypeId,
455        Box<dyn Fn(&[u8]) -> ConversionResult<Vec<u8>> + Send + Sync>,
456    )>,
457}
458
459impl std::fmt::Debug for DataFlowConverter {
460    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461        f.debug_struct("DataFlowConverter")
462            .field("options", &self.options)
463            .field("custom_conversions_count", &self.custom_conversions.len())
464            .finish()
465    }
466}
467
468impl Default for DataFlowConverter {
469    fn default() -> Self {
470        Self::new()
471    }
472}
473
474impl DataFlowConverter {
475    /// Create a new data flow converter
476    #[must_use]
477    pub fn new() -> Self {
478        Self {
479            options: ConversionOptions::default(),
480            custom_conversions: Vec::new(),
481        }
482    }
483
484    /// Create with specific options
485    #[must_use]
486    pub fn with_options(options: ConversionOptions) -> Self {
487        Self {
488            options,
489            custom_conversions: Vec::new(),
490        }
491    }
492
493    /// Get current options
494    #[must_use]
495    pub const fn options(&self) -> &ConversionOptions {
496        &self.options
497    }
498
499    /// Set options
500    pub fn set_options(&mut self, options: ConversionOptions) {
501        self.options = options;
502    }
503
504    /// Convert a slice of values
505    pub fn convert_slice<S, T>(&self, source: &[S]) -> ConversionResult<Vec<T>>
506    where
507        S: NumCast + Copy,
508        T: NumCast + Copy,
509    {
510        let adapter: TypeAdapter<S, T> = TypeAdapter::new();
511        adapter.adapt_slice(source)
512    }
513
514    /// Convert f64 to f32 with options
515    pub fn f64_to_f32(&self, values: &[f64]) -> ConversionResult<Vec<f32>> {
516        values
517            .iter()
518            .map(|v| v.convert_with_options(&self.options))
519            .collect()
520    }
521
522    /// Convert f64 to i32 with options
523    pub fn f64_to_i32(&self, values: &[f64]) -> ConversionResult<Vec<i32>> {
524        values
525            .iter()
526            .map(|v| v.convert_with_options(&self.options))
527            .collect()
528    }
529
530    /// Convert f64 to i64 with options
531    pub fn f64_to_i64(&self, values: &[f64]) -> ConversionResult<Vec<i64>> {
532        values
533            .iter()
534            .map(|v| v.convert_with_options(&self.options))
535            .collect()
536    }
537
538    /// Convert between any numeric types (convenience method)
539    pub fn convert<S, T>(&self, source: &[S]) -> ConversionResult<Vec<T>>
540    where
541        S: NumCast + Copy,
542        T: NumCast + Copy,
543    {
544        self.convert_slice(source)
545    }
546}
547
548/// Implement ArrayConvert for Vec
549impl<S, T> ArrayConvert<T> for Vec<S>
550where
551    S: NumCast + Copy,
552    T: NumCast + Copy,
553{
554    type Output = Vec<T>;
555
556    fn convert_array(&self) -> ConversionResult<Self::Output> {
557        let adapter: TypeAdapter<S, T> = TypeAdapter::new();
558        adapter.adapt_slice(self)
559    }
560
561    fn convert_array_with_options(
562        &self,
563        _options: &ConversionOptions,
564    ) -> ConversionResult<Self::Output> {
565        // For generic numeric types, we use the basic conversion
566        // More specific implementations can provide better precision handling
567        self.convert_array()
568    }
569}
570
571/// Implement ArrayConvert for slices
572impl<S, T> ArrayConvert<T> for [S]
573where
574    S: NumCast + Copy,
575    T: NumCast + Copy,
576{
577    type Output = Vec<T>;
578
579    fn convert_array(&self) -> ConversionResult<Self::Output> {
580        let adapter: TypeAdapter<S, T> = TypeAdapter::new();
581        adapter.adapt_slice(self)
582    }
583
584    fn convert_array_with_options(
585        &self,
586        _options: &ConversionOptions,
587    ) -> ConversionResult<Self::Output> {
588        self.convert_array()
589    }
590}
591
592/// Complex number conversion utilities
593pub mod complex {
594    use super::*;
595
596    /// Convert complex from one precision to another
597    pub fn convert_precision<S, T>(value: Complex<S>) -> ConversionResult<Complex<T>>
598    where
599        S: Float + NumCast,
600        T: Float + NumCast,
601    {
602        let real = NumCast::from(value.re)
603            .ok_or_else(|| ConversionError::Failed("Cannot convert real part".to_string()))?;
604        let imag = NumCast::from(value.im)
605            .ok_or_else(|| ConversionError::Failed("Cannot convert imaginary part".to_string()))?;
606        Ok(Complex::new(real, imag))
607    }
608
609    /// Convert slice of complex numbers
610    pub fn convert_slice<S, T>(values: &[Complex<S>]) -> ConversionResult<Vec<Complex<T>>>
611    where
612        S: Float + NumCast,
613        T: Float + NumCast,
614    {
615        values.iter().map(|&v| convert_precision(v)).collect()
616    }
617
618    /// Convert real slice to complex slice
619    pub fn real_to_complex<T: Float + Zero>(values: &[T]) -> Vec<Complex<T>> {
620        values.iter().map(|&v| Complex::new(v, T::zero())).collect()
621    }
622
623    /// Extract real parts from complex slice
624    pub fn extract_real<T: Float>(values: &[Complex<T>]) -> Vec<T> {
625        values.iter().map(|v| v.re).collect()
626    }
627
628    /// Extract imaginary parts from complex slice
629    pub fn extract_imag<T: Float>(values: &[Complex<T>]) -> Vec<T> {
630        values.iter().map(|v| v.im).collect()
631    }
632
633    /// Convert complex slice to magnitude slice
634    pub fn to_magnitude<T: Float>(values: &[Complex<T>]) -> Vec<T> {
635        values
636            .iter()
637            .map(|v| (v.re * v.re + v.im * v.im).sqrt())
638            .collect()
639    }
640
641    /// Convert complex slice to phase slice
642    pub fn to_phase<T: Float>(values: &[Complex<T>]) -> Vec<T> {
643        values.iter().map(|v| v.im.atan2(v.re)).collect()
644    }
645}
646
647/// Batch conversion utilities
648pub mod batch {
649    use super::*;
650
651    /// Batch conversion result
652    #[derive(Debug)]
653    pub struct BatchResult<T> {
654        /// Successfully converted values
655        pub values: Vec<T>,
656        /// Indices of failed conversions
657        pub failed_indices: Vec<usize>,
658        /// Error messages for failures
659        pub errors: Vec<ConversionError>,
660    }
661
662    impl<T> BatchResult<T> {
663        /// Check if all conversions succeeded
664        #[must_use]
665        pub fn all_succeeded(&self) -> bool {
666            self.failed_indices.is_empty()
667        }
668
669        /// Get the number of successful conversions
670        #[must_use]
671        pub fn success_count(&self) -> usize {
672            self.values.len() - self.failed_indices.len()
673        }
674
675        /// Get the number of failed conversions
676        #[must_use]
677        pub fn failure_count(&self) -> usize {
678            self.failed_indices.len()
679        }
680    }
681
682    /// Convert with failure tolerance (returns partial results)
683    pub fn convert_tolerant<S, T>(values: &[S]) -> BatchResult<T>
684    where
685        S: NumCast + Copy,
686        T: NumCast + Copy + Default,
687    {
688        let mut result = BatchResult {
689            values: Vec::with_capacity(values.len()),
690            failed_indices: Vec::new(),
691            errors: Vec::new(),
692        };
693
694        for (i, &value) in values.iter().enumerate() {
695            match NumCast::from(value) {
696                Some(converted) => result.values.push(converted),
697                None => {
698                    result.failed_indices.push(i);
699                    result.errors.push(ConversionError::Failed(format!(
700                        "Cannot convert value at index {i}"
701                    )));
702                    result.values.push(T::default());
703                }
704            }
705        }
706
707        result
708    }
709
710    /// Convert with default value for failures
711    pub fn convert_with_default<S, T>(values: &[S], default: T) -> Vec<T>
712    where
713        S: NumCast + Copy,
714        T: NumCast + Copy,
715    {
716        values
717            .iter()
718            .map(|&v| NumCast::from(v).unwrap_or(default))
719            .collect()
720    }
721
722    /// Convert in parallel chunks
723    #[cfg(feature = "parallel")]
724    pub fn convert_parallel<S, T>(values: &[S], chunk_size: usize) -> ConversionResult<Vec<T>>
725    where
726        S: NumCast + Copy + Send + Sync,
727        T: NumCast + Copy + Send + Sync,
728    {
729        use rayon::prelude::*;
730
731        let results: Vec<ConversionResult<T>> = values
732            .par_chunks(chunk_size)
733            .flat_map(|chunk| {
734                chunk
735                    .iter()
736                    .map(|&v| {
737                        NumCast::from(v)
738                            .ok_or_else(|| ConversionError::Failed("Conversion failed".to_string()))
739                    })
740                    .collect::<Vec<_>>()
741            })
742            .collect();
743
744        results.into_iter().collect()
745    }
746}
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751
752    #[test]
753    fn test_lossless_convert() {
754        let val: f64 = 2.5f32.convert_lossless().expect("Should convert");
755        assert!((val - 2.5f64).abs() < 0.001);
756
757        let val: i64 = 42i32.convert_lossless().expect("Should convert");
758        assert_eq!(val, 42);
759    }
760
761    #[test]
762    fn test_lossy_convert() {
763        let val: f32 = std::f64::consts::PI.convert_lossy();
764        assert!((val - std::f32::consts::PI).abs() < 1e-6);
765
766        let val: i32 = 3.7f64.convert_lossy();
767        assert_eq!(val, 3);
768    }
769
770    #[test]
771    fn test_lossy_convert_with_options() {
772        let options = ConversionOptions::strict();
773        let result: ConversionResult<i32> = 3.5f64.convert_with_options(&options);
774        assert!(result.is_err()); // Precision loss not allowed
775
776        let options = ConversionOptions::lenient();
777        let result: ConversionResult<i32> = 3.5f64.convert_with_options(&options);
778        assert!(result.is_ok());
779        assert_eq!(result.expect("Should convert"), 4); // Rounded
780    }
781
782    #[test]
783    fn test_type_adapter() {
784        let adapter: TypeAdapter<f64, i32> = TypeAdapter::new();
785        let result = adapter.adapt(42.5);
786        assert!(result.is_ok());
787        assert_eq!(result.expect("Should adapt"), 42);
788
789        let values = [1.0, 2.0, 3.0, 4.0];
790        let adapted: Vec<i32> = adapter.adapt_slice(&values).expect("Should adapt slice");
791        assert_eq!(adapted, vec![1, 2, 3, 4]);
792    }
793
794    #[test]
795    fn test_data_flow_converter() {
796        let converter = DataFlowConverter::new();
797        let values = [1.5, 2.5, 3.5, 4.5];
798
799        let result: Vec<i32> = converter.convert(&values).expect("Should convert");
800        assert_eq!(result, vec![1, 2, 3, 4]);
801    }
802
803    #[test]
804    fn test_array_convert() {
805        let vec: Vec<f64> = vec![1.0, 2.0, 3.0];
806        let converted: Vec<i32> = vec.convert_array().expect("Should convert");
807        assert_eq!(converted, vec![1, 2, 3]);
808    }
809
810    #[test]
811    fn test_complex_conversion() {
812        let c64 = Complex::new(1.0f64, 2.0f64);
813        let c32: Complex<f32> = complex::convert_precision(c64).expect("Should convert");
814        assert!((c32.re - 1.0f32).abs() < 1e-6);
815        assert!((c32.im - 2.0f32).abs() < 1e-6);
816    }
817
818    #[test]
819    fn test_real_to_complex() {
820        let values = [1.0, 2.0, 3.0];
821        let complex_vals = complex::real_to_complex(&values);
822        assert_eq!(complex_vals.len(), 3);
823        assert_eq!(complex_vals[0].im, 0.0);
824    }
825
826    #[test]
827    fn test_batch_convert_tolerant() {
828        let values: Vec<f64> = vec![1.0, 2.0, f64::NAN, 4.0];
829        let result: batch::BatchResult<i32> = batch::convert_tolerant(&values);
830        // NaN conversion behavior depends on platform, but we should handle it gracefully
831        assert!(result.success_count() + result.failure_count() == 4);
832    }
833
834    #[test]
835    fn test_conversion_error_display() {
836        let err = ConversionError::TypeMismatch {
837            expected: "f64".to_string(),
838            actual: "String".to_string(),
839        };
840        assert!(err.to_string().contains("f64"));
841
842        let err = ConversionError::OutOfRange {
843            value: "1e100".to_string(),
844            min: "-1e38".to_string(),
845            max: "1e38".to_string(),
846        };
847        assert!(err.to_string().contains("out of range"));
848    }
849
850    #[test]
851    fn test_conversion_options() {
852        let strict = ConversionOptions::strict();
853        assert!(!strict.allow_precision_loss);
854        assert!(!strict.clamp_to_range);
855
856        let lenient = ConversionOptions::lenient();
857        assert!(lenient.allow_precision_loss);
858        assert!(lenient.clamp_to_range);
859    }
860
861    #[test]
862    fn test_out_of_range_clamping() {
863        let options = ConversionOptions {
864            clamp_to_range: true,
865            ..Default::default()
866        };
867
868        let result: ConversionResult<i32> = 1e20f64.convert_with_options(&options);
869        assert!(result.is_ok());
870        assert_eq!(result.expect("Should clamp"), i32::MAX);
871
872        let result: ConversionResult<i32> = (-1e20f64).convert_with_options(&options);
873        assert!(result.is_ok());
874        assert_eq!(result.expect("Should clamp"), i32::MIN);
875    }
876}