neodes_codec 0.1.3

Library to read and write data from DSN files as described in the NeoDes norm.
Documentation
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::str::FromStr;

/// Structure representing the group part of a field ID
/// i.e. the `G00` in `S21.G00.42.001`
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
#[cfg_attr(
	feature = "serde",
	derive(serde_with::DeserializeFromStr, serde_with::SerializeDisplay)
)]
pub struct ShortGroupId(u8);

impl Display for ShortGroupId {
	#[inline]
	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
		write!(f, "G{:02}", self.0)
	}
}

const HIGH_BOUND: u8 = 100;

impl ShortGroupId {
	/// Creates a ShortFieldId from a number. Truncates the decimals to the last 2
	///
	/// # Examples
	///
	/// ```
	/// use neodes_codec::line::ShortGroupId;
	///
	/// assert_eq!(ShortGroupId::from_u8_lossy(000).to_string(), "G00");
	/// ```
	#[inline]
	pub const fn from_u8_lossy(value: u8) -> Self {
		ShortGroupId(value % HIGH_BOUND)
	}
}

/// Parsing error returned when parsing of `ShortGroupId` from a string failed.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub struct ShortGroupIdParsingError;

impl Error for ShortGroupIdParsingError {}

impl Display for ShortGroupIdParsingError {
	#[inline]
	fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
		write!(f, "Cannot parse ShortGroupId")
	}
}

impl FromStr for ShortGroupId {
	type Err = ShortGroupIdParsingError;

	/// # Examples
	///
	/// ```
	/// # use std::error::Error;
	/// # fn main() -> Result<(), Box<dyn Error>> {
	/// use neodes_codec::line::{ShortGroupId, ShortGroupIdParsingError};
	/// use std::str::FromStr;
	///
	/// assert_eq!(ShortGroupId::from_str("G13"), Ok(ShortGroupId::from_u8_lossy(13)));
	/// assert_eq!(ShortGroupId::from_str("XXX"), Err(ShortGroupIdParsingError));
	///
	/// # Ok(())
	/// # }
	/// ```
	#[inline]
	fn from_str(s: &str) -> Result<Self, Self::Err> {
		if s.len() == 3 && s.starts_with('G') {
			let i = s[1..3].parse().map_err(|_| ShortGroupIdParsingError)?;
			Ok(ShortGroupId(i))
		} else {
			Err(ShortGroupIdParsingError)
		}
	}
}

macro_rules! impl_numeric_traits {
    ( $( $t:ty ),* ) => {
        $(
            impl PartialEq<$t> for ShortGroupId {
				#[inline]
                fn eq(&self, other: &$t) -> bool {
                    match u8::try_from(*other) {
                        Ok(val) => self.0 == val,
                        Err(_) => false,
                    }
                }
            }

			impl PartialEq<ShortGroupId> for $t {
				#[inline]
				fn eq(&self, other: &ShortGroupId) -> bool {
					match u8::try_from(*self) {
						Ok(val) => other.0 == val,
						Err(_) => false,
					}
				}
			}

			impl TryFrom<$t> for ShortGroupId {
				type Error = ShortGroupIdParsingError;

				#[inline]
				fn try_from(value: $t) -> Result<Self, Self::Error> {
					match u8::try_from(value) {
						Ok(x) if x < HIGH_BOUND => Ok(Self(x)),
						_ => Err(ShortGroupIdParsingError)
					}
				}
			}
        )*
    }
}

macro_rules! impl_string_cmp {
    ( $( $t:ty ),* ) => {
        $(
            impl PartialEq<$t> for ShortGroupId {
				#[inline]
                fn eq(&self, other: &$t) -> bool {
                    let s = other.as_ref();
                    match ShortGroupId::from_str(s) {
                        Ok(n) => *self == n,
                        Err(_) => false,
                    }
                }
            }

			impl PartialEq<ShortGroupId> for $t {
				#[inline]
				fn eq(&self, other: &ShortGroupId) -> bool {
					other == self
				}
			}
        )*
    }
}

impl_numeric_traits!(
	usize, u8, u16, u32, u64, u128, isize, i8, i16, i32, i64, i128
);

impl_string_cmp!(str, &str, String);

#[cfg(test)]
mod tests {
	use crate::line::id::short_group_id::{ShortGroupId, ShortGroupIdParsingError};
	use parameterized::parameterized;
	use quickcheck_macros::quickcheck;
	use std::str::FromStr;

	#[parameterized(input = {
			"X01",
			"GX1",
			"G",
			"G0",
			"G000"
		})]
	fn returns_parsing_errors(input: &str) {
		assert_eq!(ShortGroupId::from_str(input), Err(ShortGroupIdParsingError));
	}

	#[quickcheck]
	fn check_from_lossy(n: u8) {
		assert_eq!(
			ShortGroupId::from_u8_lossy(n),
			ShortGroupId::from_u8_lossy(n % 100)
		);
	}

	#[quickcheck]
	fn check_display(n: u8) {
		assert_eq!(
			ShortGroupId::from_u8_lossy(n).to_string(),
			format!("G{:02}", n % 100)
		);
	}
	#[quickcheck]
	fn check_from_str(n: u8) {
		assert_eq!(
			ShortGroupId::from_str(format!("G{:02}", n % 100).as_str()),
			Ok(ShortGroupId::from_u8_lossy(n))
		);
	}

	#[quickcheck]
	fn check_from_int(n: isize) {
		let short_group_id = ShortGroupId::try_from(n);
		assert_eq!(short_group_id.is_ok(), n >= 0 && n < 100);
		if let Ok(short_group_id) = short_group_id {
			assert_eq!(n as u8, short_group_id.0);
		}
	}

	#[test]
	fn test_equals() {
		let id = ShortGroupId::from_u8_lossy(21);
		assert_eq!(id, 21usize);
		assert_eq!(id, 21u8);
		assert_eq!(id, 21u16);
		assert_eq!(id, 21u32);
		assert_eq!(id, 21u64);
		assert_eq!(id, 21u128);

		assert_eq!(id, 21isize);
		assert_eq!(id, 21i8);
		assert_eq!(id, 21i16);
		assert_eq!(id, 21i32);
		assert_eq!(id, 21i64);
		assert_eq!(id, 21i128);

		assert_eq!(21usize, id);
		assert_eq!(21u8, id);
		assert_eq!(21u16, id);
		assert_eq!(21u32, id);
		assert_eq!(21u64, id);
		assert_eq!(21u128, id);

		assert_eq!(21isize, id);
		assert_eq!(21i8, id);
		assert_eq!(21i16, id);
		assert_eq!(21i32, id);
		assert_eq!(21i64, id);
		assert_eq!(21i128, id);

		assert_eq!(id, "G21");
		assert_eq!(id, "G21".to_string());

		assert_eq!("G21", id);
		assert_eq!("G21".to_string(), id);
	}

	#[parameterized(input = {
		"22",
		"20",
		"121",
		"21.0",
		"abc",
		"",
		"21 ",
		" 21"
	})]
	fn test_not_equals(input: &str) {
		let id = ShortGroupId::from_u8_lossy(21);

		assert_ne!(id, input);
		assert_ne!(input, id);
		assert_ne!(id, input.to_string());
		assert_ne!(input.to_string(), id);
	}

	#[test]
	fn test_from_number() {
		let expected = Ok(ShortGroupId::from_u8_lossy(7));
		assert_eq!(ShortGroupId::try_from(7usize), expected);
		assert_eq!(ShortGroupId::try_from(7u8), expected);
		assert_eq!(ShortGroupId::try_from(7u16), expected);
		assert_eq!(ShortGroupId::try_from(7u32), expected);
		assert_eq!(ShortGroupId::try_from(7u64), expected);
		assert_eq!(ShortGroupId::try_from(7u128), expected);

		assert_eq!(ShortGroupId::try_from(7isize), expected);
		assert_eq!(ShortGroupId::try_from(7i8), expected);
		assert_eq!(ShortGroupId::try_from(7i16), expected);
		assert_eq!(ShortGroupId::try_from(7i32), expected);
		assert_eq!(ShortGroupId::try_from(7i64), expected);
		assert_eq!(ShortGroupId::try_from(7i128), expected);
	}

	#[test]
	fn test_try_from_number_fail() {
		let expected = Err(ShortGroupIdParsingError);
		assert_eq!(ShortGroupId::try_from(117usize), expected);
		assert_eq!(ShortGroupId::try_from(117u16), expected);
		assert_eq!(ShortGroupId::try_from(117u32), expected);
		assert_eq!(ShortGroupId::try_from(117u64), expected);
		assert_eq!(ShortGroupId::try_from(117u128), expected);

		assert_eq!(ShortGroupId::try_from(117isize), expected);
		assert_eq!(ShortGroupId::try_from(117i16), expected);
		assert_eq!(ShortGroupId::try_from(117i32), expected);
		assert_eq!(ShortGroupId::try_from(117i64), expected);
		assert_eq!(ShortGroupId::try_from(117i128), expected);
	}
}