├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── RELEASES.md └── src └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "num-ord" 3 | version = "0.1.0" 4 | edition = "2018" 5 | authors = ["Orson Peters "] 6 | description = "Numerically ordered wrapper type for cross-type comparisons" 7 | license = "Zlib" 8 | repository = "https://github.com/orlp/num-ord" 9 | readme = "README.md" 10 | keywords = ["compare", "comparison", "number", "numeric", "type"] 11 | categories = ["algorithms", "no-std", "mathematics"] 12 | 13 | [dependencies] 14 | 15 | [dev-dependencies] 16 | rug = "1.18.0" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Orson Peters 2 | 3 | This software is provided 'as-is', without any express or implied warranty. In 4 | no event will the authors be held liable for any damages arising from the use of 5 | this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, including 8 | commercial applications, and to alter it and redistribute it freely, subject to 9 | the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not claim 12 | that you wrote the original software. If you use this software in a product, 13 | an acknowledgment in the product documentation would be appreciated but is 14 | not required. 15 | 16 | 2. Altered source versions must be plainly marked as such, and must not be 17 | misrepresented as being the original software. 18 | 19 | 3. This notice may not be removed or altered from any source distribution. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # num-ord 2 | 3 | This crate provides a numerically ordered wrapper type, `NumOrd`. This 4 | type implements the `PartialOrd` and `PartialEq` traits for all the 5 | possible combinations of built-in integer types, in a mathematically correct 6 | manner without overflows. Please refer to the 7 | [**the documentation**](https://docs.rs/num-ord) for more information. 8 | 9 | To start using `num-ord` add the following to your `Cargo.toml`: 10 | 11 | ```toml 12 | [dependencies] 13 | num-ord = "0.1" 14 | ``` 15 | 16 | # Example 17 | 18 | ```rust 19 | use num_ord::NumOrd; 20 | 21 | let x = 3_i64; 22 | let y = 3.5_f64; 23 | assert_eq!(x < (y as i64), false); // Incorrect. 24 | assert_eq!(NumOrd(x) < NumOrd(y), true); // Correct. 25 | 26 | let x = 9007199254740993_i64; 27 | let y = 9007199254740992_f64; 28 | assert_eq!(format!("{}", y), "9007199254740992"); // No rounded constant trickery! 29 | assert_eq!((x as f64) <= y, true); // Incorrect. 30 | assert_eq!(NumOrd(x) <= NumOrd(y), false); // Correct. 31 | ``` 32 | 33 | # License 34 | 35 | `num-ord` is released under the Zlib license, a permissive license. It is 36 | OSI and FSF approved and GPL compatible. 37 | -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | Version 0.1 2 | =========== 3 | Initial release. -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(test), no_std)] 2 | #![warn( 3 | rustdoc::invalid_html_tags, 4 | missing_debug_implementations, 5 | trivial_casts, 6 | unused_lifetimes, 7 | unused_import_braces 8 | )] 9 | #![deny(missing_docs, unaligned_references)] 10 | 11 | //! # num-ord 12 | //! 13 | //! This crate provides a numerically ordered wrapper type, [`NumOrd`]. This 14 | //! type implements the [`PartialOrd`] and [`PartialEq`] traits for all the 15 | //! possible combinations of built-in integer types, in a mathematically correct 16 | //! manner without overflows. 17 | //! 18 | //! For example, comparing an `x: i64` and a `y: f64` is actually quite 19 | //! difficult. Neither `(x as f64) < y` nor `x < (y as i64)` is correct. But 20 | //! `NumOrd(x) < NumOrd(y)` is: 21 | //! ```rust 22 | //! use num_ord::NumOrd; 23 | //! 24 | //! let x = 3_i64; 25 | //! let y = 3.5_f64; 26 | //! assert_eq!(x < (y as i64), false); // Incorrect. 27 | //! assert_eq!(NumOrd(x) < NumOrd(y), true); // Correct. 28 | //! 29 | //! let x = 9007199254740993_i64; 30 | //! let y = 9007199254740992_f64; 31 | //! assert_eq!(format!("{}", y), "9007199254740992"); // No rounded constant trickery! 32 | //! assert_eq!((x as f64) <= y, true); // Incorrect. 33 | //! assert_eq!(NumOrd(x) <= NumOrd(y), false); // Correct. 34 | //! ``` 35 | 36 | use core::cmp::Ordering; 37 | 38 | /// A numerically ordered wrapper type. 39 | /// 40 | /// See the crate docs for more details. 41 | #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] 42 | #[repr(transparent)] 43 | pub struct NumOrd(pub T); 44 | 45 | macro_rules! common_type_impl_body { 46 | ($Lhs:ty, $Rhs:ty, $CommonT:ty, $lhs:expr, $rhs:expr, $op:ident, $less:expr, $greater:expr, $nan:expr) => {{ 47 | ($lhs as $CommonT).$op(&($rhs as $CommonT)) 48 | }}; 49 | } 50 | 51 | macro_rules! int_float_impl_body { 52 | ($IntT:ty, $FloatT:ty, $_UnusedT:ty, $lhs:expr, $rhs:expr, $op:ident, $less:expr, $greater:expr, $nan:expr) => {{ 53 | let lhs = $lhs; 54 | let rhs = $rhs; 55 | 56 | // Range in which FloatT is dense in the integers. 57 | const FLOAT_DENSE_INT_MIN: $IntT = 58 | (0 as $IntT).saturating_sub(1) << <$FloatT>::MANTISSA_DIGITS; 59 | const FLOAT_DENSE_INT_MAX: $IntT = 1 << <$FloatT>::MANTISSA_DIGITS; 60 | 61 | // One above IntT::MAX as FloatT. May be infinite. 62 | const INT_MAX_POWER_OF_TWO: $IntT = <$IntT>::MAX ^ (<$IntT>::MAX >> 1); 63 | const INT_ONE_ABOVE_MAX_AS_FLOAT: $FloatT = 2.0 * (INT_MAX_POWER_OF_TWO as $FloatT); 64 | 65 | if FLOAT_DENSE_INT_MIN <= lhs && lhs <= FLOAT_DENSE_INT_MAX { 66 | // lhs is exactly representable as an integer valued float. 67 | (lhs as $FloatT).$op(&rhs) 68 | } else if INT_ONE_ABOVE_MAX_AS_FLOAT <= rhs { 69 | $less 70 | } else if <$IntT>::MIN as $FloatT > rhs { 71 | $greater 72 | } else if rhs.is_nan() { 73 | $nan 74 | } else { 75 | // The rounding to integer can't affect the outcome, since we know that 76 | // `lhs` is sufficiently large such that if `rhs` is close, it must be 77 | // an integer. 78 | lhs.$op(&(rhs as $IntT)) 79 | } 80 | }}; 81 | } 82 | 83 | // Must have IntT <= UintT in width. 84 | macro_rules! int_uint_impl_body { 85 | ($IntT:ty, $UintT:ty, $_UnusedT:ty, $lhs:expr, $rhs:expr, $op:ident, $less:expr, $greater:expr, $nan:expr) => {{ 86 | let lhs = $lhs; 87 | let rhs = $rhs; 88 | 89 | if lhs < 0 { 90 | $less 91 | } else { 92 | (lhs as $UintT).$op(&rhs) 93 | } 94 | }}; 95 | } 96 | 97 | macro_rules! apply_impl_body { 98 | ($impl_body:ident, $Lhs:ty, $Rhs:ty, $CommonT:ty) => { 99 | impl PartialEq> for NumOrd<$Lhs> { 100 | fn eq(&self, other: &NumOrd<$Rhs>) -> bool { 101 | $impl_body!($Lhs, $Rhs, $CommonT, self.0, other.0, eq, false, false, false) 102 | } 103 | } 104 | 105 | impl PartialOrd> for NumOrd<$Lhs> { 106 | fn partial_cmp(&self, other: &NumOrd<$Rhs>) -> Option { 107 | $impl_body!( 108 | $Lhs, 109 | $Rhs, 110 | $CommonT, 111 | self.0, 112 | other.0, 113 | partial_cmp, 114 | Some(Ordering::Less), 115 | Some(Ordering::Greater), 116 | None 117 | ) 118 | } 119 | 120 | fn lt(&self, other: &NumOrd<$Rhs>) -> bool { 121 | $impl_body!($Lhs, $Rhs, $CommonT, self.0, other.0, lt, true, false, false) 122 | } 123 | 124 | fn le(&self, other: &NumOrd<$Rhs>) -> bool { 125 | $impl_body!($Lhs, $Rhs, $CommonT, self.0, other.0, le, true, false, false) 126 | } 127 | 128 | fn gt(&self, other: &NumOrd<$Rhs>) -> bool { 129 | $impl_body!($Lhs, $Rhs, $CommonT, self.0, other.0, gt, false, true, false) 130 | } 131 | 132 | fn ge(&self, other: &NumOrd<$Rhs>) -> bool { 133 | $impl_body!($Lhs, $Rhs, $CommonT, self.0, other.0, ge, false, true, false) 134 | } 135 | } 136 | 137 | // Reverse implementation. 138 | impl PartialEq> for NumOrd<$Rhs> { 139 | fn eq(&self, other: &NumOrd<$Lhs>) -> bool { 140 | other == self 141 | } 142 | } 143 | 144 | impl PartialOrd> for NumOrd<$Rhs> { 145 | fn partial_cmp(&self, other: &NumOrd<$Lhs>) -> Option { 146 | other.partial_cmp(self).map(|o| o.reverse()) 147 | } 148 | 149 | fn lt(&self, other: &NumOrd<$Lhs>) -> bool { 150 | other > self 151 | } 152 | 153 | fn le(&self, other: &NumOrd<$Lhs>) -> bool { 154 | other >= self 155 | } 156 | 157 | fn gt(&self, other: &NumOrd<$Lhs>) -> bool { 158 | other < self 159 | } 160 | 161 | fn ge(&self, other: &NumOrd<$Lhs>) -> bool { 162 | other <= self 163 | } 164 | } 165 | }; 166 | } 167 | 168 | apply_impl_body!(int_float_impl_body, i64, f32, ()); 169 | apply_impl_body!(int_float_impl_body, i128, f32, ()); 170 | apply_impl_body!(int_float_impl_body, i64, f64, ()); 171 | apply_impl_body!(int_float_impl_body, i128, f64, ()); 172 | apply_impl_body!(int_float_impl_body, u64, f32, ()); 173 | apply_impl_body!(int_float_impl_body, u128, f32, ()); 174 | apply_impl_body!(int_float_impl_body, u64, f64, ()); 175 | apply_impl_body!(int_float_impl_body, u128, f64, ()); 176 | 177 | apply_impl_body!(int_uint_impl_body, i8, u128, ()); 178 | apply_impl_body!(int_uint_impl_body, i16, u128, ()); 179 | apply_impl_body!(int_uint_impl_body, i32, u128, ()); 180 | apply_impl_body!(int_uint_impl_body, i64, u128, ()); 181 | apply_impl_body!(int_uint_impl_body, i128, u128, ()); 182 | 183 | macro_rules! impl_common_type { 184 | ($($T:ty, $U:ty => $C:ty;)*) => {$( 185 | apply_impl_body!(common_type_impl_body, $T, $U, $C); 186 | )*} 187 | } 188 | 189 | impl_common_type! { 190 | // See tools/gen.py. 191 | u8, i8 => i16; 192 | u8, u16 => u16; 193 | u8, i16 => i16; 194 | u8, u32 => u32; 195 | u8, i32 => i32; 196 | u8, u64 => u64; 197 | u8, i64 => i64; 198 | u8, u128 => u128; 199 | u8, i128 => i128; 200 | u8, f32 => f32; 201 | u8, f64 => f64; 202 | i8, u16 => i32; 203 | i8, i16 => i16; 204 | i8, u32 => i64; 205 | i8, i32 => i32; 206 | i8, u64 => i128; 207 | i8, i64 => i64; 208 | i8, i128 => i128; 209 | i8, f32 => f32; 210 | i8, f64 => f64; 211 | u16, i16 => i32; 212 | u16, u32 => u32; 213 | u16, i32 => i32; 214 | u16, u64 => u64; 215 | u16, i64 => i64; 216 | u16, u128 => u128; 217 | u16, i128 => i128; 218 | u16, f32 => f32; 219 | u16, f64 => f64; 220 | i16, u32 => i64; 221 | i16, i32 => i32; 222 | i16, u64 => i128; 223 | i16, i64 => i64; 224 | i16, i128 => i128; 225 | i16, f32 => f32; 226 | i16, f64 => f64; 227 | u32, i32 => i64; 228 | u32, u64 => u64; 229 | u32, i64 => i64; 230 | u32, u128 => u128; 231 | u32, i128 => i128; 232 | u32, f32 => f64; 233 | u32, f64 => f64; 234 | i32, u64 => i128; 235 | i32, i64 => i64; 236 | i32, i128 => i128; 237 | i32, f32 => f64; 238 | i32, f64 => f64; 239 | u64, i64 => i128; 240 | u64, u128 => u128; 241 | u64, i128 => i128; 242 | i64, i128 => i128; 243 | f32, f64 => f64; 244 | } 245 | 246 | #[cfg(test)] 247 | mod tests { 248 | use super::NumOrd; 249 | use rug::ops::Pow as _; 250 | use rug::{Integer, Rational}; 251 | 252 | struct NumType { 253 | pub interesting_values: Vec, 254 | pub convert_exactly: fn(&Rational) -> Option, 255 | } 256 | 257 | fn compare(t1: &NumType, t2: &NumType) 258 | where 259 | T1: 'static + std::fmt::Display + Copy, 260 | T2: 'static + std::fmt::Display + Copy, 261 | NumOrd: PartialOrd>, 262 | { 263 | // Ordering between equal types needn't be tested 264 | if std::any::TypeId::of::() == std::any::TypeId::of::() { 265 | return; 266 | } 267 | 268 | let mut interesting_values = t1.interesting_values.iter().collect::>(); 269 | interesting_values.extend(&t2.interesting_values); 270 | interesting_values.sort_unstable(); 271 | interesting_values.dedup(); 272 | 273 | for r1 in &interesting_values { 274 | for r2 in &interesting_values { 275 | if let (Some(v1), Some(v2)) = ((t1.convert_exactly)(r1), (t2.convert_exactly)(r2)) { 276 | let expected_ordering = r1.partial_cmp(r2).unwrap(); 277 | let got_ordering = NumOrd(v1).partial_cmp(&NumOrd(v2)).unwrap(); 278 | 279 | if expected_ordering != got_ordering { 280 | panic!( 281 | "{}_{}.cmp({}_{}) was {:?}, expected {:?}. (Raw values: {}, {})", 282 | v1, 283 | std::any::type_name::(), 284 | v2, 285 | std::any::type_name::(), 286 | got_ordering, 287 | expected_ordering, 288 | r1, 289 | r2, 290 | ); 291 | } 292 | } 293 | } 294 | } 295 | } 296 | 297 | fn to_integer(x: &Rational) -> Option<&Integer> { 298 | if *x.denom() == 1 { 299 | Some(x.numer()) 300 | } else { 301 | None 302 | } 303 | } 304 | fn interesting_values(bases: &[&Rational]) -> Vec { 305 | let one_half = Rational::from((1, 2)); 306 | 307 | let mut vals = Vec::new(); 308 | for &val in bases { 309 | vals.push(val.clone()); 310 | vals.push((val + &one_half).into()); 311 | vals.push((val - &one_half).into()); 312 | vals.push((val + 1_u32).into()); 313 | vals.push((val - 1_u32).into()); 314 | } 315 | vals 316 | } 317 | fn two_pow(exp: u32) -> Rational { 318 | Rational::from(2).pow(exp) 319 | } 320 | fn make_unsigned(bits: u32, convert_exactly: fn(&Rational) -> Option) -> NumType { 321 | let min = Rational::from(0); 322 | let max = two_pow(bits) - 1_u32; 323 | NumType { 324 | interesting_values: interesting_values(&[&min, &max]), 325 | convert_exactly, 326 | } 327 | } 328 | fn make_signed(bits: u32, convert_exactly: fn(&Rational) -> Option) -> NumType { 329 | let min = -two_pow(bits - 1); 330 | let max = two_pow(bits - 1) - 1_u32; 331 | NumType { 332 | interesting_values: interesting_values(&[&min, &max]), 333 | convert_exactly, 334 | } 335 | } 336 | 337 | #[test] 338 | fn test_everything() { 339 | macro_rules! compare_all_combinations { 340 | ($($type:ty, $type_data:ident;)*) => { 341 | macro_rules! compare_all_against { 342 | ($given_type:ty, $given_type_data:ident) => { 343 | // Compare all types against $given_type 344 | $( compare::<$type, $given_type>(&$type_data, &$given_type_data); )* 345 | }; 346 | } 347 | // For each type, compare all other types against it 348 | $( compare_all_against!($type, $type_data); )* 349 | }; 350 | } 351 | 352 | let u8_data = make_unsigned(8, |x| to_integer(x).and_then(|i| i.to_u8())); 353 | let u16_data = make_unsigned(16, |x| to_integer(x).and_then(|i| i.to_u16())); 354 | let u32_data = make_unsigned(32, |x| to_integer(x).and_then(|i| i.to_u32())); 355 | let u64_data = make_unsigned(64, |x| to_integer(x).and_then(|i| i.to_u64())); 356 | let u128_data = make_unsigned(128, |x| to_integer(x).and_then(|i| i.to_u128())); 357 | let i8_data = make_signed(8, |x| to_integer(x).and_then(|i| i.to_i8())); 358 | let i16_data = make_signed(16, |x| to_integer(x).and_then(|i| i.to_i16())); 359 | let i32_data = make_signed(32, |x| to_integer(x).and_then(|i| i.to_i32())); 360 | let i64_data = make_signed(64, |x| to_integer(x).and_then(|i| i.to_i64())); 361 | let i128_data = make_signed(128, |x| to_integer(x).and_then(|i| i.to_i128())); 362 | let f32_data = NumType { 363 | interesting_values: interesting_values(&[ 364 | &-two_pow(24), // int min 365 | &two_pow(24), // int max 366 | &-two_pow(127), // float min 367 | &two_pow(127), // float max 368 | ]), 369 | convert_exactly: |r| { 370 | (Rational::from_f32(r.to_f32()).as_ref() == Some(r)).then(|| r.to_f32()) 371 | }, 372 | }; 373 | let f64_data = NumType { 374 | interesting_values: interesting_values(&[ 375 | &-two_pow(53), // int min 376 | &two_pow(53), // int max 377 | &-two_pow(1023), // float min 378 | &two_pow(1023), // float max 379 | ]), 380 | convert_exactly: |r| (r.to_f64() == *r).then(|| r.to_f64()), 381 | }; 382 | 383 | // Verify correct ordering for all interesting values of all number type combinations 384 | compare_all_combinations!( 385 | u8, u8_data; 386 | u16, u16_data; 387 | u32, u32_data; 388 | u64, u64_data; 389 | u128, u128_data; 390 | i8, i8_data; 391 | i16, i16_data; 392 | i32, i32_data; 393 | i64, i64_data; 394 | i128, i128_data; 395 | f32, f32_data; 396 | f64, f64_data; 397 | ); 398 | } 399 | } 400 | --------------------------------------------------------------------------------