uninum 0.1.1

A robust, ergonomic unified number type for Rust with automatic overflow handling, type promotion, and cross-type consistency.
Documentation
//! Data processing and analytics showcase for `uninum`.
//!
//! Run with `cargo run --example data_processing` to execute the scenarios.

use uninum::{Number, num};

fn calculate_average(values: &[Number]) -> Number {
    if values.is_empty() {
        return num!(0);
    }

    let sum = values.iter().cloned().reduce(|a, b| a + b).unwrap();
    let count = num!(values.len());
    sum / count
}

fn calculate_weighted_average(values: &[(Number, Number)]) -> Number {
    if values.is_empty() {
        return num!(0);
    }

    let weighted_sum = values
        .iter()
        .map(|(value, weight)| value.clone() * weight.clone())
        .reduce(|a, b| a + b)
        .unwrap();

    let total_weight = values
        .iter()
        .map(|(_, weight)| weight.clone())
        .reduce(|a, b| a + b)
        .unwrap();

    weighted_sum / total_weight
}

fn analyze_mixed_type_data() {
    let values = vec![num!(10), num!(20.5), num!(30), num!(15)];

    let avg = calculate_average(&values);
    let expected = num!(18.875);
    assert!(
        avg.approx_eq(&expected, 1e-10, 0.0),
        "Mixed type average calculation error: expected {expected}, got {avg}"
    );

    let weighted_data = vec![
        (num!(85.0), num!(0.3)),
        (num!(92.0), num!(0.4)),
        (num!(78.0), num!(0.3)),
    ];

    let weighted_avg = calculate_weighted_average(&weighted_data);
    let expected_weighted = num!(85.7);
    assert!(
        weighted_avg.approx_eq(&expected_weighted, 1e-10, 0.0),
        "Weighted average calculation error: expected {expected_weighted}, got {weighted_avg}"
    );

    let total_weight = weighted_data
        .iter()
        .map(|(_, weight)| weight.clone())
        .reduce(|a, b| a + b)
        .unwrap();
    assert!(
        total_weight.approx_eq(&num!(1.0), 1e-10, 0.0),
        "Weights should sum to 1.0 for proper weighted average"
    );

    println!("Mixed-type averages: mean={avg}, weighted={weighted_avg}");
}

fn calculate_moving_average(data: &[Number], window_size: usize) -> Vec<Number> {
    if window_size == 0 || window_size > data.len() {
        return vec![];
    }

    let mut result = Vec::new();
    for window in data.windows(window_size) {
        let sum = window.iter().cloned().reduce(|a, b| a + b).unwrap();
        let count = Number::from(window_size as u64);
        result.push(sum / count);
    }
    result
}

fn calculate_growth_rate(current: Number, previous: Number) -> Number {
    if previous.is_zero() {
        return num!(0.0);
    }
    (&current - &previous) / &previous * num!(100.0)
}

fn analyze_time_series() {
    let prices = vec![
        num!(100.0),
        num!(105.0),
        num!(102.0),
        num!(108.0),
        num!(110.0),
        num!(107.0),
    ];

    let moving_avg = calculate_moving_average(&prices, 3);
    assert_eq!(moving_avg.len(), 4);

    if let Some(f) = moving_avg[0].try_get_f64() {
        assert!((f - 102.333).abs() < 0.01);
    } else {
        #[cfg(feature = "decimal")]
        {
            if moving_avg[0].try_get_decimal().is_some() {
                assert!(moving_avg[0].approx_eq(&num!(102.333), 0.01, 0.0));
            } else {
                panic!("Expected F64 or Decimal, got {:?}", moving_avg[0]);
            }
        }
        #[cfg(not(feature = "decimal"))]
        {
            panic!("Expected F64, got {:?}", moving_avg[0]);
        }
    }

    let growth_rates: Vec<Number> = prices
        .windows(2)
        .map(|window| calculate_growth_rate(window[1].clone(), window[0].clone()))
        .collect();
    assert_eq!(growth_rates[0], num!(5.0));

    println!(
        "Time-series analytics: first moving average = {}, first growth = {}%",
        moving_avg[0], growth_rates[0]
    );
}

struct SalesRecord {
    product_id: u32,
    quantity: Number,
    unit_price: Number,
    discount_rate: Number,
}

impl SalesRecord {
    fn total_revenue(&self) -> Number {
        let gross = &self.quantity * &self.unit_price;
        let discount = gross.clone() * &self.discount_rate;
        gross - discount
    }
}

fn analyze_sales() {
    let records = vec![
        SalesRecord {
            product_id: 1,
            quantity: num!(10),
            unit_price: num!(19.99),
            discount_rate: num!(0.10),
        },
        SalesRecord {
            product_id: 2,
            quantity: num!(5),
            unit_price: num!(49.99),
            discount_rate: num!(0.0),
        },
        SalesRecord {
            product_id: 3,
            quantity: num!(3),
            unit_price: num!(99.95),
            discount_rate: num!(0.15),
        },
    ];

    let mut total_revenue = num!(0);
    for record in &records {
        let revenue = record.total_revenue();
        println!("Product {} revenue: {revenue}", record.product_id);
        total_revenue = total_revenue + revenue;
    }

    let expected = num!(672.3675);
    assert!(
        total_revenue.approx_eq(&expected, 1e-10, 0.0),
        "Sales analytics total mismatch: expected {expected}, got {total_revenue}"
    );

    println!("Total revenue after discounts: {total_revenue}");
}

fn main() {
    analyze_mixed_type_data();
    analyze_time_series();
    analyze_sales();
    println!("Data processing examples completed successfully.");
}