1#![allow(
2 clippy::cast_possible_truncation,
3 clippy::cast_possible_wrap,
4 clippy::cast_sign_loss,
5 clippy::cast_precision_loss,
6 clippy::float_cmp,
7 clippy::must_use_candidate
8)]
9
10use fraction::{BigFraction, One, Zero};
11use serde_json::Number;
12#[cfg(feature = "arbitrary-precision")]
13use std::cmp::Ordering;
14
15macro_rules! define_num_cmp {
16 ($($trait_fn:ident => $fn_name:ident, $op:tt, $infinity_positive:literal, $ord_pat:pat),* $(,)?) => {
17 $(
18 pub fn $fn_name<T>(value: &Number, limit: T) -> bool
19 where
20 T: Copy + num_traits::ToPrimitive,
21 u64: num_cmp::NumCmp<T>,
22 i64: num_cmp::NumCmp<T>,
23 f64: num_cmp::NumCmp<T>,
24 {
25 if let Some(v) = value.as_u64() {
26 num_cmp::NumCmp::$trait_fn(v, limit)
27 } else if let Some(v) = value.as_i64() {
28 num_cmp::NumCmp::$trait_fn(v, limit)
29 } else if let Some(v) = value.as_f64() {
30 #[cfg(feature = "arbitrary-precision")]
33 if v <= i64::MIN as f64 || v >= u64::MAX as f64 {
34 if let Some(big_value) = bignum::try_parse_bigint(value) {
35 if let Some(ordering) = bignum::compare_bigint_to_limit(&big_value, limit) {
36 return matches!(ordering, $ord_pat);
37 }
38 }
39 }
40 num_cmp::NumCmp::$trait_fn(v, limit)
41 } else {
42 #[cfg(feature = "arbitrary-precision")]
43 {
44 if let Some(big_value) = bignum::try_parse_bigfraction(value) {
45 if let Some(limit_f64) = num_traits::ToPrimitive::to_f64(&limit) {
46 let limit_frac = BigFraction::from(limit_f64);
47 return big_value $op limit_frac;
48 }
49 }
50 let is_negative = value.as_str().starts_with('-');
52 if $infinity_positive {
53 !is_negative
54 } else {
55 is_negative
56 }
57 }
58 #[cfg(not(feature = "arbitrary-precision"))]
59 {
60 unreachable!("Always Some without `arbitrary-precision`")
61 }
62 }
63 }
64 )*
65 };
66}
67
68define_num_cmp!(
69 num_ge => ge, >=, true, Ordering::Greater | Ordering::Equal, num_le => le, <=, false, Ordering::Less | Ordering::Equal, num_gt => gt, >, true, Ordering::Greater,
72 num_lt => lt, <, false, Ordering::Less,
73);
74
75#[cfg(feature = "macros")]
76pub fn eq<T>(value: &Number, limit: T) -> bool
77where
78 T: Copy + num_traits::ToPrimitive,
79 u64: num_cmp::NumCmp<T>,
80 i64: num_cmp::NumCmp<T>,
81 f64: num_cmp::NumCmp<T>,
82{
83 if let Some(v) = value.as_u64() {
84 num_cmp::NumCmp::num_eq(v, limit)
85 } else if let Some(v) = value.as_i64() {
86 num_cmp::NumCmp::num_eq(v, limit)
87 } else if let Some(v) = value.as_f64() {
88 num_cmp::NumCmp::num_eq(v, limit)
89 } else {
90 #[cfg(feature = "arbitrary-precision")]
91 {
92 if let Some(big_value) = bignum::try_parse_bigfraction(value) {
93 if let Some(limit_f64) = num_traits::ToPrimitive::to_f64(&limit) {
94 return big_value == BigFraction::from(limit_f64);
95 }
96 }
97 false
98 }
99 #[cfg(not(feature = "arbitrary-precision"))]
100 {
101 unreachable!("Always Some without `arbitrary-precision`")
102 }
103 }
104}
105
106pub fn is_multiple_of_float(value: &Number, multiple: f64) -> bool {
107 if let Some(value_f64) = value.as_f64() {
108 if value_f64.is_zero() {
111 return true;
112 }
113 if value_f64.abs() < multiple {
114 return false;
115 }
116 (BigFraction::from(value_f64) / BigFraction::from(multiple))
124 .denom()
125 .is_none_or(One::is_one)
126 } else {
127 false
130 }
131}
132
133const MAX_SAFE_INTEGER: u64 = 1u64 << 53;
136
137pub fn is_multiple_of_integer(value: &Number, multiple: f64) -> bool {
138 let divisor_ok =
144 multiple > 0.0 && multiple <= MAX_SAFE_INTEGER as f64 && multiple.fract() == 0.0;
145 if divisor_ok {
146 if let Some(v) = value.as_u64() {
147 return (v % (multiple as u64)) == 0;
148 }
149 if let Some(v) = value.as_i64() {
150 return (v % (multiple as i64)) == 0;
151 }
152 }
153
154 if let Some(value_f64) = value.as_f64() {
155 value_f64.fract() == 0. && (value_f64 % multiple) == 0.
158 } else {
159 #[cfg(feature = "arbitrary-precision")]
161 {
162 if let Some(big_value) = bignum::try_parse_bigint(value) {
164 use num_bigint::BigInt;
165 let multiple_int = BigInt::from(multiple as i64);
175 return bignum::is_multiple_of_bigint(&big_value, &multiple_int);
176 }
177 false
179 }
180 #[cfg(not(feature = "arbitrary-precision"))]
181 {
182 unreachable!("Always Some without `arbitrary-precision`")
183 }
184 }
185}
186
187#[cfg(feature = "arbitrary-precision")]
188pub mod bignum {
189 use fraction::BigFraction;
190 use num_bigint::BigInt;
191 use num_traits::{ToPrimitive, Zero};
192 use serde_json::Number;
193 use std::str::FromStr;
194
195 const MAX_EXPONENT_ADJUSTMENT: u32 = 1_000_000;
204
205 #[derive(Debug, Clone)]
206 struct DecimalComponents {
207 negative: bool,
208 digits: String,
209 fraction_digits: usize,
210 exponent: i64,
211 }
212
213 impl DecimalComponents {
214 fn parse(num_str: &str) -> Option<Self> {
215 let bytes = num_str.as_bytes();
216 if bytes.is_empty() {
217 return None;
218 }
219
220 let mut idx = 0;
221 let negative = if bytes[idx] == b'-' {
222 idx += 1;
223 true
224 } else {
225 false
226 };
227
228 if idx >= bytes.len() {
229 return None;
230 }
231
232 let mut digits = String::with_capacity(bytes.len());
233 let int_start = idx;
234 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
235 idx += 1;
236 }
237 if int_start == idx {
238 return None;
239 }
240 digits.push_str(&num_str[int_start..idx]);
241
242 let mut fraction_digits = 0usize;
243 if idx < bytes.len() && bytes[idx] == b'.' {
244 idx += 1;
245 let frac_start = idx;
246 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
247 idx += 1;
248 }
249 if frac_start == idx {
250 return None;
251 }
252 digits.push_str(&num_str[frac_start..idx]);
253 fraction_digits = idx - frac_start;
254 }
255
256 let mut exponent: i64 = 0;
257 if idx < bytes.len() && (bytes[idx] == b'e' || bytes[idx] == b'E') {
258 idx += 1;
259 if idx >= bytes.len() {
260 return None;
261 }
262 let mut exp_sign: i64 = 1;
263 if bytes[idx] == b'+' {
264 idx += 1;
265 } else if bytes[idx] == b'-' {
266 exp_sign = -1;
267 idx += 1;
268 }
269 let exp_start = idx;
270 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
271 idx += 1;
272 }
273 if exp_start == idx {
274 return None;
275 }
276 let exp_value = num_str[exp_start..idx].parse::<i64>().ok()?;
277 exponent = exp_value.checked_mul(exp_sign)?;
278 }
279
280 if idx != bytes.len() {
281 return None;
282 }
283
284 Some(Self {
285 negative,
286 digits,
287 fraction_digits,
288 exponent,
289 })
290 }
291
292 #[inline]
293 fn decimal_shift(&self) -> i64 {
294 self.exponent - self.fraction_digits as i64
295 }
296 }
297
298 fn digits_are_zero(s: &str) -> bool {
299 s.bytes().all(|b| b == b'0')
300 }
301
302 fn trailing_zero_count(s: &str) -> usize {
303 s.as_bytes()
304 .iter()
305 .rev()
306 .take_while(|b| **b == b'0')
307 .count()
308 }
309
310 fn append_zeros(target: &mut String, count: usize) -> Option<()> {
311 let new_len = target.len().checked_add(count)?;
312 target.reserve(count);
313 target.extend(std::iter::repeat_n('0', count));
314 debug_assert_eq!(target.len(), new_len);
315 Some(())
316 }
317
318 fn pow10_bigint(exp: usize) -> Option<BigInt> {
319 if exp == 0 {
320 return Some(BigInt::from(1));
321 }
322 let exp_u32 = u32::try_from(exp).ok()?;
323 Some(BigInt::from(10).pow(exp_u32))
324 }
325
326 fn shift_exceeds_limit(shift: i64) -> bool {
327 if shift <= 0 {
328 return false;
329 }
330 shift as u64 > u64::from(MAX_EXPONENT_ADJUSTMENT)
331 }
332
333 fn exponent_reduction_exceeds_limit(exponent: i64) -> bool {
334 if exponent >= 0 {
335 return false;
336 }
337 match exponent.checked_abs() {
338 Some(abs) => abs as u64 > u64::from(MAX_EXPONENT_ADJUSTMENT),
339 None => true,
340 }
341 }
342
343 pub fn try_parse_bigint(num: &Number) -> Option<BigInt> {
346 use super::MAX_SAFE_INTEGER;
347
348 let num_str = num.as_str();
349
350 if let Some(v) = num.as_i64() {
355 if v.unsigned_abs() <= MAX_SAFE_INTEGER {
356 return None;
357 }
358 }
359
360 let has_fraction_or_exponent = num_str.bytes().any(|b| b == b'.' || b == b'e' || b == b'E');
361 if !has_fraction_or_exponent {
362 return BigInt::from_str(num_str).ok();
363 }
364
365 let mut components = DecimalComponents::parse(num_str)?;
366 let mut shift = components.decimal_shift();
367
368 if shift < 0 {
369 let needed = (-shift) as usize;
370 if digits_are_zero(&components.digits) {
371 components.digits.clear();
372 components.digits.push('0');
373 shift = 0;
374 } else {
375 if exponent_reduction_exceeds_limit(components.exponent) {
376 return None;
377 }
378 let zeros = trailing_zero_count(&components.digits);
379 if zeros < needed {
380 return None;
381 }
382 let new_len = components.digits.len() - needed;
383 components.digits.truncate(new_len);
384 shift = 0;
385 }
386 }
387
388 if shift > 0 {
389 if shift_exceeds_limit(shift) {
390 return None;
391 }
392 append_zeros(&mut components.digits, shift as usize)?;
393 }
394
395 let digits_trimmed = components.digits.trim_start_matches('0');
396 let digits_ref = if digits_trimmed.is_empty() {
397 "0"
398 } else {
399 digits_trimmed
400 };
401 let mut value = BigInt::from_str(digits_ref).ok()?;
402 if components.negative && !value.is_zero() {
403 value = -value;
404 }
405 Some(value)
406 }
407
408 pub fn try_parse_bigfraction(num: &Number) -> Option<BigFraction> {
418 if num.as_i64().is_some() {
420 return None;
421 }
422
423 let num_str = num.as_str();
424
425 let mut has_decimal_point = false;
427 let mut has_exponent = false;
428 for b in num_str.bytes() {
429 if b == b'.' {
430 has_decimal_point = true;
431 } else if b == b'e' || b == b'E' {
432 has_exponent = true;
433 break;
434 }
435 }
436
437 if !has_decimal_point && !has_exponent {
438 return None;
439 }
440
441 if !has_exponent {
442 return BigFraction::from_str(num_str).ok();
443 }
444
445 let components = DecimalComponents::parse(num_str)?;
446 let shift = components.decimal_shift();
447
448 if shift >= 0 {
450 return None;
451 }
452
453 if exponent_reduction_exceeds_limit(components.exponent) {
454 return None;
455 }
456
457 let denom_power = (-shift) as usize;
458 let denominator = pow10_bigint(denom_power)?;
459 let mut numerator = BigInt::from_str(&components.digits).ok()?;
460 if components.negative && !numerator.is_zero() {
461 numerator = -numerator;
462 }
463 Some(BigFraction::from(numerator) / BigFraction::from(denominator))
464 }
465
466 pub(crate) fn compare_bigint_to_limit<T>(big: &BigInt, limit: T) -> Option<std::cmp::Ordering>
472 where
473 T: Copy + ToPrimitive,
474 {
475 use std::cmp::Ordering;
476
477 let limit_f64 = limit.to_f64()?;
478 if limit_f64.fract() == 0.0 {
479 if let Some(limit_int) = limit.to_i64() {
481 return Some(big.cmp(&BigInt::from(limit_int)));
482 }
483 if let Some(limit_int) = limit.to_u64() {
484 return Some(big.cmp(&BigInt::from(limit_int)));
485 }
486 }
487 if limit_f64 == f64::INFINITY {
488 return Some(Ordering::Less);
489 }
490 if limit_f64 == f64::NEG_INFINITY {
491 return Some(Ordering::Greater);
492 }
493 None
494 }
495
496 macro_rules! define_bigint_cmp {
497 ($($fn_name:ident, $prim_type:ty, $to_prim:ident, $op:tt, $overflow_sign:expr);* $(;)?) => {
498 $(
499 pub fn $fn_name(bigint: &BigInt, value: $prim_type) -> bool {
500 if let Some(converted) = bigint.$to_prim() {
501 converted $op value
502 } else {
503 bigint.sign() == $overflow_sign
504 }
505 }
506 )*
507 };
508 }
509
510 define_bigint_cmp!(
511 bigint_ge_u64, u64, to_u64, >=, num_bigint::Sign::Plus;
512 bigint_le_u64, u64, to_u64, <=, num_bigint::Sign::Minus;
513 bigint_gt_u64, u64, to_u64, >, num_bigint::Sign::Plus;
514 bigint_lt_u64, u64, to_u64, <, num_bigint::Sign::Minus;
515 bigint_ge_i64, i64, to_i64, >=, num_bigint::Sign::Plus;
516 bigint_le_i64, i64, to_i64, <=, num_bigint::Sign::Minus;
517 bigint_gt_i64, i64, to_i64, >, num_bigint::Sign::Plus;
518 bigint_lt_i64, i64, to_i64, <, num_bigint::Sign::Minus;
519 bigint_ge_f64, f64, to_f64, >=, num_bigint::Sign::Plus;
520 bigint_le_f64, f64, to_f64, <=, num_bigint::Sign::Minus;
521 bigint_gt_f64, f64, to_f64, >, num_bigint::Sign::Plus;
522 bigint_lt_f64, f64, to_f64, <, num_bigint::Sign::Minus;
523 );
524
525 macro_rules! define_reverse_cmp {
527 ($($rev_ge:ident, $rev_le:ident, $rev_gt:ident, $rev_lt:ident, $prim_type:ty, $big_type:ty, $fwd_ge:ident, $fwd_le:ident, $fwd_gt:ident, $fwd_lt:ident);* $(;)?) => {
528 $(
529 pub fn $rev_ge(value: $prim_type, big: &$big_type) -> bool {
530 $fwd_le(big, value)
531 }
532
533 pub fn $rev_le(value: $prim_type, big: &$big_type) -> bool {
534 $fwd_ge(big, value)
535 }
536
537 pub fn $rev_gt(value: $prim_type, big: &$big_type) -> bool {
538 $fwd_lt(big, value)
539 }
540
541 pub fn $rev_lt(value: $prim_type, big: &$big_type) -> bool {
542 $fwd_gt(big, value)
543 }
544 )*
545 };
546 }
547
548 define_reverse_cmp!(
549 u64_ge_bigint, u64_le_bigint, u64_gt_bigint, u64_lt_bigint, u64, BigInt, bigint_ge_u64, bigint_le_u64, bigint_gt_u64, bigint_lt_u64;
550 i64_ge_bigint, i64_le_bigint, i64_gt_bigint, i64_lt_bigint, i64, BigInt, bigint_ge_i64, bigint_le_i64, bigint_gt_i64, bigint_lt_i64;
551 f64_ge_bigint, f64_le_bigint, f64_gt_bigint, f64_lt_bigint, f64, BigInt, bigint_ge_f64, bigint_le_f64, bigint_gt_f64, bigint_lt_f64;
552 );
553
554 pub fn is_multiple_of_bigint(value: &BigInt, multiple: &BigInt) -> bool {
556 if value.is_zero() {
559 return true;
560 }
561
562 (value % multiple).is_zero()
569 }
570
571 macro_rules! define_bigfraction_cmp {
573 ($($fn_name:ident, $prim_type:ty, $op:tt);* $(;)?) => {
574 $(
575 pub fn $fn_name(bigfrac: &BigFraction, value: $prim_type) -> bool {
576 let value_frac = BigFraction::from(value);
577 *bigfrac $op value_frac
578 }
579 )*
580 };
581 }
582
583 define_bigfraction_cmp!(
584 bigfrac_ge_u64, u64, >=;
585 bigfrac_le_u64, u64, <=;
586 bigfrac_gt_u64, u64, >;
587 bigfrac_lt_u64, u64, <;
588 bigfrac_ge_i64, i64, >=;
589 bigfrac_le_i64, i64, <=;
590 bigfrac_gt_i64, i64, >;
591 bigfrac_lt_i64, i64, <;
592 bigfrac_ge_f64, f64, >=;
593 bigfrac_le_f64, f64, <=;
594 bigfrac_gt_f64, f64, >;
595 bigfrac_lt_f64, f64, <;
596 );
597
598 define_reverse_cmp!(
599 u64_ge_bigfrac, u64_le_bigfrac, u64_gt_bigfrac, u64_lt_bigfrac, u64, BigFraction, bigfrac_ge_u64, bigfrac_le_u64, bigfrac_gt_u64, bigfrac_lt_u64;
600 i64_ge_bigfrac, i64_le_bigfrac, i64_gt_bigfrac, i64_lt_bigfrac, i64, BigFraction, bigfrac_ge_i64, bigfrac_le_i64, bigfrac_gt_i64, bigfrac_lt_i64;
601 f64_ge_bigfrac, f64_le_bigfrac, f64_gt_bigfrac, f64_lt_bigfrac, f64, BigFraction, bigfrac_ge_f64, bigfrac_le_f64, bigfrac_gt_f64, bigfrac_lt_f64;
602 );
603
604 pub fn is_multiple_of_bigfrac(value: &BigFraction, multiple: &BigFraction) -> bool {
606 if value.is_zero() {
608 return true;
609 }
610 if multiple.is_zero() {
612 return false;
613 }
614 (value / multiple).denom().is_none_or(fraction::One::is_one)
617 }
618}
619
620#[cfg(all(test, feature = "arbitrary-precision"))]
621mod tests {
622 use super::bignum;
623 use fraction::BigFraction;
624 use num_bigint::BigInt;
625 use serde_json::{Number, Value};
626 use std::cmp::Ordering;
627 use test_case::test_case;
628
629 fn number_from_str(raw: &str) -> Number {
630 match serde_json::from_str::<Value>(raw).expect("valid JSON number") {
631 Value::Number(num) => num,
632 _ => unreachable!(),
633 }
634 }
635
636 #[test_case("18446744073709551616", u64::MAX, Ordering::Greater; "above u64 limit")]
637 fn compare_bigint_to_u64_limit(big: &str, limit: u64, expected: Ordering) {
638 let big = BigInt::parse_bytes(big.as_bytes(), 10).unwrap();
639 assert_eq!(bignum::compare_bigint_to_limit(&big, limit), Some(expected));
640 }
641
642 #[test_case("-18446744073709551616", i64::MIN, Ordering::Less; "below i64 limit")]
643 fn compare_bigint_to_i64_limit(big: &str, limit: i64, expected: Ordering) {
644 let big = BigInt::parse_bytes(big.as_bytes(), 10).unwrap();
645 assert_eq!(bignum::compare_bigint_to_limit(&big, limit), Some(expected));
646 }
647
648 #[test_case(f64::INFINITY, Some(Ordering::Less); "infinity limit")]
651 #[test_case(f64::NEG_INFINITY, Some(Ordering::Greater); "negative infinity limit")]
652 #[test_case(0.5, None; "no exact integer form")]
653 fn compare_bigint_to_f64_limit(limit: f64, expected: Option<Ordering>) {
654 let big = BigInt::parse_bytes(b"18446744073709551616", 10).unwrap();
655 assert_eq!(bignum::compare_bigint_to_limit(&big, limit), expected);
656 }
657
658 #[test]
659 fn bigint_parses_scientific_integer() {
660 let num = number_from_str("1e19");
661 let parsed = bignum::try_parse_bigint(&num).expect("parsed bigint");
662 assert_eq!(
663 parsed,
664 BigInt::parse_bytes(b"10000000000000000000", 10).unwrap()
665 );
666 }
667
668 #[test]
669 fn bigint_rejects_non_integer_scientific() {
670 let num = number_from_str("1.25e1");
671 assert!(bignum::try_parse_bigint(&num).is_none());
672 }
673
674 #[test]
675 fn bigfraction_parses_scientific_decimal() {
676 let num = number_from_str("1.5e-5");
677 let parsed = bignum::try_parse_bigfraction(&num).expect("parsed bigfraction");
678 let expected =
679 BigFraction::from(BigInt::from(3)) / BigFraction::from(BigInt::from(200_000));
680 assert_eq!(parsed, expected);
681 }
682
683 #[test]
684 fn bigfraction_skips_scientific_integer() {
685 let num = number_from_str("3e4");
686 assert!(bignum::try_parse_bigfraction(&num).is_none());
687 }
688}