rust: Add `PaperSize` parsing and formatting in paper-sizes crate.
authorBen Pfaff <blp@cs.stanford.edu>
Mon, 6 Oct 2025 01:10:54 +0000 (18:10 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Mon, 6 Oct 2025 16:10:35 +0000 (09:10 -0700)
rust/paper-sizes/src/lib.rs
rust/paper-sizes/testdata/td4/paperspecs

index 175b083ab2fa620922ac5827486dfdfe6ab0d828..f795356e04c7b450665c3eff1f48e74ce7ce7686 100644 (file)
@@ -172,6 +172,74 @@ impl PaperSize {
     }
 }
 
+/// An error parsing a [PaperSize].
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum ParsePaperSizeError {
+    /// Invalid paper height.
+    InvalidHeight,
+
+    /// Invalid paper width.
+    InvalidWidth,
+
+    /// Invalid unit of measurement.
+    InvalidUnit,
+
+    /// Missing unit of measurement.
+    MissingUnit,
+
+    /// Missing delimiter.
+    MissingDelimiter,
+}
+
+impl Error for ParsePaperSizeError {}
+
+impl Display for ParsePaperSizeError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ParsePaperSizeError::InvalidHeight => write!(f, "Invalid paper height"),
+            ParsePaperSizeError::InvalidWidth => write!(f, "Invalid paper width"),
+            ParsePaperSizeError::InvalidUnit => write!(f, "Invalid unit of measurement"),
+            ParsePaperSizeError::MissingUnit => write!(f, "Missing unit in paper size"),
+            ParsePaperSizeError::MissingDelimiter => write!(f, "Missing delimiter in paper size"),
+        }
+    }
+}
+
+impl FromStr for PaperSize {
+    type Err = ParsePaperSizeError;
+
+    /// Parses a paper size that takes one of the forms `8.5x11in` or `8.5,11in`
+    /// or `8.5,11,in`, with optional white space.
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let Some((width, rest)) = s.split_once([',', 'x']) else {
+            return Err(ParsePaperSizeError::MissingDelimiter);
+        };
+        let (height, unit) = if let Some(result) = rest.split_once(',') {
+            result
+        } else if let Some(alpha) = rest.find(|c: char| c.is_alphabetic()) {
+            rest.split_at(alpha)
+        } else {
+            return Err(ParsePaperSizeError::MissingUnit);
+        };
+
+        let width = f64::from_str(width.trim()).map_err(|_| ParsePaperSizeError::InvalidWidth)?;
+        let height =
+            f64::from_str(height.trim()).map_err(|_| ParsePaperSizeError::InvalidHeight)?;
+        let unit = Unit::from_str(unit.trim()).map_err(|_| ParsePaperSizeError::InvalidUnit)?;
+        Ok(Self {
+            width,
+            height,
+            unit,
+        })
+    }
+}
+
+impl Display for PaperSize {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}x{}{}", self.width, self.height, self.unit)
+    }
+}
+
 /// An error parsing a [PaperSpec].
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum ParsePaperSpecError {
@@ -188,6 +256,18 @@ pub enum ParsePaperSpecError {
     MissingField,
 }
 
+impl From<ParsePaperSizeError> for ParsePaperSpecError {
+    fn from(value: ParsePaperSizeError) -> Self {
+        match value {
+            ParsePaperSizeError::InvalidHeight => Self::InvalidHeight,
+            ParsePaperSizeError::InvalidWidth => Self::InvalidWidth,
+            ParsePaperSizeError::InvalidUnit => Self::InvalidUnit,
+            ParsePaperSizeError::MissingUnit => Self::MissingField,
+            ParsePaperSizeError::MissingDelimiter => Self::MissingField,
+        }
+    }
+}
+
 impl Error for ParsePaperSpecError {}
 
 impl Display for ParsePaperSpecError {
@@ -224,23 +304,16 @@ impl PaperSpec {
 impl FromStr for PaperSpec {
     type Err = ParsePaperSpecError;
 
+    /// Parses a paper specification as `name,<size>`, where `<size>` is one of
+    /// the formats supported by [PaperSize::from_str].
+    ///
+    /// The canonical form of a paper specification is `name,width,height,unit`.
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let mut tokens = s.split(',').map(|s| s.trim());
-        if let Some(name) = tokens.next()
-            && let Some(width) = tokens.next()
-            && let Some(height) = tokens.next()
-            && let Some(unit) = tokens.next()
-        {
-            let width = f64::from_str(width).map_err(|_| ParsePaperSpecError::InvalidWidth)?;
-            let height = f64::from_str(height).map_err(|_| ParsePaperSpecError::InvalidHeight)?;
-            let unit = Unit::from_str(unit).map_err(|_| ParsePaperSpecError::InvalidUnit)?;
-            Ok(Self {
-                name: String::from(name).into(),
-                size: PaperSize::new(width, height, unit),
-            })
-        } else {
-            Err(ParsePaperSpecError::MissingField)
-        }
+        let (name, size) = s.split_once(',').ok_or(ParsePaperSpecError::MissingField)?;
+        Ok(Self {
+            name: String::from(name).into(),
+            size: size.parse()?,
+        })
     }
 }
 
@@ -660,10 +733,11 @@ impl Catalog {
 
 #[cfg(test)]
 mod tests {
-    use std::{borrow::Cow, path::Path};
+    use std::{borrow::Cow, path::Path, str::FromStr};
 
     use crate::{
-        CatalogBuildError, CatalogBuilder, PaperSize, PaperSpec, ParsePaperSpecError, Unit, locale,
+        A4, CatalogBuildError, CatalogBuilder, PaperSize, PaperSpec, ParsePaperSizeError,
+        ParsePaperSpecError, Unit, locale,
     };
 
     #[test]
@@ -714,6 +788,47 @@ mod tests {
         );
     }
 
+    #[test]
+    fn papersize() {
+        assert_eq!(
+            "8.5x11in".parse(),
+            Ok(PaperSize::new(8.5, 11.0, Unit::Inch))
+        );
+        assert_eq!(
+            "8.5,11in".parse(),
+            Ok(PaperSize::new(8.5, 11.0, Unit::Inch))
+        );
+        assert_eq!(
+            " 8.5 x 11 in ".parse(),
+            Ok(PaperSize::new(8.5, 11.0, Unit::Inch))
+        );
+        assert_eq!(
+            PaperSize::from_str("8.5x.in"),
+            Err(ParsePaperSizeError::InvalidHeight)
+        );
+        assert_eq!(
+            PaperSize::from_str(".x11in"),
+            Err(ParsePaperSizeError::InvalidWidth)
+        );
+        assert_eq!(
+            PaperSize::from_str("8.5x11xyzzy"),
+            Err(ParsePaperSizeError::InvalidUnit)
+        );
+        assert_eq!(
+            PaperSize::from_str("8.5x11"),
+            Err(ParsePaperSizeError::MissingUnit)
+        );
+        assert_eq!(
+            PaperSize::from_str(" 8.5  11 in "),
+            Err(ParsePaperSizeError::MissingDelimiter)
+        );
+        assert_eq!(
+            PaperSize::new(8.5, 11.0, Unit::Inch).to_string(),
+            "8.5x11in"
+        );
+        assert_eq!(A4.size.to_string(), "210x297mm");
+    }
+
     #[test]
     fn paperspec() {
         assert_eq!(
@@ -723,6 +838,13 @@ mod tests {
                 PaperSize::new(8.5, 11.0, Unit::Inch)
             ))
         );
+        assert_eq!(
+            "Letter,8.5x11in".parse(),
+            Ok(PaperSpec::new(
+                Cow::from("Letter"),
+                PaperSize::new(8.5, 11.0, Unit::Inch)
+            ))
+        );
     }
 
     #[test]
index 8123450613b1a264a6d53e91a2357e3c6aa24e22..7a76c72256ca343619ed43e4a5327da1df74bfa5 100644 (file)
@@ -1,4 +1,4 @@
-not,enough,fields
+not enough fields
 OK,not a width,11,in
 OK,8.5,not a height,in
-OK,8.5,11,not a unit
\ No newline at end of file
+OK,8.5,11,not a unit