rvf 0.1.0

Rust implementation of the ValueFlows vocabulary for distributed economic networks
Documentation
//! Measurement units and quantities for economic resources
//!
//! This module provides types for representing quantities with units of measure,
//! following the ValueFlows specification for handling economic quantities.

use rust_decimal::Decimal;
use std::fmt;

#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

/// A measurement with a numeric value and unit
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Measure {
    /// The numeric value
    #[cfg_attr(feature = "serde", serde(with = "rust_decimal::serde::str"))]
    pub value: Decimal,
    /// The unit of measure
    pub unit: Unit,
}

impl Measure {
    /// Create a new measure from a value and unit
    pub fn new(value: impl Into<Decimal>, unit: Unit) -> Self {
        Self {
            value: value.into(),
            unit,
        }
    }

    /// Create a measure with value 0
    pub fn zero(unit: Unit) -> Self {
        Self {
            value: Decimal::ZERO,
            unit,
        }
    }

    /// Create a measure from an f64 value
    pub fn from_f64(value: f64, unit: Unit) -> Self {
        Self {
            value: Decimal::try_from(value).unwrap_or(Decimal::ZERO),
            unit,
        }
    }

    /// Check if this measure has the same unit as another
    pub fn same_unit(&self, other: &Measure) -> bool {
        self.unit == other.unit
    }

    /// Add two measures (must have the same unit)
    pub fn add(&self, other: &Measure) -> Option<Measure> {
        if self.same_unit(other) {
            Some(Measure {
                value: self.value + other.value,
                unit: self.unit.clone(),
            })
        } else {
            None
        }
    }

    /// Subtract another measure from this one (must have the same unit)
    pub fn sub(&self, other: &Measure) -> Option<Measure> {
        if self.same_unit(other) {
            Some(Measure {
                value: self.value - other.value,
                unit: self.unit.clone(),
            })
        } else {
            None
        }
    }

    /// Check if the value is zero
    pub fn is_zero(&self) -> bool {
        self.value.is_zero()
    }

    /// Check if the value is positive
    pub fn is_positive(&self) -> bool {
        self.value > Decimal::ZERO
    }

    /// Check if the value is negative
    pub fn is_negative(&self) -> bool {
        self.value < Decimal::ZERO
    }

    /// Scale the measure by a factor
    pub fn scale(&self, factor: Decimal) -> Measure {
        Measure {
            value: self.value * factor,
            unit: self.unit.clone(),
        }
    }
}

impl fmt::Display for Measure {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {}", self.value, self.unit)
    }
}

impl Default for Measure {
    fn default() -> Self {
        Self {
            value: Decimal::ZERO,
            unit: Unit::Each,
        }
    }
}

/// Standard units of measure
///
/// These cover common cases, but the `Custom` variant allows
/// for domain-specific units.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum Unit {
    // Count units
    /// Individual items/units
    Each,
    /// Pairs of items
    Pair,
    /// Dozen (12 items)
    Dozen,

    // Weight/Mass units
    /// Grams
    Gram,
    /// Kilograms
    Kilogram,
    /// Metric tons
    Tonne,
    /// Ounces
    Ounce,
    /// Pounds
    Pound,

    // Volume units
    /// Milliliters
    Milliliter,
    /// Liters
    Liter,
    /// Cubic meters
    CubicMeter,
    /// Fluid ounces
    FluidOunce,
    /// Gallons
    Gallon,

    // Length/Distance units
    /// Millimeters
    Millimeter,
    /// Centimeters
    Centimeter,
    /// Meters
    Meter,
    /// Kilometers
    Kilometer,
    /// Inches
    Inch,
    /// Feet
    Foot,
    /// Miles
    Mile,

    // Area units
    /// Square meters
    SquareMeter,
    /// Square kilometers
    SquareKilometer,
    /// Hectares
    Hectare,
    /// Acres
    Acre,

    // Time units (for work/effort)
    /// Seconds
    Second,
    /// Minutes
    Minute,
    /// Hours
    Hour,
    /// Days
    Day,
    /// Weeks
    Week,
    /// Months
    Month,
    /// Years
    Year,

    // Energy units
    /// Joules
    Joule,
    /// Kilowatt-hours
    KilowattHour,
    /// Calories
    Calorie,
    /// British Thermal Units
    Btu,

    // Currency/monetary units
    /// US Dollars
    Usd,
    /// Euros
    Eur,
    /// British Pounds
    Gbp,
    /// Japanese Yen
    Jpy,
    /// Chinese Yuan
    Cny,
    /// Generic currency (for abstract accounting)
    Currency,
    /// Tokens (for alternative currencies)
    Token,
    /// Credits
    Credit,

    // Digital units
    /// Bytes
    Byte,
    /// Kilobytes
    Kilobyte,
    /// Megabytes
    Megabyte,
    /// Gigabytes
    Gigabyte,
    /// Terabytes
    Terabyte,

    // Custom unit with a name
    Custom(String),
}

impl Unit {
    /// Create a custom unit with the given name
    pub fn custom(name: impl Into<String>) -> Self {
        Unit::Custom(name.into())
    }

    /// Get the symbol or abbreviation for this unit
    pub fn symbol(&self) -> &str {
        match self {
            Unit::Each => "ea",
            Unit::Pair => "pr",
            Unit::Dozen => "doz",
            Unit::Gram => "g",
            Unit::Kilogram => "kg",
            Unit::Tonne => "t",
            Unit::Ounce => "oz",
            Unit::Pound => "lb",
            Unit::Milliliter => "ml",
            Unit::Liter => "L",
            Unit::CubicMeter => "",
            Unit::FluidOunce => "fl oz",
            Unit::Gallon => "gal",
            Unit::Millimeter => "mm",
            Unit::Centimeter => "cm",
            Unit::Meter => "m",
            Unit::Kilometer => "km",
            Unit::Inch => "in",
            Unit::Foot => "ft",
            Unit::Mile => "mi",
            Unit::SquareMeter => "",
            Unit::SquareKilometer => "km²",
            Unit::Hectare => "ha",
            Unit::Acre => "ac",
            Unit::Second => "s",
            Unit::Minute => "min",
            Unit::Hour => "h",
            Unit::Day => "d",
            Unit::Week => "wk",
            Unit::Month => "mo",
            Unit::Year => "yr",
            Unit::Joule => "J",
            Unit::KilowattHour => "kWh",
            Unit::Calorie => "cal",
            Unit::Btu => "BTU",
            Unit::Usd => "USD",
            Unit::Eur => "EUR",
            Unit::Gbp => "GBP",
            Unit::Jpy => "JPY",
            Unit::Cny => "CNY",
            Unit::Currency => "¤",
            Unit::Token => "tok",
            Unit::Credit => "cr",
            Unit::Byte => "B",
            Unit::Kilobyte => "KB",
            Unit::Megabyte => "MB",
            Unit::Gigabyte => "GB",
            Unit::Terabyte => "TB",
            Unit::Custom(name) => name.as_str(),
        }
    }

    /// Check if this is a time-based unit
    pub fn is_time_unit(&self) -> bool {
        matches!(
            self,
            Unit::Second
                | Unit::Minute
                | Unit::Hour
                | Unit::Day
                | Unit::Week
                | Unit::Month
                | Unit::Year
        )
    }

    /// Check if this is a currency unit
    pub fn is_currency(&self) -> bool {
        matches!(
            self,
            Unit::Usd
                | Unit::Eur
                | Unit::Gbp
                | Unit::Jpy
                | Unit::Cny
                | Unit::Currency
                | Unit::Token
                | Unit::Credit
        )
    }
}

impl fmt::Display for Unit {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.symbol())
    }
}

impl Default for Unit {
    fn default() -> Self {
        Unit::Each
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_measure_operations() {
        let m1 = Measure::new(10, Unit::Kilogram);
        let m2 = Measure::new(5, Unit::Kilogram);

        let sum = m1.add(&m2).unwrap();
        assert_eq!(sum.value, Decimal::from(15));

        let diff = m1.sub(&m2).unwrap();
        assert_eq!(diff.value, Decimal::from(5));
    }

    #[test]
    fn test_unit_mismatch() {
        let m1 = Measure::new(10, Unit::Kilogram);
        let m2 = Measure::new(5, Unit::Liter);

        assert!(m1.add(&m2).is_none());
        assert!(m1.sub(&m2).is_none());
    }

    #[test]
    fn test_measure_scale() {
        let m = Measure::new(10, Unit::Each);
        let scaled = m.scale(Decimal::from(3));
        assert_eq!(scaled.value, Decimal::from(30));
    }

    #[test]
    fn test_custom_unit() {
        let unit = Unit::custom("widgets");
        assert_eq!(unit.symbol(), "widgets");
    }

    #[cfg(feature = "serde")]
    #[test]
    fn test_measure_serialization() {
        let m = Measure::new(10, Unit::Kilogram);
        let json = serde_json::to_string(&m).unwrap();
        let parsed: Measure = serde_json::from_str(&json).unwrap();
        assert_eq!(m, parsed);
    }
}