Skip to main content

pyra_margin/
math.rs

1/// Checked ceiling division for integer types.
2pub trait CheckedDivCeil {
3    fn checked_div_ceil(self, rhs: Self) -> Option<Self>
4    where
5        Self: Sized;
6}
7
8impl CheckedDivCeil for u128 {
9    fn checked_div_ceil(self, divisor: u128) -> Option<u128> {
10        if divisor == 0 {
11            return None;
12        }
13        let quotient = self.checked_div(divisor)?;
14        let product = quotient.checked_mul(divisor)?;
15        let remainder = self.checked_sub(product)?;
16        if remainder == 0 {
17            Some(quotient)
18        } else {
19            quotient.checked_add(1)
20        }
21    }
22}
23
24impl CheckedDivCeil for i128 {
25    fn checked_div_ceil(self, divisor: i128) -> Option<i128> {
26        if divisor == 0 {
27            return None;
28        }
29        let quotient = self.checked_div(divisor)?;
30        let quotient_times_divisor = quotient.checked_mul(divisor)?;
31        let remainder = self.checked_sub(quotient_times_divisor)?;
32        if remainder == 0 {
33            return Some(quotient);
34        }
35        // Round up only when both operands have the same sign
36        let same_sign = (self > 0 && divisor > 0) || (self < 0 && divisor < 0);
37        if same_sign {
38            quotient.checked_add(1)
39        } else {
40            Some(quotient)
41        }
42    }
43}
44
45impl CheckedDivCeil for i64 {
46    fn checked_div_ceil(self, divisor: i64) -> Option<i64> {
47        if divisor == 0 {
48            return None;
49        }
50        let quotient = self.checked_div(divisor)?;
51        let quotient_times_divisor = quotient.checked_mul(divisor)?;
52        let remainder = self.checked_sub(quotient_times_divisor)?;
53        if remainder == 0 {
54            return Some(quotient);
55        }
56        let same_sign = (self > 0 && divisor > 0) || (self < 0 && divisor < 0);
57        if same_sign {
58            quotient.checked_add(1)
59        } else {
60            Some(quotient)
61        }
62    }
63}
64
65#[cfg(test)]
66#[allow(
67    clippy::allow_attributes,
68    clippy::allow_attributes_without_reason,
69    clippy::unwrap_used,
70    clippy::expect_used,
71    clippy::panic,
72    clippy::arithmetic_side_effects,
73    reason = "test code"
74)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn u128_div_ceil_exact() {
80        assert_eq!(10u128.checked_div_ceil(5), Some(2));
81    }
82
83    #[test]
84    fn u128_div_ceil_rounds_up() {
85        assert_eq!(11u128.checked_div_ceil(5), Some(3));
86    }
87
88    #[test]
89    fn u128_div_ceil_zero_divisor() {
90        assert_eq!(10u128.checked_div_ceil(0), None);
91    }
92
93    #[test]
94    fn i128_div_ceil_positive() {
95        assert_eq!(11i128.checked_div_ceil(5), Some(3));
96    }
97
98    #[test]
99    fn i128_div_ceil_negative_no_roundup() {
100        // -11 / 5 = -2 remainder -1; different signs, no round up
101        assert_eq!((-11i128).checked_div_ceil(5), Some(-2));
102    }
103
104    #[test]
105    fn i128_div_ceil_both_negative_rounds_up() {
106        // -11 / -5 = 2 remainder -1; same signs, round up
107        assert_eq!((-11i128).checked_div_ceil(-5), Some(3));
108    }
109
110    #[test]
111    fn i64_div_ceil_exact() {
112        assert_eq!(10i64.checked_div_ceil(5), Some(2));
113    }
114
115    #[test]
116    fn i64_div_ceil_rounds_up() {
117        assert_eq!(11i64.checked_div_ceil(5), Some(3));
118    }
119}
120
121#[cfg(test)]
122#[allow(
123    clippy::allow_attributes,
124    clippy::allow_attributes_without_reason,
125    clippy::unwrap_used,
126    clippy::expect_used,
127    clippy::panic,
128    clippy::arithmetic_side_effects,
129    reason = "test code"
130)]
131mod proptests {
132    use super::*;
133    use proptest::prelude::*;
134
135    proptest! {
136        #[test]
137        fn u128_div_ceil_never_panics(a: u128, b: u128) {
138            let _ = a.checked_div_ceil(b);
139        }
140
141        #[test]
142        fn u128_div_ceil_correct(a in 0u128..=u128::MAX / 2, b in 1u128..=1_000_000_000) {
143            let result = a.checked_div_ceil(b).unwrap();
144            // result >= a/b (ceiling property)
145            let floor = a / b;
146            let remainder = a % b;
147            if remainder == 0 {
148                prop_assert_eq!(result, floor);
149            } else {
150                prop_assert_eq!(result, floor + 1);
151            }
152        }
153
154        #[test]
155        fn i128_div_ceil_never_panics(a: i64, b: i64) {
156            let _ = (a as i128).checked_div_ceil(b as i128);
157        }
158
159        #[test]
160        fn i128_div_ceil_rounds_toward_positive_infinity_for_same_sign(
161            a in 1i128..=1_000_000_000_000,
162            b in 1i128..=1_000_000_000,
163        ) {
164            let result = a.checked_div_ceil(b).unwrap();
165            // For positive/positive: result * b >= a
166            prop_assert!(result * b >= a);
167            // But (result - 1) * b < a (tightest ceiling)
168            prop_assert!((result - 1) * b < a);
169        }
170    }
171}