use std::borrow::Cow;
use std::fmt;
use std::str::FromStr;
use dear_imgui_rs::{NumericFormat, NumericFormatError};
#[derive(Clone, Debug, PartialEq)]
pub struct FloatFormat<'a>(NumericFormat<'a, f64>);
impl<'a> FloatFormat<'a> {
pub fn new(storage: impl Into<Cow<'a, str>>) -> Result<Self, FloatFormatError> {
let format = NumericFormat::new(storage)?;
if let Some(byte_offset) = format.as_str().bytes().position(|byte| !byte.is_ascii()) {
return Err(FloatFormatError::NonAscii { byte_offset });
}
Ok(Self(format))
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn borrowed(&self) -> FloatFormat<'_> {
FloatFormat(self.0.borrowed())
}
pub fn into_owned(self) -> FloatFormat<'static> {
FloatFormat(self.0.into_owned())
}
pub fn into_numeric_format(self) -> NumericFormat<'a, f64> {
self.0
}
}
impl AsRef<str> for FloatFormat<'_> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<'a> TryFrom<&'a str> for FloatFormat<'a> {
type Error = FloatFormatError;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<String> for FloatFormat<'static> {
type Error = FloatFormatError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl FromStr for FloatFormat<'static> {
type Err = FloatFormatError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value.to_owned())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum FloatFormatError {
Numeric(NumericFormatError),
NonAscii { byte_offset: usize },
}
impl fmt::Display for FloatFormatError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Numeric(error) => error.fmt(formatter),
Self::NonAscii { byte_offset } => write!(
formatter,
"ImPlot numeric formats must be ASCII; non-ASCII text starts at byte {byte_offset}"
),
}
}
}
impl std::error::Error for FloatFormatError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Numeric(error) => Some(error),
Self::NonAscii { .. } => None,
}
}
}
impl From<NumericFormatError> for FloatFormatError {
fn from(error: NumericFormatError) -> Self {
Self::Numeric(error)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AxisFormat<'a>(FloatFormat<'a>);
impl<'a> AxisFormat<'a> {
pub const MAX_LEN: usize = 15;
pub fn new(storage: impl Into<Cow<'a, str>>) -> Result<Self, AxisFormatError> {
Self::try_from(FloatFormat::new(storage)?)
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn borrowed(&self) -> AxisFormat<'_> {
AxisFormat(self.0.borrowed())
}
pub fn into_owned(self) -> AxisFormat<'static> {
AxisFormat(self.0.into_owned())
}
pub fn into_float_format(self) -> FloatFormat<'a> {
self.0
}
}
impl<'a> TryFrom<FloatFormat<'a>> for AxisFormat<'a> {
type Error = AxisFormatError;
fn try_from(format: FloatFormat<'a>) -> Result<Self, Self::Error> {
let length = format.as_str().len();
if length > Self::MAX_LEN {
return Err(AxisFormatError::TooLong {
length,
maximum: Self::MAX_LEN,
});
}
Ok(Self(format))
}
}
impl AsRef<str> for AxisFormat<'_> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl<'a> TryFrom<&'a str> for AxisFormat<'a> {
type Error = AxisFormatError;
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl TryFrom<String> for AxisFormat<'static> {
type Error = AxisFormatError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::new(value)
}
}
impl FromStr for AxisFormat<'static> {
type Err = AxisFormatError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::new(value.to_owned())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum AxisFormatError {
Format(FloatFormatError),
TooLong { length: usize, maximum: usize },
}
impl fmt::Display for AxisFormatError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Format(error) => error.fmt(formatter),
Self::TooLong { length, maximum } => write!(
formatter,
"axis format is {length} bytes; ImPlot supports at most {maximum}"
),
}
}
}
impl std::error::Error for AxisFormatError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Format(error) => Some(error),
Self::TooLong { .. } => None,
}
}
}
impl From<FloatFormatError> for AxisFormatError {
fn from(error: FloatFormatError) -> Self {
Self::Format(error)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn axis_formats_validate_numeric_type_and_storage_length() {
assert_eq!(AxisFormat::new("%.3f").unwrap().as_str(), "%.3f");
assert!(matches!(
AxisFormat::new("%s"),
Err(AxisFormatError::Format(_))
));
assert!(matches!(
FloatFormat::new("%.2f °C"),
Err(FloatFormatError::NonAscii { .. })
));
let maximum = format!("{}%.1f", "x".repeat(11));
assert_eq!(maximum.len(), AxisFormat::MAX_LEN);
assert!(AxisFormat::new(maximum).is_ok());
let too_long = format!("{}%.1f", "x".repeat(12));
assert!(matches!(
AxisFormat::new(too_long),
Err(AxisFormatError::TooLong { length: 16, .. })
));
}
}