#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, num::NonZeroUsize, str::FromStr};
use std::error::Error;
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum KinematicsKind {
Forward,
Inverse,
Differential,
Velocity,
Position,
Unknown,
Custom(String),
}
impl fmt::Display for KinematicsKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Forward => "forward",
Self::Inverse => "inverse",
Self::Differential => "differential",
Self::Velocity => "velocity",
Self::Position => "position",
Self::Unknown => "unknown",
Self::Custom(value) => value.as_str(),
})
}
}
impl FromStr for KinematicsKind {
type Err = KinematicsKindParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(KinematicsKindParseError::Empty);
}
match normalized_token(trimmed).as_str() {
"forward" | "forward-kinematics" => Ok(Self::Forward),
"inverse" | "inverse-kinematics" => Ok(Self::Inverse),
"differential" | "differential-kinematics" => Ok(Self::Differential),
"velocity" | "velocity-kinematics" => Ok(Self::Velocity),
"position" | "position-kinematics" => Ok(Self::Position),
"unknown" => Ok(Self::Unknown),
_ => Ok(Self::Custom(trimmed.to_string())),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct KinematicChainName(String);
impl KinematicChainName {
pub fn new(value: impl AsRef<str>) -> Result<Self, KinematicsTextError> {
non_empty_kinematics_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for KinematicChainName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for KinematicChainName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for KinematicChainName {
type Err = KinematicsTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct DegreeOfFreedom(NonZeroUsize);
impl DegreeOfFreedom {
pub const fn new(value: usize) -> Result<Self, DegreeOfFreedomError> {
match NonZeroUsize::new(value) {
Some(value) => Ok(Self(value)),
None => Err(DegreeOfFreedomError::Zero),
}
}
#[must_use]
pub const fn get(self) -> usize {
self.0.get()
}
}
impl TryFrom<usize> for DegreeOfFreedom {
type Error = DegreeOfFreedomError;
fn try_from(value: usize) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl fmt::Display for DegreeOfFreedom {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.get().fmt(formatter)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LinkName(String);
impl LinkName {
pub fn new(value: impl AsRef<str>) -> Result<Self, KinematicsTextError> {
non_empty_kinematics_text(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for LinkName {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for LinkName {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for LinkName {
type Err = KinematicsTextError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum KinematicsKindParseError {
Empty,
}
impl fmt::Display for KinematicsKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("kinematics kind cannot be empty"),
}
}
}
impl Error for KinematicsKindParseError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum KinematicsTextError {
Empty,
}
impl fmt::Display for KinematicsTextError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("kinematics text cannot be empty"),
}
}
}
impl Error for KinematicsTextError {}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DegreeOfFreedomError {
Zero,
}
impl fmt::Display for DegreeOfFreedomError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Zero => formatter.write_str("degree of freedom must be non-zero"),
}
}
}
impl Error for DegreeOfFreedomError {}
fn non_empty_kinematics_text(value: impl AsRef<str>) -> Result<String, KinematicsTextError> {
let trimmed = value.as_ref().trim();
if trimmed.is_empty() {
Err(KinematicsTextError::Empty)
} else {
Ok(trimmed.to_string())
}
}
fn normalized_token(value: &str) -> String {
value
.trim()
.chars()
.map(|character| match character {
'_' | ' ' => '-',
other => other.to_ascii_lowercase(),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::{
DegreeOfFreedom, DegreeOfFreedomError, KinematicChainName, KinematicsKind,
KinematicsKindParseError, KinematicsTextError,
};
#[test]
fn displays_and_parses_kinematics_kind() -> Result<(), KinematicsKindParseError> {
assert_eq!(
"forward kinematics".parse::<KinematicsKind>()?,
KinematicsKind::Forward
);
assert_eq!(KinematicsKind::Differential.to_string(), "differential");
Ok(())
}
#[test]
fn stores_custom_kinematics_kind() -> Result<(), KinematicsKindParseError> {
assert_eq!(
"redundancy-resolution".parse::<KinematicsKind>()?,
KinematicsKind::Custom("redundancy-resolution".to_string())
);
Ok(())
}
#[test]
fn constructs_valid_chain_name() -> Result<(), KinematicsTextError> {
let name = KinematicChainName::new(" arm-chain ")?;
assert_eq!(name.as_str(), "arm-chain");
Ok(())
}
#[test]
fn rejects_empty_chain_name() {
assert_eq!(KinematicChainName::new(""), Err(KinematicsTextError::Empty));
}
#[test]
fn constructs_valid_degree_of_freedom() -> Result<(), DegreeOfFreedomError> {
let dof = DegreeOfFreedom::new(6)?;
assert_eq!(dof.get(), 6);
assert_eq!(dof.to_string(), "6");
Ok(())
}
#[test]
fn rejects_zero_degree_of_freedom() {
assert_eq!(DegreeOfFreedom::new(0), Err(DegreeOfFreedomError::Zero));
}
}