hwpforge_foundation/
units.rs

1//! HWP measurement unit system.
2//!
3//! HWP documents measure everything in **HwpUnit**, where 1 point = 100 HwpUnit.
4//! This module provides [`HwpUnit`] and geometry composites built from it:
5//! [`Size`], [`Point`], [`Rect`], and [`Insets`].
6//!
7//! # Conversion Constants
8//!
9//! | Unit | HwpUnit equivalent |
10//! |------|--------------------|
11//! | 1 pt | 100 |
12//! | 1 inch | 7200 |
13//! | 1 mm | 283.465... (f64 math) |
14//!
15//! # Examples
16//!
17//! ```
18//! use hwpforge_foundation::HwpUnit;
19//!
20//! let twelve_pt = HwpUnit::from_pt(12.0).unwrap();
21//! assert_eq!(twelve_pt.as_i32(), 1200);
22//! assert!((twelve_pt.to_pt() - 12.0).abs() < f64::EPSILON);
23//! ```
24
25use std::fmt;
26use std::ops::{Add, Div, Mul, Neg, Sub};
27
28use serde::{Deserialize, Deserializer, Serialize};
29
30use crate::error::{FoundationError, FoundationResult};
31
32// ---------------------------------------------------------------------------
33// HwpUnit
34// ---------------------------------------------------------------------------
35
36/// The universal measurement unit used throughout HWP documents.
37///
38/// Internally an `i32` where **1 point = 100 HwpUnit**.
39/// `repr(transparent)` guarantees zero overhead over a bare `i32`.
40///
41/// FROZEN: Do not change the internal representation after v1.0.
42///
43/// # Valid Range
44///
45/// `[-100_000_000, 100_000_000]` -- comfortably covers A0 paper
46/// (841 mm width ~ 2_384_252 HwpUnit).
47///
48/// # Examples
49///
50/// ```
51/// use hwpforge_foundation::HwpUnit;
52///
53/// let one_inch = HwpUnit::from_inch(1.0).unwrap();
54/// assert_eq!(one_inch.as_i32(), 7200);
55/// ```
56#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
57#[repr(transparent)]
58pub struct HwpUnit(i32);
59
60// Compile-time size guarantee
61const _: () = assert!(std::mem::size_of::<HwpUnit>() == 4);
62
63/// Conversion: 1 point = 100 HwpUnit.
64const HWPUNIT_PER_PT: f64 = 100.0;
65
66/// Conversion: 1 inch = 72 pt = 7200 HwpUnit.
67const HWPUNIT_PER_INCH: f64 = 7200.0;
68
69/// Conversion: 1 mm = 72/25.4 pt = 283.4645... HwpUnit.
70const HWPUNIT_PER_MM: f64 = 7200.0 / 25.4;
71
72impl HwpUnit {
73    /// Minimum valid value (inclusive).
74    pub const MIN_VALUE: i32 = -100_000_000;
75    /// Maximum valid value (inclusive).
76    pub const MAX_VALUE: i32 = 100_000_000;
77
78    /// Zero HwpUnit.
79    pub const ZERO: Self = Self(0);
80    /// One typographic point (100 HwpUnit).
81    pub const ONE_PT: Self = Self(100);
82
83    /// Creates an `HwpUnit` from a raw `i32`, validating the range.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`FoundationError::InvalidHwpUnit`] when `value` lies
88    /// outside `[MIN_VALUE, MAX_VALUE]`.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use hwpforge_foundation::HwpUnit;
94    ///
95    /// assert!(HwpUnit::new(0).is_ok());
96    /// assert!(HwpUnit::new(200_000_000).is_err());
97    /// ```
98    pub fn new(value: i32) -> FoundationResult<Self> {
99        if !(Self::MIN_VALUE..=Self::MAX_VALUE).contains(&value) {
100            return Err(FoundationError::InvalidHwpUnit {
101                value: value as i64, // i64 for error reporting (no truncation)
102                min: Self::MIN_VALUE,
103                max: Self::MAX_VALUE,
104            });
105        }
106        Ok(Self(value))
107    }
108
109    /// Creates an `HwpUnit` from a raw `i32` without validation.
110    ///
111    /// Intended for internal use where the value is already known-valid
112    /// (e.g. constants, deserialized-then-checked data).
113    pub(crate) const fn new_unchecked(value: i32) -> Self {
114        Self(value)
115    }
116
117    /// Returns the raw `i32` value.
118    pub const fn as_i32(self) -> i32 {
119        self.0
120    }
121
122    /// Returns `true` if this unit is zero.
123    ///
124    /// Useful as a `skip_serializing_if` predicate for serde.
125    pub const fn is_zero(&self) -> bool {
126        self.0 == 0
127    }
128
129    /// Constructs an `HwpUnit` from typographic points.
130    ///
131    /// # Errors
132    ///
133    /// Returns an error when `pt` is non-finite or the converted
134    /// value exceeds the valid range.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use hwpforge_foundation::HwpUnit;
140    ///
141    /// let u = HwpUnit::from_pt(12.0).unwrap();
142    /// assert_eq!(u.as_i32(), 1200);
143    /// ```
144    pub fn from_pt(pt: f64) -> FoundationResult<Self> {
145        Self::from_f64(pt, HWPUNIT_PER_PT, "pt")
146    }
147
148    /// Constructs an `HwpUnit` from millimeters.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error when `mm` is non-finite or the converted
153    /// value exceeds the valid range.
154    pub fn from_mm(mm: f64) -> FoundationResult<Self> {
155        Self::from_f64(mm, HWPUNIT_PER_MM, "mm")
156    }
157
158    /// Constructs an `HwpUnit` from inches.
159    ///
160    /// # Errors
161    ///
162    /// Returns an error when `inch` is non-finite or the converted
163    /// value exceeds the valid range.
164    pub fn from_inch(inch: f64) -> FoundationResult<Self> {
165        Self::from_f64(inch, HWPUNIT_PER_INCH, "inch")
166    }
167
168    /// Converts to typographic points (f64).
169    pub fn to_pt(self) -> f64 {
170        self.0 as f64 / HWPUNIT_PER_PT
171    }
172
173    /// Converts to millimeters (f64).
174    pub fn to_mm(self) -> f64 {
175        self.0 as f64 / HWPUNIT_PER_MM
176    }
177
178    /// Converts to inches (f64).
179    pub fn to_inch(self) -> f64 {
180        self.0 as f64 / HWPUNIT_PER_INCH
181    }
182
183    // Internal: shared f64 -> HwpUnit conversion with validation.
184    fn from_f64(value: f64, scale: f64, unit_name: &str) -> FoundationResult<Self> {
185        if !value.is_finite() {
186            return Err(FoundationError::InvalidField {
187                field: unit_name.to_string(),
188                reason: format!("{value} is not finite"),
189            });
190        }
191        let raw = (value * scale).round() as i64;
192        if raw < Self::MIN_VALUE as i64 || raw > Self::MAX_VALUE as i64 {
193            return Err(FoundationError::InvalidHwpUnit {
194                value: raw, // i64 그대로 (truncation 방지)
195                min: Self::MIN_VALUE,
196                max: Self::MAX_VALUE,
197            });
198        }
199        Ok(Self(raw as i32))
200    }
201}
202
203impl Default for HwpUnit {
204    fn default() -> Self {
205        Self::ZERO
206    }
207}
208
209impl<'de> Deserialize<'de> for HwpUnit {
210    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
211        let raw = i32::deserialize(deserializer)?;
212        HwpUnit::new(raw).map_err(|_| {
213            serde::de::Error::custom(format!(
214                "HwpUnit out of range: {raw} (must be in [{}, {}])",
215                HwpUnit::MIN_VALUE,
216                HwpUnit::MAX_VALUE
217            ))
218        })
219    }
220}
221
222impl fmt::Debug for HwpUnit {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        write!(f, "HwpUnit({})", self.0)
225    }
226}
227
228impl fmt::Display for HwpUnit {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        write!(f, "{} hwp", self.0)
231    }
232}
233
234impl Add for HwpUnit {
235    type Output = Self;
236    fn add(self, rhs: Self) -> Self {
237        Self(self.0.saturating_add(rhs.0))
238    }
239}
240
241impl Sub for HwpUnit {
242    type Output = Self;
243    fn sub(self, rhs: Self) -> Self {
244        Self(self.0.saturating_sub(rhs.0))
245    }
246}
247
248impl Neg for HwpUnit {
249    type Output = Self;
250    fn neg(self) -> Self {
251        Self(self.0.saturating_neg())
252    }
253}
254
255impl Mul<i32> for HwpUnit {
256    type Output = Self;
257    fn mul(self, rhs: i32) -> Self {
258        Self(self.0.saturating_mul(rhs))
259    }
260}
261
262impl Div<i32> for HwpUnit {
263    type Output = Self;
264    fn div(self, rhs: i32) -> Self {
265        if rhs == 0 {
266            return Self::ZERO;
267        }
268        Self(self.0.saturating_div(rhs))
269    }
270}
271
272// ---------------------------------------------------------------------------
273// Geometry composites
274// ---------------------------------------------------------------------------
275
276/// A 2-dimensional size (width x height) in [`HwpUnit`].
277///
278/// # Examples
279///
280/// ```
281/// use hwpforge_foundation::{HwpUnit, Size};
282///
283/// let a4 = Size::A4;
284/// assert!(a4.width.as_i32() > 0);
285/// assert!(a4.height.as_i32() > 0);
286/// ```
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
288pub struct Size {
289    /// Horizontal extent.
290    pub width: HwpUnit,
291    /// Vertical extent.
292    pub height: HwpUnit,
293}
294
295const _: () = assert!(std::mem::size_of::<Size>() == 8);
296
297impl Size {
298    /// A4 paper: 210 mm x 297 mm.
299    pub const A4: Self = Self {
300        width: HwpUnit::new_unchecked(59528),  // round(210 * 7200/25.4)
301        height: HwpUnit::new_unchecked(84188), // round(297 * 7200/25.4)
302    };
303
304    /// US Letter: 8.5 in x 11 in.
305    pub const LETTER: Self = Self {
306        width: HwpUnit::new_unchecked(61200),  // 8.5 * 7200
307        height: HwpUnit::new_unchecked(79200), // 11 * 7200
308    };
309
310    /// B5 (JIS): 182 mm x 257 mm.
311    pub const B5: Self = Self {
312        width: HwpUnit::new_unchecked(51591),  // round(182 * 7200/25.4)
313        height: HwpUnit::new_unchecked(72850), // round(257 * 7200/25.4)
314    };
315
316    /// Constructs a new [`Size`].
317    pub const fn new(width: HwpUnit, height: HwpUnit) -> Self {
318        Self { width, height }
319    }
320}
321
322impl fmt::Display for Size {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        write!(f, "{} x {}", self.width, self.height)
325    }
326}
327
328/// A 2-dimensional point (x, y) in [`HwpUnit`].
329///
330/// # Examples
331///
332/// ```
333/// use hwpforge_foundation::{HwpUnit, Point};
334///
335/// let origin = Point::ORIGIN;
336/// assert_eq!(origin.x, HwpUnit::ZERO);
337/// ```
338#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
339pub struct Point {
340    /// Horizontal coordinate.
341    pub x: HwpUnit,
342    /// Vertical coordinate.
343    pub y: HwpUnit,
344}
345
346const _: () = assert!(std::mem::size_of::<Point>() == 8);
347
348impl Point {
349    /// The origin (0, 0).
350    pub const ORIGIN: Self = Self { x: HwpUnit::ZERO, y: HwpUnit::ZERO };
351
352    /// Constructs a new [`Point`].
353    pub const fn new(x: HwpUnit, y: HwpUnit) -> Self {
354        Self { x, y }
355    }
356}
357
358impl fmt::Display for Point {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        write!(f, "({}, {})", self.x, self.y)
361    }
362}
363
364/// A rectangle defined by an origin [`Point`] and a [`Size`].
365///
366/// # Examples
367///
368/// ```
369/// use hwpforge_foundation::{Point, Size, Rect};
370///
371/// let r = Rect::new(Point::ORIGIN, Size::A4);
372/// assert_eq!(r.origin, Point::ORIGIN);
373/// ```
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
375pub struct Rect {
376    /// Top-left corner.
377    pub origin: Point,
378    /// Width and height.
379    pub size: Size,
380}
381
382const _: () = assert!(std::mem::size_of::<Rect>() == 16);
383
384impl Rect {
385    /// Constructs a new [`Rect`].
386    pub const fn new(origin: Point, size: Size) -> Self {
387        Self { origin, size }
388    }
389}
390
391impl fmt::Display for Rect {
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393        write!(f, "{} @ {}", self.size, self.origin)
394    }
395}
396
397/// Edge insets (margins/padding) in [`HwpUnit`].
398///
399/// # Examples
400///
401/// ```
402/// use hwpforge_foundation::{HwpUnit, Insets};
403///
404/// let uniform = Insets::uniform(HwpUnit::ONE_PT);
405/// assert_eq!(uniform.top, uniform.bottom);
406/// ```
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
408pub struct Insets {
409    /// Top inset.
410    pub top: HwpUnit,
411    /// Bottom inset.
412    pub bottom: HwpUnit,
413    /// Left inset.
414    pub left: HwpUnit,
415    /// Right inset.
416    pub right: HwpUnit,
417}
418
419const _: () = assert!(std::mem::size_of::<Insets>() == 16);
420
421impl Insets {
422    /// Creates insets with the same value on all four sides.
423    pub const fn uniform(value: HwpUnit) -> Self {
424        Self { top: value, bottom: value, left: value, right: value }
425    }
426
427    /// Creates insets with separate horizontal and vertical values.
428    pub const fn symmetric(horizontal: HwpUnit, vertical: HwpUnit) -> Self {
429        Self { top: vertical, bottom: vertical, left: horizontal, right: horizontal }
430    }
431
432    /// Creates insets with individual side values.
433    pub const fn new(top: HwpUnit, bottom: HwpUnit, left: HwpUnit, right: HwpUnit) -> Self {
434        Self { top, bottom, left, right }
435    }
436}
437
438impl fmt::Display for Insets {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        write!(
441            f,
442            "Insets(top={}, bottom={}, left={}, right={})",
443            self.top, self.bottom, self.left, self.right
444        )
445    }
446}
447
448// ---------------------------------------------------------------------------
449// schemars impls (manual for transparent newtypes)
450// ---------------------------------------------------------------------------
451
452impl schemars::JsonSchema for HwpUnit {
453    fn schema_name() -> std::borrow::Cow<'static, str> {
454        "HwpUnit".into()
455    }
456
457    fn json_schema(gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
458        gen.subschema_for::<i32>()
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    // ===================================================================
467    // HwpUnit edge cases (10+)
468    // ===================================================================
469
470    // Edge Case 1: Zero
471    #[test]
472    fn hwpunit_zero() {
473        let u = HwpUnit::new(0).unwrap();
474        assert_eq!(u.as_i32(), 0);
475        assert_eq!(u, HwpUnit::ZERO);
476    }
477
478    // Edge Case 2: Minimum valid boundary
479    #[test]
480    fn hwpunit_min_valid() {
481        let u = HwpUnit::new(HwpUnit::MIN_VALUE).unwrap();
482        assert_eq!(u.as_i32(), -100_000_000);
483    }
484
485    // Edge Case 3: Maximum valid boundary
486    #[test]
487    fn hwpunit_max_valid() {
488        let u = HwpUnit::new(HwpUnit::MAX_VALUE).unwrap();
489        assert_eq!(u.as_i32(), 100_000_000);
490    }
491
492    // Edge Case 4: Below minimum -> error
493    #[test]
494    fn hwpunit_below_min_is_error() {
495        assert!(HwpUnit::new(HwpUnit::MIN_VALUE - 1).is_err());
496        assert!(HwpUnit::new(i32::MIN).is_err());
497    }
498
499    // Edge Case 5: Above maximum -> error
500    #[test]
501    fn hwpunit_above_max_is_error() {
502        assert!(HwpUnit::new(HwpUnit::MAX_VALUE + 1).is_err());
503        assert!(HwpUnit::new(i32::MAX).is_err());
504    }
505
506    // Edge Case 6: Infinity -> error
507    #[test]
508    fn hwpunit_from_pt_infinity_is_error() {
509        assert!(HwpUnit::from_pt(f64::INFINITY).is_err());
510        assert!(HwpUnit::from_pt(f64::NEG_INFINITY).is_err());
511    }
512
513    // Edge Case 7: NaN -> error
514    #[test]
515    fn hwpunit_from_pt_nan_is_error() {
516        assert!(HwpUnit::from_pt(f64::NAN).is_err());
517    }
518
519    // Edge Case 8: Negative zero -> produces 0
520    #[test]
521    fn hwpunit_from_pt_negative_zero() {
522        let u = HwpUnit::from_pt(-0.0).unwrap();
523        assert_eq!(u.as_i32(), 0);
524    }
525
526    // Edge Case 9: Roundtrip pt
527    #[test]
528    fn hwpunit_roundtrip_pt() {
529        let u = HwpUnit::from_pt(12.5).unwrap();
530        assert!((u.to_pt() - 12.5).abs() < 0.01);
531    }
532
533    // Edge Case 10: Roundtrip mm
534    #[test]
535    fn hwpunit_roundtrip_mm() {
536        let u = HwpUnit::from_mm(25.4).unwrap();
537        // 25.4 mm = 1 inch = 72 pt = 7200 hwp
538        assert_eq!(u.as_i32(), 7200);
539        assert!((u.to_mm() - 25.4).abs() < 0.01);
540    }
541
542    // Edge Case 11: Roundtrip inch
543    #[test]
544    fn hwpunit_roundtrip_inch() {
545        let u = HwpUnit::from_inch(1.0).unwrap();
546        assert_eq!(u.as_i32(), 7200);
547        assert!((u.to_inch() - 1.0).abs() < f64::EPSILON);
548    }
549
550    // Arithmetic tests
551
552    #[test]
553    fn hwpunit_add() {
554        let a = HwpUnit::new(100).unwrap();
555        let b = HwpUnit::new(200).unwrap();
556        assert_eq!((a + b).as_i32(), 300);
557    }
558
559    #[test]
560    fn hwpunit_sub() {
561        let a = HwpUnit::new(300).unwrap();
562        let b = HwpUnit::new(100).unwrap();
563        assert_eq!((a - b).as_i32(), 200);
564    }
565
566    #[test]
567    fn hwpunit_neg() {
568        let a = HwpUnit::new(100).unwrap();
569        assert_eq!((-a).as_i32(), -100);
570    }
571
572    #[test]
573    fn hwpunit_mul_scalar() {
574        let a = HwpUnit::new(100).unwrap();
575        assert_eq!((a * 3).as_i32(), 300);
576    }
577
578    #[test]
579    fn hwpunit_div_scalar() {
580        let a = HwpUnit::new(300).unwrap();
581        assert_eq!((a / 3).as_i32(), 100);
582    }
583
584    #[test]
585    fn hwpunit_add_saturates_on_overflow() {
586        let a = HwpUnit::new_unchecked(i32::MAX);
587        let b = HwpUnit::new_unchecked(1);
588        assert_eq!((a + b).as_i32(), i32::MAX);
589    }
590
591    #[test]
592    fn hwpunit_display() {
593        let u = HwpUnit::new(7200).unwrap();
594        assert_eq!(u.to_string(), "7200 hwp");
595    }
596
597    #[test]
598    fn hwpunit_debug() {
599        let u = HwpUnit::new(100).unwrap();
600        assert_eq!(format!("{u:?}"), "HwpUnit(100)");
601    }
602
603    #[test]
604    fn hwpunit_default_is_zero() {
605        assert_eq!(HwpUnit::default(), HwpUnit::ZERO);
606    }
607
608    #[test]
609    fn hwpunit_ord() {
610        let a = HwpUnit::new(100).unwrap();
611        let b = HwpUnit::new(200).unwrap();
612        assert!(a < b);
613    }
614
615    #[test]
616    fn hwpunit_serde_roundtrip() {
617        let u = HwpUnit::new(1200).unwrap();
618        let json = serde_json::to_string(&u).unwrap();
619        assert_eq!(json, "1200");
620        let back: HwpUnit = serde_json::from_str(&json).unwrap();
621        assert_eq!(back, u);
622    }
623
624    // ===================================================================
625    // Geometry types tests (10+)
626    // ===================================================================
627
628    #[test]
629    fn size_a4_dimensions() {
630        // A4 = 210mm x 297mm
631        let a4 = Size::A4;
632        assert!((HwpUnit(a4.width.as_i32()).to_mm() - 210.0).abs() < 0.1);
633        assert!((HwpUnit(a4.height.as_i32()).to_mm() - 297.0).abs() < 0.1);
634    }
635
636    #[test]
637    fn size_letter_dimensions() {
638        let letter = Size::LETTER;
639        assert_eq!(letter.width.as_i32(), 61200);
640        assert_eq!(letter.height.as_i32(), 79200);
641    }
642
643    #[test]
644    fn size_display() {
645        let s = Size::new(HwpUnit::new_unchecked(100), HwpUnit::new_unchecked(200));
646        assert_eq!(s.to_string(), "100 hwp x 200 hwp");
647    }
648
649    #[test]
650    fn point_origin() {
651        let o = Point::ORIGIN;
652        assert_eq!(o.x, HwpUnit::ZERO);
653        assert_eq!(o.y, HwpUnit::ZERO);
654    }
655
656    #[test]
657    fn point_display() {
658        let p = Point::new(HwpUnit::new_unchecked(10), HwpUnit::new_unchecked(20));
659        assert_eq!(p.to_string(), "(10 hwp, 20 hwp)");
660    }
661
662    #[test]
663    fn rect_construction() {
664        let r = Rect::new(Point::ORIGIN, Size::A4);
665        assert_eq!(r.origin, Point::ORIGIN);
666        assert_eq!(r.size, Size::A4);
667    }
668
669    #[test]
670    fn rect_display() {
671        let r = Rect::new(
672            Point::new(HwpUnit::new_unchecked(1), HwpUnit::new_unchecked(2)),
673            Size::new(HwpUnit::new_unchecked(3), HwpUnit::new_unchecked(4)),
674        );
675        assert_eq!(r.to_string(), "3 hwp x 4 hwp @ (1 hwp, 2 hwp)");
676    }
677
678    #[test]
679    fn insets_uniform() {
680        let ins = Insets::uniform(HwpUnit::ONE_PT);
681        assert_eq!(ins.top, HwpUnit::ONE_PT);
682        assert_eq!(ins.bottom, HwpUnit::ONE_PT);
683        assert_eq!(ins.left, HwpUnit::ONE_PT);
684        assert_eq!(ins.right, HwpUnit::ONE_PT);
685    }
686
687    #[test]
688    fn insets_symmetric() {
689        let h = HwpUnit::new(10).unwrap();
690        let v = HwpUnit::new(20).unwrap();
691        let ins = Insets::symmetric(h, v);
692        assert_eq!(ins.left, h);
693        assert_eq!(ins.right, h);
694        assert_eq!(ins.top, v);
695        assert_eq!(ins.bottom, v);
696    }
697
698    #[test]
699    fn geometry_serde_roundtrip() {
700        let size = Size::A4;
701        let json = serde_json::to_string(&size).unwrap();
702        let back: Size = serde_json::from_str(&json).unwrap();
703        assert_eq!(back, size);
704
705        let rect = Rect::new(Point::ORIGIN, Size::A4);
706        let json = serde_json::to_string(&rect).unwrap();
707        let back: Rect = serde_json::from_str(&json).unwrap();
708        assert_eq!(back, rect);
709    }
710
711    #[test]
712    fn geometry_default_is_zero() {
713        assert_eq!(Size::default(), Size::new(HwpUnit::ZERO, HwpUnit::ZERO));
714        assert_eq!(Point::default(), Point::ORIGIN);
715    }
716
717    // ===================================================================
718    // proptest
719    // ===================================================================
720
721    use proptest::prelude::*;
722
723    proptest! {
724        #[test]
725        fn prop_hwpunit_pt_roundtrip(pt in -1_000_000.0f64..1_000_000.0f64) {
726            if let Ok(u) = HwpUnit::from_pt(pt) {
727                let back = u.to_pt();
728                prop_assert!((back - pt).abs() < 0.01,
729                    "pt={pt}, back={back}, diff={}", (back - pt).abs());
730            }
731        }
732
733        #[test]
734        fn prop_hwpunit_mm_roundtrip(mm in -350.0f64..350.0f64) {
735            if let Ok(u) = HwpUnit::from_mm(mm) {
736                let back = u.to_mm();
737                prop_assert!((back - mm).abs() < 0.01,
738                    "mm={mm}, back={back}, diff={}", (back - mm).abs());
739            }
740        }
741
742        #[test]
743        fn prop_hwpunit_inch_roundtrip(inch in -14.0f64..14.0f64) {
744            if let Ok(u) = HwpUnit::from_inch(inch) {
745                let back = u.to_inch();
746                prop_assert!((back - inch).abs() < 0.001,
747                    "inch={inch}, back={back}");
748            }
749        }
750    }
751
752    // ===================================================================
753    // C1: Div<i32> panic fixes
754    // ===================================================================
755
756    #[test]
757    fn hwpunit_div_by_zero_returns_zero() {
758        let u = HwpUnit::new(300).unwrap();
759        assert_eq!((u / 0).as_i32(), 0);
760    }
761
762    #[test]
763    fn hwpunit_div_min_by_neg_one_saturates() {
764        // i32::MIN / -1 would overflow; saturating_div clamps to i32::MAX
765        let u = HwpUnit::new_unchecked(i32::MIN);
766        let result = u / -1;
767        assert_eq!(result.as_i32(), i32::MAX);
768    }
769
770    #[test]
771    fn hwpunit_div_normal_works() {
772        let u = HwpUnit::new(600).unwrap();
773        assert_eq!((u / 2).as_i32(), 300);
774    }
775
776    // ===================================================================
777    // C2: custom Deserialize range validation
778    // ===================================================================
779
780    #[test]
781    fn hwpunit_deser_valid_roundtrip() {
782        let u = HwpUnit::new(42000).unwrap();
783        let json = serde_json::to_string(&u).unwrap();
784        let back: HwpUnit = serde_json::from_str(&json).unwrap();
785        assert_eq!(back, u);
786    }
787
788    #[test]
789    fn hwpunit_deser_out_of_range_is_error() {
790        // 200_000_000 exceeds MAX_VALUE (100_000_000)
791        let err = serde_json::from_str::<HwpUnit>("200000000");
792        assert!(err.is_err(), "expected error for out-of-range value");
793    }
794
795    #[test]
796    fn hwpunit_deser_i32_min_is_error() {
797        let json = format!("{}", i32::MIN);
798        let err = serde_json::from_str::<HwpUnit>(&json);
799        assert!(err.is_err(), "i32::MIN should be rejected");
800    }
801
802    #[test]
803    fn hwpunit_deser_max_valid_boundary() {
804        let json = format!("{}", HwpUnit::MAX_VALUE);
805        let u: HwpUnit = serde_json::from_str(&json).unwrap();
806        assert_eq!(u.as_i32(), HwpUnit::MAX_VALUE);
807    }
808
809    #[test]
810    fn hwpunit_deser_min_valid_boundary() {
811        let json = format!("{}", HwpUnit::MIN_VALUE);
812        let u: HwpUnit = serde_json::from_str(&json).unwrap();
813        assert_eq!(u.as_i32(), HwpUnit::MIN_VALUE);
814    }
815}