typeshare/
integer.rs

1//! Integer types for use in the FFI layer.
2
3use serde::{Deserialize, Serialize};
4use std::cmp::Ordering;
5use std::convert::TryFrom;
6use std::fmt;
7use std::fmt::{Display, Formatter};
8
9const U53_MAX: u64 = 9_007_199_254_740_991;
10#[allow(clippy::as_conversions)]
11const I54_MAX: i64 = U53_MAX as i64;
12const I54_MIN: i64 = -9_007_199_254_740_991;
13
14/// Just like [`std::num::TryFromIntError`].
15///
16/// `std::num::TryFromIntError` cannot be constructed from outside of libstd so we have to provide
17/// our own equivalent.
18#[derive(Debug, PartialEq, Eq)]
19pub struct TryFromIntError(());
20
21// Error must implement Display for `#[serde(try_from = "FromType")]` https://serde.rs/container-attrs.html#try_from
22impl Display for TryFromIntError {
23    fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
24        write!(fmt, "Integer type conversion fail")
25    }
26}
27
28macro_rules! truncated_type {
29    ($truncated: ident, $untruncated: ident, $untruncated_str: expr, $min: expr, $max: expr, $doc: expr) => {
30        #[doc = $doc]
31        #[derive(
32            Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Hash,
33        )]
34        #[serde(try_from = $untruncated_str)]
35        pub struct $truncated($untruncated);
36
37        impl $truncated {
38            /// The smallest value that can be represented by this integer type.
39            pub const MIN: $truncated = $truncated($min);
40
41            /// The largest value that can be represented by this integer type.
42            pub const MAX: $truncated = $truncated($max);
43        }
44
45        impl fmt::Debug for $truncated {
46            fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
47                fmt::Debug::fmt(&self.0, fmt)
48            }
49        }
50
51        impl fmt::Display for $truncated {
52            fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
53                fmt::Display::fmt(&self.0, fmt)
54            }
55        }
56
57        impl TryFrom<$untruncated> for $truncated {
58            type Error = TryFromIntError;
59
60            #[allow(unused_comparisons)]
61            fn try_from(value: $untruncated) -> Result<Self, Self::Error> {
62                if !($min..=$max).contains(&value) {
63                    return Err(TryFromIntError(()));
64                }
65
66                Ok($truncated(value))
67            }
68        }
69
70        impl From<$truncated> for $untruncated {
71            fn from(value: $truncated) -> $untruncated {
72                value.0
73            }
74        }
75
76        impl PartialEq<$untruncated> for $truncated {
77            fn eq(&self, other: &$untruncated) -> bool {
78                self.0 == *other
79            }
80        }
81
82        impl PartialOrd<$untruncated> for $truncated {
83            fn partial_cmp(&self, other: &$untruncated) -> Option<Ordering> {
84                Some(self.0.cmp(other))
85            }
86        }
87    };
88}
89
90macro_rules! impl_truncated_type_from {
91    ($from: ident, $into: ident) => {
92        impl From<$into> for $from {
93            fn from(value: $into) -> $from {
94                $from(value.into())
95            }
96        }
97
98        impl TryFrom<$from> for $into {
99            type Error = TryFromIntError;
100
101            #[allow(unused_comparisons)]
102            fn try_from(value: $from) -> Result<$into, Self::Error> {
103                if value.0 < $into::MIN.into() || value.0 > $into::MAX.into() {
104                    return Err(TryFromIntError(()));
105                }
106
107                #[allow(clippy::as_conversions)]
108                Ok(value.0 as $into)
109            }
110        }
111    };
112}
113
114truncated_type!(
115    U53,
116    u64,
117    "u64",
118    0,
119    U53_MAX,
120    "The 53-bit unsigned integer type. Purpose of this type is to mimic JavaScript's integer type."
121);
122impl_truncated_type_from!(U53, u32);
123impl_truncated_type_from!(U53, u16);
124impl_truncated_type_from!(U53, u8);
125
126truncated_type!(
127    I54,
128    i64,
129    "i64",
130    I54_MIN,
131    I54_MAX,
132    "The 54-bit signed integer type. Purpose of this type is to mimic JavaScript's integer type."
133);
134impl_truncated_type_from!(I54, i32);
135impl_truncated_type_from!(I54, i16);
136impl_truncated_type_from!(I54, i8);
137
138/// Safely convert a `U53` integer to `usize`
139#[inline]
140#[must_use]
141pub fn usize_from_u53_saturated(value: U53) -> usize {
142    usize_from_u64_saturated(value.0)
143}
144
145/// Safely convert an unsigned 64-bit integer to `usize`
146#[allow(clippy::as_conversions)]
147#[inline]
148#[must_use]
149pub fn usize_from_u64_saturated(value: u64) -> usize {
150    std::cmp::min(value, usize::MAX as u64) as usize
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    // I54
158    #[test]
159    fn test_i54_init() {
160        assert_eq!(I54::try_from(I54_MAX).unwrap(), I54_MAX);
161    }
162
163    #[test]
164    fn test_i54_overflow() {
165        assert_eq!(I54::try_from(I54_MAX + 1), Err(TryFromIntError(())));
166    }
167
168    #[test]
169    fn test_i54_underflow() {
170        assert_eq!(I54::try_from(I54_MIN - 1), Err(TryFromIntError(())));
171    }
172
173    #[test]
174    fn test_i64_to_i54() {
175        assert_eq!(I54::try_from(i64::MAX), Err(TryFromIntError(())));
176    }
177
178    #[test]
179    fn test_i54_to_i64() {
180        assert_eq!(i64::from(I54::try_from(I54_MAX).unwrap()), I54_MAX);
181    }
182
183    #[test]
184    fn test_i54_to_i32() {
185        assert_eq!(i32::try_from(I54::from(i32::MAX)).unwrap(), i32::MAX);
186    }
187
188    #[test]
189    #[allow(clippy::as_conversions)]
190    fn test_i32_to_i54() {
191        assert_eq!(I54::from(i32::MAX), i64::from(i32::MAX));
192    }
193
194    // U53
195    #[test]
196    fn test_u53_init() {
197        assert_eq!(U53::try_from(U53_MAX).unwrap(), U53_MAX);
198    }
199
200    #[test]
201    fn test_u53_overflow() {
202        assert_eq!(U53::try_from(U53_MAX + 1), Err(TryFromIntError(())));
203    }
204
205    #[test]
206    fn test_u64_to_u53() {
207        assert_eq!(U53::try_from(u64::MAX), Err(TryFromIntError(())));
208    }
209
210    #[test]
211    fn test_u53_to_u64() {
212        assert_eq!(u64::from(U53::try_from(U53_MAX).unwrap()), U53_MAX);
213    }
214
215    #[test]
216    fn test_u53_to_u32() {
217        assert_eq!(u32::try_from(U53::from(u32::MAX)).unwrap(), u32::MAX);
218    }
219
220    #[test]
221    #[allow(clippy::as_conversions)]
222    fn test_u32_to_u53() {
223        assert_eq!(U53::from(u32::MAX), u64::from(u32::MAX));
224    }
225
226    #[test]
227    fn test_order() {
228        assert!(U53::from(u32::MAX) < u64::MAX);
229    }
230
231    #[test]
232    fn test_serde_serialize() {
233        #[derive(Serialize)]
234        struct Person {
235            age: I54,
236        }
237
238        let j = serde_json::to_string(&Person { age: I54::from(12) }).unwrap();
239        assert_eq!(j, r##"{"age":12}"##);
240    }
241
242    #[test]
243    fn test_serde_deserialize() {
244        #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
245        struct Person {
246            age: I54,
247        }
248
249        let j = r##"{"age":14}"##;
250        assert_eq!(
251            serde_json::from_str::<Person>(j).unwrap(),
252            Person { age: I54::from(14) }
253        );
254    }
255
256    #[test]
257    fn test_serde_deserialize_overflow() {
258        #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
259        struct Person {
260            age: I54,
261        }
262
263        let j = format!(r##"{{"age":{}}}"##, I54_MAX + 1);
264        assert!(serde_json::from_str::<Person>(j.as_str()).is_err());
265    }
266
267    #[test]
268    fn test_serde_deserialize_underflow() {
269        #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
270        struct Person {
271            age: I54,
272        }
273
274        let j = format!(r##"{{"age":{}}}"##, I54_MIN - 1);
275        assert!(serde_json::from_str::<Person>(j.as_str()).is_err());
276    }
277
278    #[test]
279    fn test_formatter_flags() {
280        let value: I54 = 125.into();
281
282        // Right-aligned, include the + sign, width=8,
283        assert_eq!(format!("{:>+8}", value), "    +125");
284    }
285
286    #[test]
287    fn i54_max() {
288        assert_eq!(I54_MAX, i64::from(I54::MAX));
289    }
290
291    #[test]
292    fn i54_min() {
293        assert_eq!(I54_MIN, i64::from(I54::MIN));
294    }
295
296    #[test]
297    fn u53_max() {
298        assert_eq!(U53_MAX, u64::from(U53::MAX));
299    }
300
301    #[test]
302    fn u53_min() {
303        assert_eq!(0, u64::from(U53::MIN));
304    }
305}