uninum 0.1.1

A robust, ergonomic unified number type for Rust with automatic overflow handling, type promotion, and cross-type consistency.
Documentation
//! Tests for Display trait formatting
//!
//! This module tests the Display trait implementation for Number variants
//! and various formatting edge cases.

use uninum::{Number, num};

/// Tests Display formatting
#[test]
fn test_display_formatting() {
    // Integers display as-is
    assert_eq!(Number::from(42u64).to_string(), "42");
    assert_eq!(Number::from(-42i64).to_string(), "-42");
    assert_eq!(Number::from(1234567890u64).to_string(), "1234567890");

    // Floats display with their precision
    assert_eq!(num!(3.15159).to_string(), "3.15159");

    // Special float values
    assert_eq!(num!(f64::INFINITY).to_string(), "inf");
    assert_eq!(num!(f64::NEG_INFINITY).to_string(), "-inf");
    assert_eq!(num!(f64::NAN).to_string(), "NaN");

    #[cfg(feature = "decimal")]
    {
        use rust_decimal::Decimal;
        let dec = Number::from(Decimal::new(31415, 4));
        assert_eq!(dec.to_string(), "3.1415");
    }
}

/// Tests Display formatting for all edge cases
#[test]
fn test_display_edge_cases() {
    // Test that very large numbers display correctly
    assert_eq!(
        Number::from(18446744073709551615u64).to_string(),
        "18446744073709551615"
    );
    assert_eq!(
        Number::from(-9223372036854775808i64).to_string(),
        "-9223372036854775808"
    );

    // Test float precision in display
    assert!(num!(1.0 / 3.0).to_string().contains("0.3333"));

    // Test very small floats (use decimal notation, not scientific)
    assert_eq!(num!(1e-10).to_string(), "0.0000000001");
    assert!(num!(1e-300).to_string().starts_with("0."));

    // Test very large floats (use decimal notation, not scientific)
    assert!(num!(1e100).to_string().starts_with("1000"));
    assert!(num!(1e308).to_string().starts_with("1000"));
}

/// Tests roundtrip conversions (parse → display → parse) with comprehensive
/// cases
#[test]
fn test_roundtrip_conversions_comprehensive() {
    let test_cases = vec![
        // Basic integers
        "0",
        "1",
        "-1",
        "42",
        "-42",
        // Boundary values that fit in different types
        "127",
        "-128",
        "255",
        "256",
        "-129",
        "32767",
        "-32768",
        "65535",
        "65536",
        "-32769",
        "2147483647",
        "-2147483648",
        "4294967295",
        "4294967296",
        "9223372036854775807",
        "-9223372036854775808",
        // Floats
        "0.0",
        "1.0",
        "-1.0",
        "3.16",
        "-3.16",
        "2.72",
        "1.23456789",
        "9876543210.123456789",
        // Scientific notation
        "1e10",
        "1.5e-10",
        "3.16e2",
        "-2.5e-3",
        // Special values
        "inf",
        "-inf",
        "NaN",
    ];

    for input in test_cases {
        let num = Number::try_from(input).unwrap();
        let output = num.to_string();
        let reparsed = Number::try_from(output.as_str()).unwrap();

        // Should get the same value (type might differ due to optimal parsing)
        // For special values like NaN, we use pattern matching
        match (&num, &reparsed) {
            (n1, n2) if n1.is_nan() && n2.is_nan() => {
                // Both are NaN, this is correct
            }
            _ => {
                // For other values, use direct equality
                assert_eq!(
                    num, reparsed,
                    "Roundtrip failed for input '{input}': {num} -> {output} -> {reparsed}"
                );
            }
        }
    }

    // Test explicit type suffixes maintain type
    // Test that typed suffixes are no longer supported
    let typed_cases = vec!["42u64", "42u64", "-42i64", "-42i64", "3.16f64"];

    for input in typed_cases {
        assert!(
            Number::try_from(input).is_err(),
            "Typed suffix should be rejected: '{input}'"
        );
    }
}

#[test]
#[cfg(feature = "decimal")]
fn test_decimal_display_formatting() {
    use rust_decimal::Decimal;

    // Test various decimal representations
    let cases = vec![
        (Decimal::new(0, 0), "0"),
        (Decimal::new(42, 0), "42"),
        (Decimal::new(315159, 5), "3.15159"),
        (Decimal::new(-27123, 4), "-2.7123"),
        (Decimal::new(1000000, 3), "1000.000"),
        (Decimal::new(1, 10), "0.0000000001"),
    ];

    for (decimal, expected) in cases {
        let number = Number::from(decimal);
        assert_eq!(number.to_string(), expected);
    }
}

#[test]
fn test_zero_display() {
    // Test various zero representations
    assert_eq!(Number::from(0u64).to_string(), "0");
    assert_eq!(Number::from(0i64).to_string(), "0");
    assert_eq!(Number::from(0u64).to_string(), "0");
    assert_eq!(Number::from(0i64).to_string(), "0");
    #[cfg(feature = "decimal")]
    assert_eq!(num!(0.0).to_string(), "0.0");
    #[cfg(not(feature = "decimal"))]
    assert_eq!(num!(0.0).to_string(), "0");
    // num!(-0.0) now preserves sign as F64 to maintain negative zero
    assert_eq!(num!(-0.0).to_string(), "-0");

    #[cfg(feature = "decimal")]
    {
        use rust_decimal::Decimal;

        assert_eq!(Number::from(Decimal::ZERO).to_string(), "0");
        // Decimal does support negative zero like floats
        assert_eq!(Number::from(-Decimal::ZERO).to_string(), "-0");
    }
}

#[test]
fn test_float64_direct_instantiation() {
    // Test direct f64 values wrapped in Number
    let f = Number::from(42.0f64);
    assert_eq!(f.try_get_f64().unwrap(), 42.0);

    let f_nan = Number::from(f64::NAN);
    assert!(f_nan.try_get_f64().unwrap().is_nan());

    let f_inf = Number::from(f64::INFINITY);
    assert!(f_inf.try_get_f64().unwrap().is_infinite());
    assert!(f_inf.try_get_f64().unwrap() > 0.0);

    let f_neg_inf = Number::from(f64::NEG_INFINITY);
    assert!(f_neg_inf.try_get_f64().unwrap().is_infinite());
    assert!(f_neg_inf.try_get_f64().unwrap() < 0.0);
}

#[test]
fn test_float64_deref_trait() {
    // Test Number f64 methods
    let f = Number::from(3.15159f64);

    // Test f64 methods through Number
    let f_val = f.try_get_f64().unwrap();
    assert_eq!(f_val.floor(), 3.0);
    assert_eq!(f_val.ceil(), 4.0);
    assert_eq!(f_val.round(), 3.0);
    assert!(f_val.is_finite());
    assert!(!f_val.is_infinite());
    assert!(!f_val.is_nan());

    // Test special values
    let f_nan = Number::from(f64::NAN);
    assert!(f_nan.try_get_f64().unwrap().is_nan());

    let f_inf = Number::from(f64::INFINITY);
    assert!(f_inf.try_get_f64().unwrap().is_infinite());
    assert!(!f_inf.try_get_f64().unwrap().is_finite());

    // Test mathematical operations
    let f1_val = Number::from(2.0f64).try_get_f64().unwrap();
    let f2_val = Number::from(3.0f64).try_get_f64().unwrap();
    assert_eq!(f1_val.powf(f2_val), 8.0);
    assert_eq!(f1_val.sqrt(), std::f64::consts::SQRT_2);
}

#[test]
fn test_float64_display_trait() {
    // Test Number Display trait implementation
    let f = num!(3.15159);
    assert_eq!(format!("{f}"), "3.15159");

    let f_zero = num!(0.0);
    #[cfg(feature = "decimal")]
    assert_eq!(format!("{f_zero}"), "0.0");
    #[cfg(not(feature = "decimal"))]
    assert_eq!(format!("{f_zero}"), "0");

    let f_neg_zero = num!(-0.0);
    // num!(-0.0) now preserves sign as F64 to maintain negative zero
    assert_eq!(format!("{f_neg_zero}"), "-0");

    let f_inf = num!(f64::INFINITY);
    assert_eq!(format!("{f_inf}"), "inf");

    let f_neg_inf = num!(f64::NEG_INFINITY);
    assert_eq!(format!("{f_neg_inf}"), "-inf");

    let f_nan = num!(f64::NAN);
    assert_eq!(format!("{f_nan}"), "NaN");

    // Test various float formats
    let f_small = num!(0.000001);
    assert_eq!(format!("{f_small}"), "0.000001");

    let f_large = num!(1000000.0);
    #[cfg(feature = "decimal")]
    assert_eq!(format!("{f_large}"), "1000000.0");
    #[cfg(not(feature = "decimal"))]
    assert_eq!(format!("{f_large}"), "1000000");

    let f_scientific = num!(1e20);
    assert_eq!(format!("{f_scientific}"), "100000000000000000000");
}

#[test]
fn test_float64_default_trait() {
    // Test Number default for f64
    let f_default = Number::from(0.0f64);
    assert_eq!(f_default.try_get_f64().unwrap(), 0.0);
    assert!(!f_default.try_get_f64().unwrap().is_nan());
    assert!(f_default.try_get_f64().unwrap().is_finite());

    // Default should equal zero
    assert_eq!(f_default, Number::from(0.0f64));
}

#[test]
fn test_float64_from_into_traits() {
    // Test From<f64> for Number
    let f_from = Number::from(42.5);
    assert_eq!(f_from.try_get_f64().unwrap(), 42.5);

    let f_from_nan = Number::from(f64::NAN);
    assert!(f_from_nan.try_get_f64().unwrap().is_nan());

    let f_from_inf = Number::from(f64::INFINITY);
    assert!(f_from_inf.try_get_f64().unwrap().is_infinite());

    // Test extracting f64 from Number
    let f = Number::from(3.15159f64);
    let f64_from = f.try_get_f64().unwrap();
    assert_eq!(f64_from, 3.15159);

    let f_nan = Number::from(f64::NAN);
    let f64_from_nan = f_nan.try_get_f64().unwrap();
    assert!(f64_from_nan.is_nan());

    let f_inf = Number::from(f64::INFINITY);
    let f64_from_inf = f_inf.try_get_f64().unwrap();
    assert!(f64_from_inf.is_infinite());
}

#[test]
fn test_float64_special_values_edge_cases() {
    // Test subnormal numbers
    let subnormal = Number::from(f64::MIN_POSITIVE / 2.0);
    let subnormal_val = subnormal.try_get_f64().unwrap();
    assert!(subnormal_val > 0.0);
    assert!(subnormal_val < f64::MIN_POSITIVE);

    // Test largest finite value
    let max_finite = Number::from(f64::MAX);
    let max_val = max_finite.try_get_f64().unwrap();
    assert!(max_val.is_finite());
    assert!(!max_val.is_infinite());

    // Test smallest positive normal value
    let min_positive = Number::from(f64::MIN_POSITIVE);
    let min_val = min_positive.try_get_f64().unwrap();
    assert!(min_val > 0.0);
    assert!(min_val.is_normal());

    // Test epsilon
    let epsilon = Number::from(f64::EPSILON);
    let eps_val = epsilon.try_get_f64().unwrap();
    assert!(eps_val > 0.0);
    assert!(eps_val < 1.0);

    // Test different NaN representations
    let nan1 = Number::from(f64::NAN);
    let nan2 = Number::from(f64::INFINITY - f64::INFINITY);

    assert!(nan1.try_get_f64().unwrap().is_nan());
    assert!(nan2.try_get_f64().unwrap().is_nan());

    // Test infinity operations
    let pos_inf = Number::from(f64::INFINITY);
    let neg_inf = Number::from(f64::NEG_INFINITY);

    assert!(pos_inf.try_get_f64().unwrap().is_infinite());
    assert!(neg_inf.try_get_f64().unwrap().is_infinite());
    assert!(pos_inf.try_get_f64().unwrap() > 0.0);
    assert!(neg_inf.try_get_f64().unwrap() < 0.0);

    // Test that our custom equality works correctly
    assert_eq!(nan1, nan2);
    assert_eq!(num!(0.0), num!(-0.0));
}

#[test]
fn test_float64_clone_copy() {
    // Test that Number implements Clone
    let f = num!(42.0);
    let f_cloned = f.clone();
    let f_copied = f.clone();

    assert_eq!(f_cloned, num!(42.0));
    assert_eq!(f_copied, num!(42.0));

    // Original should still be usable
    assert_eq!(f, num!(42.0));
}

#[test]
fn test_float64_debug() {
    // Test Debug trait implementation
    let f = num!(3.15159);
    let debug_str = format!("{f:?}");
    assert!(debug_str.contains("3.15159"));

    let f_nan = num!(f64::NAN);
    let debug_nan = format!("{f_nan:?}");
    assert!(debug_nan.contains("NaN"));

    let f_inf = num!(f64::INFINITY);
    let debug_inf = format!("{f_inf:?}");
    assert!(debug_inf.contains("inf"));
}