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 addition operations

use uninum::Number;

/// Tests basic addition operations with proper type handling
#[test]
fn test_basic_addition() {
    // Test integer additions using From trait
    assert_eq!(
        Number::from(10u64) + Number::from(20u64),
        Number::from(30u64)
    );
    assert_eq!(
        Number::from(-10i64) + Number::from(20i64),
        Number::from(10i64)
    );

    // Test float additions
    assert_eq!(Number::from(10.5) + Number::from(2.5), Number::from(13.0));

    // Test mixed type additions - demonstrates automatic type promotion
    assert_eq!(Number::from(10u64) + Number::from(2.5), Number::from(12.5));
    assert_eq!(
        Number::from(-5i64) + Number::from(10u64),
        Number::from(5i64)
    );
}

/// Tests automatic type promotion on overflow
#[test]
fn test_overflow_promotion() {
    // Test i64 overflow promotes to higher precision type
    let result = Number::from(i64::MAX) + Number::from(1i64);
    assert!(result.try_get_i64().is_none()); // No longer fits in i64

    // Test u64 overflow promotes to higher precision type
    let result = Number::from(u64::MAX) + Number::from(1u64);
    assert!(result.try_get_u64().is_none()); // No longer fits in u64

    // Verify the result is still valid and can be used
    assert!(result.is_finite());
    assert!(!result.is_nan());
}

/// Tests addition with primitive types through operator overloading
#[test]
fn test_primitive_operations() {
    let num = Number::from(10u64);

    // Test various primitive type additions
    assert_eq!(&num + 5, Number::from(15u64));
    assert_eq!(5 + &num, Number::from(15u64));
    assert_eq!(&num + 5.5, Number::from(15.5));

    // Test that references work correctly
    let a = Number::from(10u64);
    let b = Number::from(20u64);
    assert_eq!(&a + &b, Number::from(30u64));

    // Verify originals are unchanged (important for reference semantics)
    assert_eq!(a, Number::from(10u64));
    assert_eq!(b, Number::from(20u64));
}

/// Tests special float value handling (NaN, Infinity)
#[test]
fn test_special_float_values() {
    let inf = Number::from(f64::INFINITY);
    let neg_inf = Number::from(f64::NEG_INFINITY);
    let nan = Number::from(f64::NAN);
    let normal = Number::from(42);

    // Infinity + normal = Infinity
    assert!((inf.clone() + normal.clone()).is_pos_inf());
    assert!((neg_inf.clone() + normal.clone()).is_neg_inf());

    // NaN + anything = NaN
    assert!((nan.clone() + normal.clone()).is_nan());
    assert!((nan.clone() + inf.clone()).is_nan());

    // Infinity + -Infinity = NaN (undefined)
    assert!((inf.clone() + neg_inf.clone()).is_nan());
}

/// Tests zero handling including signed zeros
#[test]
fn test_zero_handling() {
    // Integer zeros
    assert_eq!(Number::from(0u64) + Number::from(0u64), Number::from(0u64));
    assert_eq!(Number::from(0i64) + Number::from(0i64), Number::from(0i64));

    // Float zeros - test signed zero preservation
    let pos_zero = Number::from(0.0);
    let neg_zero = Number::from(-0.0);

    // 0.0 + 0.0 = 0.0
    let result = pos_zero.clone() + pos_zero.clone();
    if let Some(f) = result.try_get_f64() {
        assert!(f == 0.0 && f.is_sign_positive());
    }

    // -0.0 + -0.0 = -0.0
    let result = neg_zero.clone() + neg_zero.clone();
    if let Some(f) = result.try_get_f64() {
        assert!(f == 0.0 && f.is_sign_negative());
    }

    // Numbers that cancel to zero
    assert_eq!(
        Number::from(42i64) + Number::from(-42i64),
        Number::from(0i64)
    );
}

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

    // Test decimal precision preservation
    let a = Number::from(Decimal::new(12345, 4)); // 1.2345
    let b = Number::from(Decimal::new(67890, 4)); // 6.7890
    let sum = a + b;

    // Verify result maintains decimal precision
    if let Some(d) = sum.try_get_decimal() {
        assert_eq!(d.to_string(), "8.0235");
    }

    // Test decimal overflow handling
    let max_dec = Number::from(Decimal::MAX);
    let overflow = max_dec.clone() + max_dec;
    assert!(overflow.try_get_decimal().is_none()); // Overflowed to different type
    assert!(overflow.is_finite()); // But still a valid number
}

/// Tests mathematical properties: commutativity and associativity
#[test]
fn test_mathematical_properties() {
    let a = Number::from(10);
    let b = Number::from(20);
    let c = Number::from(30);

    // Commutativity: a + b = b + a
    assert_eq!(a.clone() + b.clone(), b.clone() + a.clone());

    // Associativity: (a + b) + c = a + (b + c)
    assert_eq!(
        (a.clone() + b.clone()) + c.clone(),
        a.clone() + (b.clone() + c.clone())
    );

    // Test with mixed types to ensure properties hold
    let x = Number::from(5u64);
    let y = Number::from(-3i64);
    let z = Number::from(2.5);

    // Properties should hold even with type conversions
    assert_eq!(
        (x.clone() + y.clone()) + z.clone(),
        x.clone() + (y.clone() + z.clone())
    );
}

/// Tests edge cases with extreme values
#[test]
fn test_extreme_values() {
    // Very small positive numbers
    let tiny = Number::from(f64::MIN_POSITIVE);
    let sum = tiny.clone() + tiny;
    if let Some(f) = sum.try_get_f64() {
        assert_eq!(f, f64::MIN_POSITIVE * 2.0);
    }

    // Mix of very large and very small (tests precision loss)
    let large = Number::from(1e308);
    let small = Number::from(1e-308);
    let sum = large + small;
    if let Some(f) = sum.try_get_f64() {
        // Small number lost in precision
        assert_eq!(f, 1e308);
    }

    // Maximum safe integer in f64 (2^53 - 1)
    let max_safe = Number::from(9007199254740991.0);
    let one = Number::from(1.0);
    let sum = max_safe + one;
    if let Some(f) = sum.try_get_f64() {
        assert_eq!(f, 9007199254740992.0);
    }
}

/// Tests addition chains and compound operations
#[test]
fn test_compound_operations() {
    // Simple chain
    let result = Number::from(10) + Number::from(5) + Number::from(3);
    assert_eq!(result, Number::from(18));

    // Chain with potential overflow
    let a = Number::from(u64::MAX / 2);
    let b = Number::from(u64::MAX / 2);
    let c = Number::from(10u64);
    let result = a + b + c;

    // Should have overflowed from u64
    assert!(result.try_get_u64().is_none());
    assert!(result.is_finite());
}