use rust_decimal::Decimal;
use std::fmt;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Measure {
#[cfg_attr(feature = "serde", serde(with = "rust_decimal::serde::str"))]
pub value: Decimal,
pub unit: Unit,
}
impl Measure {
pub fn new(value: impl Into<Decimal>, unit: Unit) -> Self {
Self {
value: value.into(),
unit,
}
}
pub fn zero(unit: Unit) -> Self {
Self {
value: Decimal::ZERO,
unit,
}
}
pub fn from_f64(value: f64, unit: Unit) -> Self {
Self {
value: Decimal::try_from(value).unwrap_or(Decimal::ZERO),
unit,
}
}
pub fn same_unit(&self, other: &Measure) -> bool {
self.unit == other.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
}
}
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
}
}
pub fn is_zero(&self) -> bool {
self.value.is_zero()
}
pub fn is_positive(&self) -> bool {
self.value > Decimal::ZERO
}
pub fn is_negative(&self) -> bool {
self.value < Decimal::ZERO
}
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,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum Unit {
Each,
Pair,
Dozen,
Gram,
Kilogram,
Tonne,
Ounce,
Pound,
Milliliter,
Liter,
CubicMeter,
FluidOunce,
Gallon,
Millimeter,
Centimeter,
Meter,
Kilometer,
Inch,
Foot,
Mile,
SquareMeter,
SquareKilometer,
Hectare,
Acre,
Second,
Minute,
Hour,
Day,
Week,
Month,
Year,
Joule,
KilowattHour,
Calorie,
Btu,
Usd,
Eur,
Gbp,
Jpy,
Cny,
Currency,
Token,
Credit,
Byte,
Kilobyte,
Megabyte,
Gigabyte,
Terabyte,
Custom(String),
}
impl Unit {
pub fn custom(name: impl Into<String>) -> Self {
Unit::Custom(name.into())
}
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 => "m³",
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 => "m²",
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(),
}
}
pub fn is_time_unit(&self) -> bool {
matches!(
self,
Unit::Second
| Unit::Minute
| Unit::Hour
| Unit::Day
| Unit::Week
| Unit::Month
| Unit::Year
)
}
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);
}
}