rust: Add Length type to paper-sizes crate.
authorBen Pfaff <blp@cs.stanford.edu>
Mon, 24 Nov 2025 17:09:37 +0000 (09:09 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Mon, 24 Nov 2025 17:11:54 +0000 (09:11 -0800)
rust/Cargo.lock
rust/paper-sizes/Cargo.toml
rust/paper-sizes/src/lib.rs
rust/paper-sizes/src/serde.rs [new file with mode: 0644]

index 4ad4518df28975573676b46997969c055c630c91..9ed9ede56c9c19bc93673b5827c269a22ace6eb6 100644 (file)
@@ -1571,7 +1571,7 @@ dependencies = [
 
 [[package]]
 name = "paper-sizes"
-version = "0.2.0"
+version = "0.3.0"
 dependencies = [
  "bindgen",
  "libc",
index 73611bff564255836628b108a1f04bf5c20490e6..28b88e2ed1914cb25b8e67a0476c734f26ad610f 100644 (file)
@@ -1,6 +1,6 @@
 [package]
 name = "paper-sizes"
-version = "0.2.0"
+version = "0.3.0"
 edition = "2024"
 license = "MIT OR Apache-2.0 OR LGPL-2.1-or-later"
 authors = [ "Ben Pfaff" ]
index 3303ee1033e98b1d43fa943fd5f814b3d2c4d64a..e312374bf312d16e0c4750ec7ca99ce156bc113d 100644 (file)
@@ -42,6 +42,9 @@ use xdg::BaseDirectories;
 #[cfg(target_os = "linux")]
 mod locale;
 
+#[cfg(feature = "serde")]
+mod serde;
+
 include!(concat!(env!("OUT_DIR"), "/paperspecs.rs"));
 
 static PAPERSIZE_FILENAME: &str = "papersize";
@@ -69,6 +72,14 @@ pub enum Unit {
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub struct ParseUnitError;
 
+impl Error for ParseUnitError {}
+
+impl Display for ParseUnitError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "unknown unit")
+    }
+}
+
 impl FromStr for Unit {
     type Err = ParseUnitError;
 
@@ -98,7 +109,7 @@ impl Unit {
     ///
     /// To convert a quantity of unit `a` into unit `b`, multiply by
     /// `a.as_unit(b)`.
-    fn as_unit(&self, other: Unit) -> f64 {
+    pub fn as_unit(&self, other: Unit) -> f64 {
         match (*self, other) {
             (Unit::Point, Unit::Point) => 1.0,
             (Unit::Point, Unit::Inch) => 1.0 / 72.0,
@@ -119,6 +130,80 @@ impl Display for Unit {
     }
 }
 
+/// A physical length with a [Unit].
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub struct Length {
+    /// The length.
+    pub value: f64,
+
+    /// The length's unit.
+    pub unit: Unit,
+}
+
+impl Length {
+    /// Constructs a new `Length` from `value` and `unit`.
+    pub fn new(value: f64, unit: Unit) -> Self {
+        Self { value, unit }
+    }
+
+    /// Returns this length converted to `unit`.
+    pub fn as_unit(&self, unit: Unit) -> Self {
+        Self {
+            value: self.value * unit.as_unit(Unit::Inch),
+            unit,
+        }
+    }
+
+    /// Returns the value of this length in `unit`.
+    pub fn into_unit(&self, unit: Unit) -> f64 {
+        self.as_unit(unit).value
+    }
+}
+
+/// An error parsing a [Length].
+#[derive(Copy, Clone, Debug)]
+pub enum ParseLengthError {
+    /// Missing unit.
+    MissingUnit,
+    /// Invalid unit.
+    InvalidUnit,
+    /// Invalid value
+    InvalidValue,
+}
+
+impl Error for ParseLengthError {}
+
+impl Display for ParseLengthError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            ParseLengthError::MissingUnit => write!(f, "Missing unit"),
+            ParseLengthError::InvalidUnit => write!(f, "Invalid unit of measurement"),
+            ParseLengthError::InvalidValue => write!(f, "Invalid length"),
+        }
+    }
+}
+
+impl FromStr for Length {
+    type Err = ParseLengthError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some(index) = s.find(|c: char| c.is_alphabetic()) {
+            let (value, unit) = s.split_at(index);
+            let value = value.parse().map_err(|_| ParseLengthError::InvalidValue)?;
+            let unit = unit.parse().map_err(|_| ParseLengthError::InvalidUnit)?;
+            Ok(Self { value, unit })
+        } else {
+            Err(ParseLengthError::MissingUnit)
+        }
+    }
+}
+
+impl Display for Length {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}{}", self.value, self.unit)
+    }
+}
+
 /// The size of a piece of paper.
 #[derive(Copy, Clone, Debug, PartialEq)]
 pub struct PaperSize {
@@ -170,6 +255,16 @@ impl PaperSize {
         let (bw, bh) = other.as_unit(unit).into_width_height();
         aw.round() == bw.round() && ah.round() == bh.round()
     }
+
+    /// Returns the paper's width as a [Length].
+    pub fn width(&self) -> Length {
+        Length::new(self.width, self.unit)
+    }
+
+    /// Returns the paper's height as a [Length].
+    pub fn height(&self) -> Length {
+        Length::new(self.height, self.unit)
+    }
 }
 
 /// An error parsing a [PaperSize].
@@ -240,29 +335,6 @@ impl Display for PaperSize {
     }
 }
 
-#[cfg(feature = "serde")]
-impl serde::Serialize for PaperSize {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        self.to_string().serialize(serializer)
-    }
-}
-
-#[cfg(feature = "serde")]
-impl<'de> serde::Deserialize<'de> for PaperSize {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: serde::Deserializer<'de>,
-    {
-        use serde::de::Error;
-        String::deserialize(deserializer)?
-            .parse()
-            .map_err(D::Error::custom)
-    }
-}
-
 /// An error parsing a [PaperSpec].
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum ParsePaperSpecError {
@@ -1028,17 +1100,4 @@ mod tests {
             );
         }
     }
-
-    #[cfg(feature = "serde")]
-    #[test]
-    fn test_serde() {
-        assert_eq!(
-            serde_json::to_string(&PaperSize::new(8.5, 11.0, Unit::Inch)).unwrap(),
-            "\"8.5x11in\""
-        );
-        assert_eq!(
-            serde_json::from_str::<PaperSize>("\"8.5x11in\"").unwrap(),
-            PaperSize::new(8.5, 11.0, Unit::Inch)
-        )
-    }
 }
diff --git a/rust/paper-sizes/src/serde.rs b/rust/paper-sizes/src/serde.rs
new file mode 100644 (file)
index 0000000..278f423
--- /dev/null
@@ -0,0 +1,100 @@
+use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error};
+
+#[cfg(feature = "serde")]
+use crate::PaperSize;
+use crate::{Length, Unit};
+
+impl<'de> Deserialize<'de> for Length {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        String::deserialize(deserializer)?
+            .parse()
+            .map_err(D::Error::custom)
+    }
+}
+
+impl Serialize for Length {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        self.to_string().serialize(serializer)
+    }
+}
+
+impl Serialize for PaperSize {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        self.to_string().serialize(serializer)
+    }
+}
+
+impl<'de> Deserialize<'de> for PaperSize {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        String::deserialize(deserializer)?
+            .parse()
+            .map_err(D::Error::custom)
+    }
+}
+
+impl Serialize for Unit {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        self.to_string().serialize(serializer)
+    }
+}
+
+impl<'de> Deserialize<'de> for Unit {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        String::deserialize(deserializer)?
+            .parse()
+            .map_err(D::Error::custom)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{Length, PaperSize, Unit};
+
+    #[test]
+    fn unit() {
+        assert_eq!(serde_json::to_string(&Unit::Point).unwrap(), "\"pt\"");
+        assert_eq!(serde_json::from_str::<Unit>("\"pt\"").unwrap(), Unit::Point)
+    }
+
+    #[test]
+    fn length() {
+        assert_eq!(
+            serde_json::to_string(&Length::new(123.0, Unit::Millimeter)).unwrap(),
+            "\"123mm\""
+        );
+        assert_eq!(
+            serde_json::from_str::<Length>("\"123mm\"").unwrap(),
+            Length::new(123.0, Unit::Millimeter)
+        )
+    }
+
+    #[test]
+    fn paper_size() {
+        assert_eq!(
+            serde_json::to_string(&PaperSize::new(8.5, 11.0, Unit::Inch)).unwrap(),
+            "\"8.5x11in\""
+        );
+        assert_eq!(
+            serde_json::from_str::<PaperSize>("\"8.5x11in\"").unwrap(),
+            PaperSize::new(8.5, 11.0, Unit::Inch)
+        )
+    }
+}