use std::fmt;
use std::ops::{Add, Div, Mul, Neg, Sub};
use serde::{Deserialize, Deserializer, Serialize};
use crate::error::{FoundationError, FoundationResult};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[repr(transparent)]
pub struct HwpUnit(i32);
const _: () = assert!(std::mem::size_of::<HwpUnit>() == 4);
const HWPUNIT_PER_PT: f64 = 100.0;
const HWPUNIT_PER_INCH: f64 = 7200.0;
const HWPUNIT_PER_MM: f64 = 7200.0 / 25.4;
impl HwpUnit {
pub const MIN_VALUE: i32 = -100_000_000;
pub const MAX_VALUE: i32 = 100_000_000;
pub const ZERO: Self = Self(0);
pub const ONE_PT: Self = Self(100);
pub fn new(value: i32) -> FoundationResult<Self> {
if !(Self::MIN_VALUE..=Self::MAX_VALUE).contains(&value) {
return Err(FoundationError::InvalidHwpUnit {
value: value as i64, min: Self::MIN_VALUE,
max: Self::MAX_VALUE,
});
}
Ok(Self(value))
}
pub(crate) const fn new_unchecked(value: i32) -> Self {
Self(value)
}
pub const fn as_i32(self) -> i32 {
self.0
}
pub const fn is_zero(&self) -> bool {
self.0 == 0
}
pub fn from_pt(pt: f64) -> FoundationResult<Self> {
Self::from_f64(pt, HWPUNIT_PER_PT, "pt")
}
pub fn from_mm(mm: f64) -> FoundationResult<Self> {
Self::from_f64(mm, HWPUNIT_PER_MM, "mm")
}
pub fn from_inch(inch: f64) -> FoundationResult<Self> {
Self::from_f64(inch, HWPUNIT_PER_INCH, "inch")
}
pub fn to_pt(self) -> f64 {
self.0 as f64 / HWPUNIT_PER_PT
}
pub fn to_mm(self) -> f64 {
self.0 as f64 / HWPUNIT_PER_MM
}
pub fn to_inch(self) -> f64 {
self.0 as f64 / HWPUNIT_PER_INCH
}
fn from_f64(value: f64, scale: f64, unit_name: &str) -> FoundationResult<Self> {
if !value.is_finite() {
return Err(FoundationError::InvalidField {
field: unit_name.to_string(),
reason: format!("{value} is not finite"),
});
}
let raw = (value * scale).round() as i64;
if raw < Self::MIN_VALUE as i64 || raw > Self::MAX_VALUE as i64 {
return Err(FoundationError::InvalidHwpUnit {
value: raw, min: Self::MIN_VALUE,
max: Self::MAX_VALUE,
});
}
Ok(Self(raw as i32))
}
}
impl Default for HwpUnit {
fn default() -> Self {
Self::ZERO
}
}
impl<'de> Deserialize<'de> for HwpUnit {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = i32::deserialize(deserializer)?;
HwpUnit::new(raw).map_err(|_| {
serde::de::Error::custom(format!(
"HwpUnit out of range: {raw} (must be in [{}, {}])",
HwpUnit::MIN_VALUE,
HwpUnit::MAX_VALUE
))
})
}
}
impl fmt::Debug for HwpUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "HwpUnit({})", self.0)
}
}
impl fmt::Display for HwpUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} hwp", self.0)
}
}
impl Add for HwpUnit {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self(self.0.saturating_add(rhs.0))
}
}
impl Sub for HwpUnit {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self(self.0.saturating_sub(rhs.0))
}
}
impl Neg for HwpUnit {
type Output = Self;
fn neg(self) -> Self {
Self(self.0.saturating_neg())
}
}
impl Mul<i32> for HwpUnit {
type Output = Self;
fn mul(self, rhs: i32) -> Self {
Self(self.0.saturating_mul(rhs))
}
}
impl Div<i32> for HwpUnit {
type Output = Self;
fn div(self, rhs: i32) -> Self {
if rhs == 0 {
return Self::ZERO;
}
Self(self.0.saturating_div(rhs))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct Size {
pub width: HwpUnit,
pub height: HwpUnit,
}
const _: () = assert!(std::mem::size_of::<Size>() == 8);
impl Size {
pub const A4: Self = Self {
width: HwpUnit::new_unchecked(59528), height: HwpUnit::new_unchecked(84188), };
pub const LETTER: Self = Self {
width: HwpUnit::new_unchecked(61200), height: HwpUnit::new_unchecked(79200), };
pub const B5: Self = Self {
width: HwpUnit::new_unchecked(51591), height: HwpUnit::new_unchecked(72850), };
pub const fn new(width: HwpUnit, height: HwpUnit) -> Self {
Self { width, height }
}
}
impl fmt::Display for Size {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} x {}", self.width, self.height)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct Point {
pub x: HwpUnit,
pub y: HwpUnit,
}
const _: () = assert!(std::mem::size_of::<Point>() == 8);
impl Point {
pub const ORIGIN: Self = Self { x: HwpUnit::ZERO, y: HwpUnit::ZERO };
pub const fn new(x: HwpUnit, y: HwpUnit) -> Self {
Self { x, y }
}
}
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct Rect {
pub origin: Point,
pub size: Size,
}
const _: () = assert!(std::mem::size_of::<Rect>() == 16);
impl Rect {
pub const fn new(origin: Point, size: Size) -> Self {
Self { origin, size }
}
}
impl fmt::Display for Rect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} @ {}", self.size, self.origin)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct Insets {
pub top: HwpUnit,
pub bottom: HwpUnit,
pub left: HwpUnit,
pub right: HwpUnit,
}
const _: () = assert!(std::mem::size_of::<Insets>() == 16);
impl Insets {
pub const fn uniform(value: HwpUnit) -> Self {
Self { top: value, bottom: value, left: value, right: value }
}
pub const fn symmetric(horizontal: HwpUnit, vertical: HwpUnit) -> Self {
Self { top: vertical, bottom: vertical, left: horizontal, right: horizontal }
}
pub const fn new(top: HwpUnit, bottom: HwpUnit, left: HwpUnit, right: HwpUnit) -> Self {
Self { top, bottom, left, right }
}
}
impl fmt::Display for Insets {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Insets(top={}, bottom={}, left={}, right={})",
self.top, self.bottom, self.left, self.right
)
}
}
impl schemars::JsonSchema for HwpUnit {
fn schema_name() -> std::borrow::Cow<'static, str> {
"HwpUnit".into()
}
fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
gen.subschema_for::<i32>()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hwpunit_zero() {
let u = HwpUnit::new(0).unwrap();
assert_eq!(u.as_i32(), 0);
assert_eq!(u, HwpUnit::ZERO);
}
#[test]
fn hwpunit_min_valid() {
let u = HwpUnit::new(HwpUnit::MIN_VALUE).unwrap();
assert_eq!(u.as_i32(), -100_000_000);
}
#[test]
fn hwpunit_max_valid() {
let u = HwpUnit::new(HwpUnit::MAX_VALUE).unwrap();
assert_eq!(u.as_i32(), 100_000_000);
}
#[test]
fn hwpunit_below_min_is_error() {
assert!(HwpUnit::new(HwpUnit::MIN_VALUE - 1).is_err());
assert!(HwpUnit::new(i32::MIN).is_err());
}
#[test]
fn hwpunit_above_max_is_error() {
assert!(HwpUnit::new(HwpUnit::MAX_VALUE + 1).is_err());
assert!(HwpUnit::new(i32::MAX).is_err());
}
#[test]
fn hwpunit_from_pt_infinity_is_error() {
assert!(HwpUnit::from_pt(f64::INFINITY).is_err());
assert!(HwpUnit::from_pt(f64::NEG_INFINITY).is_err());
}
#[test]
fn hwpunit_from_pt_nan_is_error() {
assert!(HwpUnit::from_pt(f64::NAN).is_err());
}
#[test]
fn hwpunit_from_pt_negative_zero() {
let u = HwpUnit::from_pt(-0.0).unwrap();
assert_eq!(u.as_i32(), 0);
}
#[test]
fn hwpunit_roundtrip_pt() {
let u = HwpUnit::from_pt(12.5).unwrap();
assert!((u.to_pt() - 12.5).abs() < 0.01);
}
#[test]
fn hwpunit_roundtrip_mm() {
let u = HwpUnit::from_mm(25.4).unwrap();
assert_eq!(u.as_i32(), 7200);
assert!((u.to_mm() - 25.4).abs() < 0.01);
}
#[test]
fn hwpunit_roundtrip_inch() {
let u = HwpUnit::from_inch(1.0).unwrap();
assert_eq!(u.as_i32(), 7200);
assert!((u.to_inch() - 1.0).abs() < f64::EPSILON);
}
#[test]
fn hwpunit_add() {
let a = HwpUnit::new(100).unwrap();
let b = HwpUnit::new(200).unwrap();
assert_eq!((a + b).as_i32(), 300);
}
#[test]
fn hwpunit_sub() {
let a = HwpUnit::new(300).unwrap();
let b = HwpUnit::new(100).unwrap();
assert_eq!((a - b).as_i32(), 200);
}
#[test]
fn hwpunit_neg() {
let a = HwpUnit::new(100).unwrap();
assert_eq!((-a).as_i32(), -100);
}
#[test]
fn hwpunit_mul_scalar() {
let a = HwpUnit::new(100).unwrap();
assert_eq!((a * 3).as_i32(), 300);
}
#[test]
fn hwpunit_div_scalar() {
let a = HwpUnit::new(300).unwrap();
assert_eq!((a / 3).as_i32(), 100);
}
#[test]
fn hwpunit_add_saturates_on_overflow() {
let a = HwpUnit::new_unchecked(i32::MAX);
let b = HwpUnit::new_unchecked(1);
assert_eq!((a + b).as_i32(), i32::MAX);
}
#[test]
fn hwpunit_display() {
let u = HwpUnit::new(7200).unwrap();
assert_eq!(u.to_string(), "7200 hwp");
}
#[test]
fn hwpunit_debug() {
let u = HwpUnit::new(100).unwrap();
assert_eq!(format!("{u:?}"), "HwpUnit(100)");
}
#[test]
fn hwpunit_default_is_zero() {
assert_eq!(HwpUnit::default(), HwpUnit::ZERO);
}
#[test]
fn hwpunit_ord() {
let a = HwpUnit::new(100).unwrap();
let b = HwpUnit::new(200).unwrap();
assert!(a < b);
}
#[test]
fn hwpunit_serde_roundtrip() {
let u = HwpUnit::new(1200).unwrap();
let json = serde_json::to_string(&u).unwrap();
assert_eq!(json, "1200");
let back: HwpUnit = serde_json::from_str(&json).unwrap();
assert_eq!(back, u);
}
#[test]
fn size_a4_dimensions() {
let a4 = Size::A4;
assert!((HwpUnit(a4.width.as_i32()).to_mm() - 210.0).abs() < 0.1);
assert!((HwpUnit(a4.height.as_i32()).to_mm() - 297.0).abs() < 0.1);
}
#[test]
fn size_letter_dimensions() {
let letter = Size::LETTER;
assert_eq!(letter.width.as_i32(), 61200);
assert_eq!(letter.height.as_i32(), 79200);
}
#[test]
fn size_display() {
let s = Size::new(HwpUnit::new_unchecked(100), HwpUnit::new_unchecked(200));
assert_eq!(s.to_string(), "100 hwp x 200 hwp");
}
#[test]
fn point_origin() {
let o = Point::ORIGIN;
assert_eq!(o.x, HwpUnit::ZERO);
assert_eq!(o.y, HwpUnit::ZERO);
}
#[test]
fn point_display() {
let p = Point::new(HwpUnit::new_unchecked(10), HwpUnit::new_unchecked(20));
assert_eq!(p.to_string(), "(10 hwp, 20 hwp)");
}
#[test]
fn rect_construction() {
let r = Rect::new(Point::ORIGIN, Size::A4);
assert_eq!(r.origin, Point::ORIGIN);
assert_eq!(r.size, Size::A4);
}
#[test]
fn rect_display() {
let r = Rect::new(
Point::new(HwpUnit::new_unchecked(1), HwpUnit::new_unchecked(2)),
Size::new(HwpUnit::new_unchecked(3), HwpUnit::new_unchecked(4)),
);
assert_eq!(r.to_string(), "3 hwp x 4 hwp @ (1 hwp, 2 hwp)");
}
#[test]
fn insets_uniform() {
let ins = Insets::uniform(HwpUnit::ONE_PT);
assert_eq!(ins.top, HwpUnit::ONE_PT);
assert_eq!(ins.bottom, HwpUnit::ONE_PT);
assert_eq!(ins.left, HwpUnit::ONE_PT);
assert_eq!(ins.right, HwpUnit::ONE_PT);
}
#[test]
fn insets_symmetric() {
let h = HwpUnit::new(10).unwrap();
let v = HwpUnit::new(20).unwrap();
let ins = Insets::symmetric(h, v);
assert_eq!(ins.left, h);
assert_eq!(ins.right, h);
assert_eq!(ins.top, v);
assert_eq!(ins.bottom, v);
}
#[test]
fn geometry_serde_roundtrip() {
let size = Size::A4;
let json = serde_json::to_string(&size).unwrap();
let back: Size = serde_json::from_str(&json).unwrap();
assert_eq!(back, size);
let rect = Rect::new(Point::ORIGIN, Size::A4);
let json = serde_json::to_string(&rect).unwrap();
let back: Rect = serde_json::from_str(&json).unwrap();
assert_eq!(back, rect);
}
#[test]
fn geometry_default_is_zero() {
assert_eq!(Size::default(), Size::new(HwpUnit::ZERO, HwpUnit::ZERO));
assert_eq!(Point::default(), Point::ORIGIN);
}
use proptest::prelude::*;
proptest! {
#[test]
fn prop_hwpunit_pt_roundtrip(pt in -1_000_000.0f64..1_000_000.0f64) {
if let Ok(u) = HwpUnit::from_pt(pt) {
let back = u.to_pt();
prop_assert!((back - pt).abs() < 0.01,
"pt={pt}, back={back}, diff={}", (back - pt).abs());
}
}
#[test]
fn prop_hwpunit_mm_roundtrip(mm in -350.0f64..350.0f64) {
if let Ok(u) = HwpUnit::from_mm(mm) {
let back = u.to_mm();
prop_assert!((back - mm).abs() < 0.01,
"mm={mm}, back={back}, diff={}", (back - mm).abs());
}
}
#[test]
fn prop_hwpunit_inch_roundtrip(inch in -14.0f64..14.0f64) {
if let Ok(u) = HwpUnit::from_inch(inch) {
let back = u.to_inch();
prop_assert!((back - inch).abs() < 0.001,
"inch={inch}, back={back}");
}
}
}
#[test]
fn hwpunit_div_by_zero_returns_zero() {
let u = HwpUnit::new(300).unwrap();
assert_eq!((u / 0).as_i32(), 0);
}
#[test]
fn hwpunit_div_min_by_neg_one_saturates() {
let u = HwpUnit::new_unchecked(i32::MIN);
let result = u / -1;
assert_eq!(result.as_i32(), i32::MAX);
}
#[test]
fn hwpunit_div_normal_works() {
let u = HwpUnit::new(600).unwrap();
assert_eq!((u / 2).as_i32(), 300);
}
#[test]
fn hwpunit_deser_valid_roundtrip() {
let u = HwpUnit::new(42000).unwrap();
let json = serde_json::to_string(&u).unwrap();
let back: HwpUnit = serde_json::from_str(&json).unwrap();
assert_eq!(back, u);
}
#[test]
fn hwpunit_deser_out_of_range_is_error() {
let err = serde_json::from_str::<HwpUnit>("200000000");
assert!(err.is_err(), "expected error for out-of-range value");
}
#[test]
fn hwpunit_deser_i32_min_is_error() {
let json = format!("{}", i32::MIN);
let err = serde_json::from_str::<HwpUnit>(&json);
assert!(err.is_err(), "i32::MIN should be rejected");
}
#[test]
fn hwpunit_deser_max_valid_boundary() {
let json = format!("{}", HwpUnit::MAX_VALUE);
let u: HwpUnit = serde_json::from_str(&json).unwrap();
assert_eq!(u.as_i32(), HwpUnit::MAX_VALUE);
}
#[test]
fn hwpunit_deser_min_valid_boundary() {
let json = format!("{}", HwpUnit::MIN_VALUE);
let u: HwpUnit = serde_json::from_str(&json).unwrap();
assert_eq!(u.as_i32(), HwpUnit::MIN_VALUE);
}
}