use std::{
borrow::Cow,
num::{ParseFloatError, ParseIntError, TryFromIntError},
ops::BitAnd,
path::PathBuf,
str::FromStr,
};
use thiserror::Error;
#[derive(Debug, Error, PartialEq)]
pub enum NumberError {
#[error("invalid conversion")]
InvalidConversion,
#[error("{0}")]
TryFromInt(#[from] TryFromIntError),
#[error("{0}")]
ParseInt(#[from] ParseIntError),
#[error("{0}")]
ParseFloat(#[from] ParseFloatError),
#[error("other: {0}")]
Other(String),
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum Number {
Int(i64),
Uint(u64),
Float(f64),
}
impl TryFrom<Number> for i64 {
type Error = NumberError;
fn try_from(value: Number) -> Result<Self, Self::Error> {
match value {
Number::Float(v) => {
if v >= i64::MIN as f64 && v <= i64::MAX as f64 {
return Ok(v as i64);
}
Err(NumberError::InvalidConversion)
}
Number::Uint(v) => v.try_into().map_err(NumberError::TryFromInt),
Number::Int(v) => Ok(v),
}
}
}
impl TryFrom<Number> for u64 {
type Error = NumberError;
fn try_from(value: Number) -> Result<Self, Self::Error> {
match value {
Number::Float(v) => {
if v >= 0.0 && v <= u64::MAX as f64 {
return Ok(v as u64);
}
Err(NumberError::InvalidConversion)
}
Number::Uint(v) => Ok(v),
Number::Int(v) => v.try_into().map_err(NumberError::TryFromInt),
}
}
}
impl TryFrom<Number> for f64 {
type Error = NumberError;
fn try_from(value: Number) -> Result<Self, Self::Error> {
match value {
Number::Float(v) => Ok(v),
_ => Err(NumberError::InvalidConversion),
}
}
}
impl std::fmt::Display for Number {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Float(v) => write!(f, "{v}"),
Self::Uint(v) => write!(f, "{v}"),
Self::Int(v) => write!(f, "{v}"),
}
}
}
impl BitAnd for Number {
type Output = Self;
#[inline(always)]
fn bitand(self, rhs: Self) -> Self::Output {
if let (Self::Int(s), Self::Int(o)) = (&self, &rhs) {
return Self::Int(s & o);
}
if let (Self::Uint(s), Self::Uint(o)) = (&self, &rhs) {
return Self::Uint(s & o);
}
if matches!(self, Self::Float(_)) || matches!(rhs, Self::Float(_)) {
panic!("cannot bitand floats")
}
panic!("numbers needs to be of the same type")
}
}
macro_rules! impl_unsigned_number {
($($src:ty),*) => {
$(impl From<$src> for Number {
#[inline(always)]
fn from(value: $src) -> Self {
Self::Uint(value as u64)
}
})*
};
}
macro_rules! impl_signed_number {
($($src:ty),*) => {
$(impl From<$src> for Number {
#[inline(always)]
fn from(value: $src) -> Self {
if value < 0 {
return Self::Int(value as i64);
}
Self::Uint(value as u64)
}
})*
};
}
impl_unsigned_number!(u8, u16, u32, u64, usize);
impl_signed_number!(i8, i16, i32, i64, isize);
impl From<f32> for Number {
#[inline(always)]
fn from(value: f32) -> Self {
Self::from(value as f64)
}
}
impl From<f64> for Number {
#[inline(always)]
fn from(value: f64) -> Self {
Self::Float(value)
}
}
impl FromStr for Number {
type Err = NumberError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl Number {
pub fn parse<S: AsRef<str>>(s: S) -> Result<Self, NumberError> {
let s = s.as_ref();
if s.starts_with("0x") {
return Ok(u64::from_str_radix(s.strip_prefix("0x").unwrap(), 16)?.into());
}
if s.starts_with('-') {
if s.contains('.') {
return Ok(f64::from_str(s)?.into());
}
return Ok(i64::from_str(s)?.into());
}
if s.contains('.') {
return Ok(f64::from_str(s)?.into());
}
Ok(u64::from_str(s)?.into())
}
#[inline(always)]
pub fn is_uint(&self) -> bool {
matches!(self, Self::Uint(_))
}
#[inline(always)]
pub fn is_float(&self) -> bool {
matches!(self, Self::Float(_))
}
#[inline(always)]
pub fn is_int(&self) -> bool {
matches!(self, Self::Int(_))
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum FieldValue<'field> {
Vector(Vec<FieldValue<'field>>),
String(Cow<'field, str>),
Number(Number),
Bool(bool),
Some,
None,
}
impl FieldValue<'_> {
pub(crate) const fn type_str(&self) -> &'static str {
match self {
Self::Vector(_) => "vector",
Self::Bool(_) => "bool",
Self::String(_) => "string",
Self::Number(_) => "number",
Self::Some => "some",
Self::None => "none",
}
}
pub(crate) fn string_into_number(&self) -> Result<Self, NumberError> {
match self {
Self::String(s) => Ok(Self::Number(Number::from_str(s)?)),
_ => Err(NumberError::Other(String::from(
"enum variant is not a string",
))),
}
}
pub(crate) const fn is_string(&self) -> bool {
matches!(self, FieldValue::String(_))
}
#[inline(always)]
pub(crate) const fn is_some(&self) -> bool {
!self.is_none()
}
#[inline(always)]
pub(crate) const fn is_none(&self) -> bool {
matches!(self, FieldValue::None)
}
#[cfg(test)]
#[inline(always)]
pub(crate) const fn is_vector(&self) -> bool {
matches!(self, FieldValue::Vector(_))
}
}
macro_rules! impl_field_value_number {
($($src:ty),*) => {
$(
impl From<$src> for FieldValue<'_> {
fn from(value: $src) -> Self {
Self::Number(value.into())
}
}
impl From<&$src> for FieldValue<'_> {
fn from(value: &$src) -> Self {
Self::Number((*value).into())
}
}
)*
};
}
impl_field_value_number!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64);
impl<'s> From<Cow<'s, str>> for FieldValue<'s> {
fn from(value: Cow<'s, str>) -> Self {
Self::String(value)
}
}
impl<'s> From<&'s Cow<'s, str>> for FieldValue<'s> {
fn from(value: &'s Cow<'s, str>) -> Self {
Self::String(value.as_ref().into())
}
}
impl<'s> From<Cow<'s, PathBuf>> for FieldValue<'s> {
fn from(value: Cow<'s, PathBuf>) -> Self {
value.to_string_lossy().to_string().into()
}
}
impl<'s> From<&'s Cow<'s, PathBuf>> for FieldValue<'s> {
fn from(value: &'s Cow<'s, PathBuf>) -> Self {
value.to_string_lossy().into()
}
}
impl<'s> From<&'s str> for FieldValue<'s> {
fn from(value: &'s str) -> Self {
Self::String(Cow::Borrowed(value))
}
}
impl<'s> From<&&'s str> for FieldValue<'s> {
fn from(value: &&'s str) -> Self {
Self::String(Cow::Borrowed(value))
}
}
impl From<String> for FieldValue<'_> {
fn from(value: String) -> Self {
Self::String(Cow::from(value))
}
}
impl<'s> From<&'s String> for FieldValue<'s> {
fn from(value: &'s String) -> Self {
Self::String(Cow::from(value))
}
}
impl From<PathBuf> for FieldValue<'_> {
fn from(value: PathBuf) -> Self {
value.to_string_lossy().to_string().into()
}
}
impl<'f> From<&'f PathBuf> for FieldValue<'f> {
fn from(value: &'f PathBuf) -> Self {
value.to_string_lossy().into()
}
}
impl From<bool> for FieldValue<'_> {
fn from(value: bool) -> Self {
Self::Bool(value)
}
}
impl From<&bool> for FieldValue<'_> {
fn from(value: &bool) -> Self {
Self::Bool(*value)
}
}
impl<'s, T> From<Option<T>> for FieldValue<'s>
where
T: Into<FieldValue<'s>>,
{
fn from(value: Option<T>) -> Self {
match value {
Some(b) => b.into(),
None => Self::None,
}
}
}
impl<'s, T> From<&'s Option<T>> for FieldValue<'s>
where
T: Into<FieldValue<'s>> + Clone,
FieldValue<'s>: std::convert::From<&'s T>,
{
fn from(value: &'s Option<T>) -> Self {
match value {
Some(b) => b.into(),
None => Self::None,
}
}
}
macro_rules! impl_field_value_vec_conversions {
($($ty:ty),*) => {
$(
impl<'s> From<&'s [$ty]> for FieldValue<'s> {
fn from(value: &'s [$ty]) -> Self {
Self::Vector(value.iter().map(|t| t.into()).collect())
}
}
impl<'s> From<&'s Vec<$ty>> for FieldValue<'s> {
fn from(value: &'s Vec<$ty>) -> Self {
value.as_slice().into()
}
}
impl<'s> From<Vec<$ty>> for FieldValue<'s> {
fn from(value: Vec<$ty>) -> Self {
Self::Vector(value.into_iter().map(|t| t.into()).collect())
}
}
)*
};
}
impl_field_value_vec_conversions!(
String,
&'s str,
Cow<'s, str>,
PathBuf,
i8,
i16,
i32,
i64,
isize,
u8,
u16,
u32,
u64,
usize,
f32,
f64,
bool
);
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_number() {
let a = Number::from(42u8);
assert!(a.is_uint());
let b = Number::from(42isize);
assert!(b.is_uint());
let c = Number::from(4242);
assert!(Number::from(-42).is_int());
assert_eq!(a, b);
assert!(a < c);
assert!(c > b);
let f = Number::from(42.0);
assert!(f.is_float());
let e = Number::from(0x40_u32);
assert!(Number::from(0x100040) & e == e);
assert!(Number::from(0x100020) & e != e);
assert_eq!(Number::from_str("-1").unwrap(), Number::from(-1));
assert_eq!(Number::from_str("0.41").unwrap(), Number::from(0.41));
}
#[test]
fn test_field_value() {
assert_eq!(
FieldValue::String("42.0".into())
.string_into_number()
.unwrap(),
FieldValue::Number(42.0.into())
);
assert_eq!(
FieldValue::String("-0.42".into())
.string_into_number()
.unwrap(),
FieldValue::Number(Number::from(-0.42))
);
assert_eq!(
FieldValue::String("-42".into())
.string_into_number()
.unwrap(),
FieldValue::Number(Number::from(-42))
);
}
}