gunny 0.3.0

A library for rendering static text content from templates.
Documentation
use std::{cmp::Ordering, fmt::Display};

use serde::Serialize;

use crate::errors::JsonError;

/// The sign of a number.
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Sign {
    Negative,
    Positive,
}

impl Display for Sign {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Sign::Negative => write!(f, "-"),
            Sign::Positive => write!(f, "+"),
        }
    }
}

/// Can represent various types of signed or unsigned, integer or floating point numbers.
///
/// Note: equality of floating point numbers suffers from all of the usual problems with comparing
/// floating point numbers.
#[derive(Debug, Clone, PartialOrd)]
pub enum Number {
    UnsignedInteger(u64),
    UnsignedBigInteger(u128),
    SignedInteger(i64),
    SignedBigInteger(i128),
    Float(f64),
}

impl TryFrom<serde_json::Number> for Number {
    type Error = JsonError;

    fn try_from(value: serde_json::Number) -> Result<Self, Self::Error> {
        if value.is_i64() {
            Ok(Self::SignedInteger(value.as_i64().ok_or(
                JsonError::Number("hint was i64, but i64 was not returned".to_string()),
            )?))
        } else if value.is_u64() {
            Ok(Self::UnsignedInteger(value.as_u64().ok_or(
                JsonError::Number("hint was u64, but u64 was not returned".to_string()),
            )?))
        } else if value.is_f64() {
            Ok(Self::Float(value.as_f64().ok_or(JsonError::Number(
                "hint was f64, but f64 was not returned".to_string(),
            ))?))
        } else {
            Err(JsonError::Number(format!(
                "unrecognized number type: {}",
                value
            )))
        }
    }
}

impl Display for Number {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Number::UnsignedInteger(value) => write!(f, "{}", value),
            Number::UnsignedBigInteger(value) => write!(f, "{}", value),
            Number::SignedInteger(value) => write!(f, "{}", value),
            Number::SignedBigInteger(value) => write!(f, "{}", value),
            Number::Float(value) => write!(f, "{}", value),
        }
    }
}

impl PartialEq for Number {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::UnsignedInteger(a), Self::UnsignedInteger(b)) => a == b,
            (Self::UnsignedBigInteger(a), Self::UnsignedBigInteger(b)) => a == b,
            (Self::SignedInteger(a), Self::SignedInteger(b)) => a == b,
            (Self::SignedBigInteger(a), Self::SignedBigInteger(b)) => a == b,
            (Self::Float(a), Self::Float(b)) => a == b,
            _ => {
                self.sign() == other.sign()
                    && self.integer_part() == other.integer_part()
                    && self.fractional_part() == other.fractional_part()
            }
        }
    }
}

impl Eq for Number {}

impl Ord for Number {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        match (self, other) {
            (Number::UnsignedInteger(a), Number::UnsignedInteger(b)) => a.cmp(b),
            (Number::UnsignedBigInteger(a), Number::UnsignedBigInteger(b)) => a.cmp(b),
            (Number::SignedInteger(a), Number::SignedInteger(b)) => a.cmp(b),
            (Number::SignedBigInteger(a), Number::SignedBigInteger(b)) => a.cmp(b),
            (Number::Float(a), Number::Float(b)) => a.total_cmp(b),
            _ => {
                if self == other {
                    Ordering::Equal
                } else {
                    match self.sign().cmp(&other.sign()) {
                        Ordering::Less => Ordering::Less,
                        Ordering::Equal => match self.integer_part().cmp(&other.integer_part()) {
                            Ordering::Less => Ordering::Less,
                            Ordering::Equal => {
                                if self.fractional_part() < other.fractional_part() {
                                    Ordering::Less
                                } else {
                                    Ordering::Greater
                                }
                            }
                            Ordering::Greater => Ordering::Greater,
                        },
                        Ordering::Greater => Ordering::Greater,
                    }
                }
            }
        }
    }
}

impl From<i8> for Number {
    fn from(value: i8) -> Self {
        Self::SignedInteger(value.into())
    }
}

impl From<i16> for Number {
    fn from(value: i16) -> Self {
        Self::SignedInteger(value.into())
    }
}

impl From<i32> for Number {
    fn from(value: i32) -> Self {
        Self::SignedInteger(value.into())
    }
}

impl From<i64> for Number {
    fn from(value: i64) -> Self {
        Self::SignedInteger(value)
    }
}

impl From<i128> for Number {
    fn from(value: i128) -> Self {
        Self::SignedBigInteger(value)
    }
}

impl From<u8> for Number {
    fn from(value: u8) -> Self {
        Self::UnsignedInteger(value.into())
    }
}

impl From<u16> for Number {
    fn from(value: u16) -> Self {
        Self::UnsignedInteger(value.into())
    }
}

impl From<u32> for Number {
    fn from(value: u32) -> Self {
        Self::UnsignedInteger(value.into())
    }
}

impl From<u64> for Number {
    fn from(value: u64) -> Self {
        Self::UnsignedInteger(value.into())
    }
}

impl From<u128> for Number {
    fn from(value: u128) -> Self {
        Self::UnsignedBigInteger(value)
    }
}

impl From<f32> for Number {
    fn from(value: f32) -> Self {
        Self::Float(value.into())
    }
}

impl From<f64> for Number {
    fn from(value: f64) -> Self {
        Self::Float(value)
    }
}

impl Serialize for Number {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        match self {
            Number::UnsignedInteger(value) => serializer.serialize_u64(*value),
            Number::UnsignedBigInteger(value) => serializer.serialize_u128(*value),
            Number::SignedInteger(value) => serializer.serialize_i64(*value),
            Number::SignedBigInteger(value) => serializer.serialize_i128(*value),
            Number::Float(value) => serializer.serialize_f64(*value),
        }
    }
}

impl Number {
    /// Return the sign of the number.
    pub fn sign(&self) -> Sign {
        match self {
            Number::UnsignedInteger(_) => Sign::Positive,
            Number::UnsignedBigInteger(_) => Sign::Positive,
            Number::SignedInteger(value) => {
                if *value < 0 {
                    Sign::Negative
                } else {
                    Sign::Positive
                }
            }
            Number::SignedBigInteger(value) => {
                if *value < 0 {
                    Sign::Negative
                } else {
                    Sign::Positive
                }
            }
            Number::Float(value) => {
                if *value < 0.0 {
                    Sign::Negative
                } else {
                    Sign::Positive
                }
            }
        }
    }

    /// Return the whole (a.k.a. integer) part of the number.
    pub fn integer_part(&self) -> u128 {
        match self {
            Number::UnsignedInteger(value) => (*value).into(),
            Number::UnsignedBigInteger(value) => *value,
            Number::SignedInteger(value) => value.unsigned_abs().into(),
            Number::SignedBigInteger(value) => value.unsigned_abs(),
            Number::Float(value) => value.abs() as u128,
        }
    }

    /// Return the fractional part of the number, if it has one.
    pub fn fractional_part(&self) -> Option<f64> {
        match self {
            Number::Float(value) => {
                if value.abs().fract() == 0.0 {
                    None
                } else {
                    Some(value.abs().fract())
                }
            }
            _ => None,
        }
    }
}

#[cfg(test)]
mod test {
    use std::cmp::Ordering;

    use crate::number::Number;

    const NUMBER_ORDERING_TEST_CASES: &[(Number, Number, Ordering)] = &[
        (
            Number::SignedInteger(0),
            Number::SignedInteger(0),
            Ordering::Equal,
        ),
        (
            Number::SignedInteger(0),
            Number::SignedInteger(1),
            Ordering::Less,
        ),
        (
            Number::SignedInteger(1),
            Number::SignedInteger(-1),
            Ordering::Greater,
        ),
        (
            Number::UnsignedInteger(1),
            Number::SignedInteger(1),
            Ordering::Equal,
        ),
        (
            Number::SignedInteger(-1),
            Number::UnsignedInteger(1),
            Ordering::Less,
        ),
        (
            Number::Float(1.5),
            Number::UnsignedBigInteger(2),
            Ordering::Less,
        ),
        (
            Number::SignedInteger(2),
            Number::Float(1.5),
            Ordering::Greater,
        ),
    ];

    #[test]
    fn number_ordering_works() {
        for test_case in NUMBER_ORDERING_TEST_CASES {
            let (a, b, expected_ordering) = test_case;
            assert_eq!(
                a.cmp(b),
                *expected_ordering,
                "testing {:?} vs {:?}, expecting {:?}",
                a,
                b,
                *expected_ordering,
            );
        }
    }
}