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};
11#[cfg(feature = "arbitrary-precision")]
12use std::cmp::Ordering;
13
14#[cfg(feature = "arbitrary-precision")]
21#[inline]
22fn exact_ordering<N, T>(value: &N, rounded: f64, limit: T) -> Option<Ordering>
23where
24 N: crate::JsonNumber,
25 T: Copy + num_traits::ToPrimitive,
26 f64: num_cmp::NumCmp<T>,
27{
28 let saturated = rounded <= i64::MIN as f64 || rounded >= u64::MAX as f64;
29 if !saturated && !num_cmp::NumCmp::num_eq(rounded, limit) {
30 return None;
31 }
32 bignum::compare_to_limit(&value.to_number(), limit)
33}
34
35macro_rules! define_num_cmp {
36 ($($trait_fn:ident => $fn_name:ident, $op:tt, $infinity_positive:literal, $ord_pat:pat),* $(,)?) => {
37 $(
38 pub fn $fn_name<N, T>(value: &N, limit: T) -> bool
39 where
40 N: crate::JsonNumber,
41 T: Copy + num_traits::ToPrimitive,
42 u64: num_cmp::NumCmp<T>,
43 i64: num_cmp::NumCmp<T>,
44 f64: num_cmp::NumCmp<T>,
45 {
46 if let Some(v) = value.as_u64() {
47 num_cmp::NumCmp::$trait_fn(v, limit)
48 } else if let Some(v) = value.as_i64() {
49 num_cmp::NumCmp::$trait_fn(v, limit)
50 } else if let Some(v) = value.as_f64() {
51 #[cfg(feature = "arbitrary-precision")]
52 if let Some(ordering) = exact_ordering(value, v, limit) {
53 return matches!(ordering, $ord_pat);
54 }
55 num_cmp::NumCmp::$trait_fn(v, limit)
56 } else {
57 #[cfg(feature = "arbitrary-precision")]
58 {
59 if let Some(big_value) = bignum::try_parse_bigfraction(&value.to_number()) {
60 if let Some(limit_f64) = num_traits::ToPrimitive::to_f64(&limit) {
61 let limit_frac = BigFraction::from(limit_f64);
62 return big_value $op limit_frac;
63 }
64 }
65 let is_negative = value.as_str().starts_with('-');
67 if $infinity_positive {
68 !is_negative
69 } else {
70 is_negative
71 }
72 }
73 #[cfg(not(feature = "arbitrary-precision"))]
74 {
75 unreachable!("Always Some without `arbitrary-precision`")
76 }
77 }
78 }
79 )*
80 };
81}
82
83define_num_cmp!(
84 num_ge => ge, >=, true, Ordering::Greater | Ordering::Equal, num_le => le, <=, false, Ordering::Less | Ordering::Equal, num_gt => gt, >, true, Ordering::Greater,
87 num_lt => lt, <, false, Ordering::Less,
88);
89
90#[cfg(feature = "macros")]
91pub fn eq<N, T>(value: &N, limit: T) -> bool
92where
93 N: crate::JsonNumber,
94 T: Copy + num_traits::ToPrimitive,
95 u64: num_cmp::NumCmp<T>,
96 i64: num_cmp::NumCmp<T>,
97 f64: num_cmp::NumCmp<T>,
98{
99 if let Some(v) = value.as_u64() {
100 num_cmp::NumCmp::num_eq(v, limit)
101 } else if let Some(v) = value.as_i64() {
102 num_cmp::NumCmp::num_eq(v, limit)
103 } else if let Some(v) = value.as_f64() {
104 #[cfg(feature = "arbitrary-precision")]
105 if let Some(ordering) = exact_ordering(value, v, limit) {
106 return ordering == Ordering::Equal;
107 }
108 num_cmp::NumCmp::num_eq(v, limit)
109 } else {
110 #[cfg(feature = "arbitrary-precision")]
111 {
112 if let Some(big_value) = bignum::try_parse_bigfraction(&value.to_number()) {
113 if let Some(limit_f64) = num_traits::ToPrimitive::to_f64(&limit) {
114 return big_value == BigFraction::from(limit_f64);
115 }
116 }
117 false
118 }
119 #[cfg(not(feature = "arbitrary-precision"))]
120 {
121 unreachable!("Always Some without `arbitrary-precision`")
122 }
123 }
124}
125
126pub fn is_multiple_of_float<N: crate::JsonNumber>(value: &N, multiple: f64) -> bool {
127 if let Some(value_f64) = value.as_f64() {
128 if value_f64.is_zero() {
131 return true;
132 }
133 if value_f64.abs() < multiple {
134 return false;
135 }
136 (BigFraction::from(value_f64) / BigFraction::from(multiple))
144 .denom()
145 .is_none_or(One::is_one)
146 } else {
147 false
150 }
151}
152
153const MAX_SAFE_INTEGER: u64 = 1u64 << 53;
156
157pub fn is_multiple_of_integer<N: crate::JsonNumber>(value: &N, multiple: f64) -> bool {
158 let divisor_ok =
164 multiple > 0.0 && multiple <= MAX_SAFE_INTEGER as f64 && multiple.fract() == 0.0;
165 if divisor_ok {
166 if let Some(v) = value.as_u64() {
167 return (v % (multiple as u64)) == 0;
168 }
169 if let Some(v) = value.as_i64() {
170 return (v % (multiple as i64)) == 0;
171 }
172 #[cfg(feature = "arbitrary-precision")]
175 if let Some(big_value) = bignum::try_parse_bigint(&value.to_number()) {
176 let divisor = num_bigint::BigInt::from(multiple as i64);
177 return bignum::is_multiple_of_bigint(&big_value, &divisor);
178 }
179 }
180
181 if let Some(value_f64) = value.as_f64() {
182 #[cfg(feature = "arbitrary-precision")]
186 if value_f64 == 0.0 && !bignum::is_zero_literal(&value.to_number()) {
187 return false;
188 }
189 value_f64.fract() == 0. && (value_f64 % multiple) == 0.
192 } else {
193 #[cfg(feature = "arbitrary-precision")]
195 {
196 if let Some(big_value) = bignum::try_parse_bigint(&value.to_number()) {
198 use num_bigint::BigInt;
199 let multiple_int = BigInt::from(multiple as i64);
209 return bignum::is_multiple_of_bigint(&big_value, &multiple_int);
210 }
211 false
213 }
214 #[cfg(not(feature = "arbitrary-precision"))]
215 {
216 unreachable!("Always Some without `arbitrary-precision`")
217 }
218 }
219}
220
221#[cfg(feature = "arbitrary-precision")]
222pub mod bignum {
223 use fraction::BigFraction;
224 use num_bigint::BigInt;
225 use num_traits::{ToPrimitive, Zero};
226 use serde_json::Number;
227 use std::str::FromStr;
228
229 const MAX_EXPONENT_ADJUSTMENT: u32 = 1_000_000;
238
239 #[derive(Debug, Clone)]
240 struct DecimalComponents {
241 negative: bool,
242 digits: String,
243 fraction_digits: usize,
244 exponent: i64,
245 }
246
247 impl DecimalComponents {
248 fn parse(num_str: &str) -> Option<Self> {
249 let bytes = num_str.as_bytes();
250 if bytes.is_empty() {
251 return None;
252 }
253
254 let mut idx = 0;
255 let negative = if bytes[idx] == b'-' {
256 idx += 1;
257 true
258 } else {
259 false
260 };
261
262 if idx >= bytes.len() {
263 return None;
264 }
265
266 let mut digits = String::with_capacity(bytes.len());
267 let int_start = idx;
268 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
269 idx += 1;
270 }
271 if int_start == idx {
272 return None;
273 }
274 digits.push_str(&num_str[int_start..idx]);
275
276 let mut fraction_digits = 0usize;
277 if idx < bytes.len() && bytes[idx] == b'.' {
278 idx += 1;
279 let frac_start = idx;
280 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
281 idx += 1;
282 }
283 if frac_start == idx {
284 return None;
285 }
286 digits.push_str(&num_str[frac_start..idx]);
287 fraction_digits = idx - frac_start;
288 }
289
290 let mut exponent: i64 = 0;
291 if idx < bytes.len() && (bytes[idx] == b'e' || bytes[idx] == b'E') {
292 idx += 1;
293 if idx >= bytes.len() {
294 return None;
295 }
296 let mut exp_sign: i64 = 1;
297 if bytes[idx] == b'+' {
298 idx += 1;
299 } else if bytes[idx] == b'-' {
300 exp_sign = -1;
301 idx += 1;
302 }
303 let exp_start = idx;
304 while idx < bytes.len() && bytes[idx].is_ascii_digit() {
305 idx += 1;
306 }
307 if exp_start == idx {
308 return None;
309 }
310 let exp_value = num_str[exp_start..idx].parse::<i64>().ok()?;
311 exponent = exp_value.checked_mul(exp_sign)?;
312 }
313
314 if idx != bytes.len() {
315 return None;
316 }
317
318 Some(Self {
319 negative,
320 digits,
321 fraction_digits,
322 exponent,
323 })
324 }
325
326 #[inline]
327 fn decimal_shift(&self) -> i64 {
328 self.exponent - self.fraction_digits as i64
329 }
330 }
331
332 fn digits_are_zero(s: &str) -> bool {
333 s.bytes().all(|b| b == b'0')
334 }
335
336 fn trailing_zero_count(s: &str) -> usize {
337 s.as_bytes()
338 .iter()
339 .rev()
340 .take_while(|b| **b == b'0')
341 .count()
342 }
343
344 fn append_zeros(target: &mut String, count: usize) -> Option<()> {
345 let new_len = target.len().checked_add(count)?;
346 target.reserve(count);
347 target.extend(std::iter::repeat_n('0', count));
348 debug_assert_eq!(target.len(), new_len);
349 Some(())
350 }
351
352 fn pow10_bigint(exp: usize) -> Option<BigInt> {
353 if exp == 0 {
354 return Some(BigInt::from(1));
355 }
356 let exp_u32 = u32::try_from(exp).ok()?;
357 Some(BigInt::from(10).pow(exp_u32))
358 }
359
360 fn shift_exceeds_limit(shift: i64) -> bool {
361 if shift <= 0 {
362 return false;
363 }
364 shift as u64 > u64::from(MAX_EXPONENT_ADJUSTMENT)
365 }
366
367 fn exponent_reduction_exceeds_limit(exponent: i64) -> bool {
368 if exponent >= 0 {
369 return false;
370 }
371 match exponent.checked_abs() {
372 Some(abs) => abs as u64 > u64::from(MAX_EXPONENT_ADJUSTMENT),
373 None => true,
374 }
375 }
376
377 pub fn try_parse_bigint(num: &Number) -> Option<BigInt> {
380 use super::MAX_SAFE_INTEGER;
381
382 let num_str = num.as_str();
383
384 if let Some(v) = num.as_i64() {
389 if v.unsigned_abs() <= MAX_SAFE_INTEGER {
390 return None;
391 }
392 }
393
394 let has_fraction_or_exponent = num_str.bytes().any(|b| b == b'.' || b == b'e' || b == b'E');
395 if !has_fraction_or_exponent {
396 return BigInt::from_str(num_str).ok();
397 }
398
399 let mut components = DecimalComponents::parse(num_str)?;
400 let mut shift = components.decimal_shift();
401
402 if shift < 0 {
403 let needed = (-shift) as usize;
404 if digits_are_zero(&components.digits) {
405 components.digits.clear();
406 components.digits.push('0');
407 shift = 0;
408 } else {
409 if exponent_reduction_exceeds_limit(components.exponent) {
410 return None;
411 }
412 let zeros = trailing_zero_count(&components.digits);
413 if zeros < needed {
414 return None;
415 }
416 let new_len = components.digits.len() - needed;
417 components.digits.truncate(new_len);
418 shift = 0;
419 }
420 }
421
422 if shift > 0 {
423 if shift_exceeds_limit(shift) {
424 return None;
425 }
426 append_zeros(&mut components.digits, shift as usize)?;
427 }
428
429 let digits_trimmed = components.digits.trim_start_matches('0');
430 let digits_ref = if digits_trimmed.is_empty() {
431 "0"
432 } else {
433 digits_trimmed
434 };
435 let mut value = BigInt::from_str(digits_ref).ok()?;
436 if components.negative && !value.is_zero() {
437 value = -value;
438 }
439 Some(value)
440 }
441
442 pub fn try_parse_bigfraction(num: &Number) -> Option<BigFraction> {
452 if num.as_i64().is_some() {
454 return None;
455 }
456
457 let num_str = num.as_str();
458
459 let mut has_decimal_point = false;
461 let mut has_exponent = false;
462 for b in num_str.bytes() {
463 if b == b'.' {
464 has_decimal_point = true;
465 } else if b == b'e' || b == b'E' {
466 has_exponent = true;
467 break;
468 }
469 }
470
471 if !has_decimal_point && !has_exponent {
472 return None;
473 }
474
475 if !has_exponent {
476 return BigFraction::from_str(num_str).ok();
477 }
478
479 let components = DecimalComponents::parse(num_str)?;
480 let shift = components.decimal_shift();
481
482 if shift >= 0 {
484 return None;
485 }
486
487 if exponent_reduction_exceeds_limit(components.exponent) {
488 return None;
489 }
490
491 let denom_power = (-shift) as usize;
492 let denominator = pow10_bigint(denom_power)?;
493 let mut numerator = BigInt::from_str(&components.digits).ok()?;
494 if components.negative && !numerator.is_zero() {
495 numerator = -numerator;
496 }
497 Some(BigFraction::from(numerator) / BigFraction::from(denominator))
498 }
499
500 pub(crate) fn compare_bigint_to_limit<T>(big: &BigInt, limit: T) -> Option<std::cmp::Ordering>
506 where
507 T: Copy + ToPrimitive,
508 {
509 use std::cmp::Ordering;
510
511 let limit_f64 = limit.to_f64()?;
512 if limit_f64.fract() == 0.0 {
513 if let Some(limit_int) = limit.to_i64() {
515 return Some(big.cmp(&BigInt::from(limit_int)));
516 }
517 if let Some(limit_int) = limit.to_u64() {
518 return Some(big.cmp(&BigInt::from(limit_int)));
519 }
520 }
521 if limit_f64 == f64::INFINITY {
522 return Some(Ordering::Less);
523 }
524 if limit_f64 == f64::NEG_INFINITY {
525 return Some(Ordering::Greater);
526 }
527 None
528 }
529
530 fn limit_as_bigfraction<T>(limit: T) -> Option<BigFraction>
534 where
535 T: Copy + ToPrimitive,
536 {
537 if limit.to_f64()?.fract() != 0.0 {
538 return None;
539 }
540 if let Some(limit_int) = limit.to_i64() {
541 return Some(BigFraction::from(limit_int));
542 }
543 limit.to_u64().map(BigFraction::from)
544 }
545
546 pub(crate) fn compare_to_limit<T>(num: &Number, limit: T) -> Option<std::cmp::Ordering>
551 where
552 T: Copy + ToPrimitive,
553 {
554 if let Some(big) = try_parse_bigint(num) {
555 return compare_bigint_to_limit(&big, limit);
556 }
557 let value = try_parse_bigfraction(num)?;
558 value.partial_cmp(&limit_as_bigfraction(limit)?)
559 }
560
561 pub(crate) fn is_zero_literal(num: &Number) -> bool {
566 DecimalComponents::parse(num.as_str())
567 .is_some_and(|components| digits_are_zero(&components.digits))
568 }
569
570 macro_rules! define_bigint_cmp {
571 ($($fn_name:ident, $prim_type:ty, $to_prim:ident, $op:tt, $overflow_sign:expr);* $(;)?) => {
572 $(
573 pub fn $fn_name(bigint: &BigInt, value: $prim_type) -> bool {
574 if let Some(converted) = bigint.$to_prim() {
575 converted $op value
576 } else {
577 bigint.sign() == $overflow_sign
578 }
579 }
580 )*
581 };
582 }
583
584 define_bigint_cmp!(
585 bigint_ge_u64, u64, to_u64, >=, num_bigint::Sign::Plus;
586 bigint_le_u64, u64, to_u64, <=, num_bigint::Sign::Minus;
587 bigint_gt_u64, u64, to_u64, >, num_bigint::Sign::Plus;
588 bigint_lt_u64, u64, to_u64, <, num_bigint::Sign::Minus;
589 bigint_ge_i64, i64, to_i64, >=, num_bigint::Sign::Plus;
590 bigint_le_i64, i64, to_i64, <=, num_bigint::Sign::Minus;
591 bigint_gt_i64, i64, to_i64, >, num_bigint::Sign::Plus;
592 bigint_lt_i64, i64, to_i64, <, num_bigint::Sign::Minus;
593 bigint_ge_f64, f64, to_f64, >=, num_bigint::Sign::Plus;
594 bigint_le_f64, f64, to_f64, <=, num_bigint::Sign::Minus;
595 bigint_gt_f64, f64, to_f64, >, num_bigint::Sign::Plus;
596 bigint_lt_f64, f64, to_f64, <, num_bigint::Sign::Minus;
597 );
598
599 macro_rules! define_reverse_cmp {
601 ($($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);* $(;)?) => {
602 $(
603 pub fn $rev_ge(value: $prim_type, big: &$big_type) -> bool {
604 $fwd_le(big, value)
605 }
606
607 pub fn $rev_le(value: $prim_type, big: &$big_type) -> bool {
608 $fwd_ge(big, value)
609 }
610
611 pub fn $rev_gt(value: $prim_type, big: &$big_type) -> bool {
612 $fwd_lt(big, value)
613 }
614
615 pub fn $rev_lt(value: $prim_type, big: &$big_type) -> bool {
616 $fwd_gt(big, value)
617 }
618 )*
619 };
620 }
621
622 define_reverse_cmp!(
623 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;
624 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;
625 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;
626 );
627
628 pub fn is_multiple_of_bigint(value: &BigInt, multiple: &BigInt) -> bool {
630 if value.is_zero() {
633 return true;
634 }
635
636 (value % multiple).is_zero()
643 }
644
645 macro_rules! define_bigfraction_cmp {
647 ($($fn_name:ident, $prim_type:ty, $op:tt);* $(;)?) => {
648 $(
649 pub fn $fn_name(bigfrac: &BigFraction, value: $prim_type) -> bool {
650 let value_frac = BigFraction::from(value);
651 *bigfrac $op value_frac
652 }
653 )*
654 };
655 }
656
657 define_bigfraction_cmp!(
658 bigfrac_ge_u64, u64, >=;
659 bigfrac_le_u64, u64, <=;
660 bigfrac_gt_u64, u64, >;
661 bigfrac_lt_u64, u64, <;
662 bigfrac_ge_i64, i64, >=;
663 bigfrac_le_i64, i64, <=;
664 bigfrac_gt_i64, i64, >;
665 bigfrac_lt_i64, i64, <;
666 bigfrac_ge_f64, f64, >=;
667 bigfrac_le_f64, f64, <=;
668 bigfrac_gt_f64, f64, >;
669 bigfrac_lt_f64, f64, <;
670 );
671
672 define_reverse_cmp!(
673 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;
674 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;
675 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;
676 );
677
678 pub fn is_multiple_of_bigfrac(value: &BigFraction, multiple: &BigFraction) -> bool {
680 if value.is_zero() {
682 return true;
683 }
684 if multiple.is_zero() {
686 return false;
687 }
688 (value / multiple).denom().is_none_or(fraction::One::is_one)
691 }
692}
693
694#[cfg(all(test, feature = "arbitrary-precision"))]
695mod tests {
696 use super::bignum;
697 use fraction::BigFraction;
698 use num_bigint::BigInt;
699 use serde_json::{Number, Value};
700 use std::cmp::Ordering;
701 use test_case::test_case;
702
703 fn number_from_str(raw: &str) -> Number {
704 match serde_json::from_str::<Value>(raw).expect("valid JSON number") {
705 Value::Number(num) => num,
706 _ => unreachable!(),
707 }
708 }
709
710 #[test_case("18446744073709551616", u64::MAX, Ordering::Greater; "above u64 limit")]
711 fn compare_bigint_to_u64_limit(big: &str, limit: u64, expected: Ordering) {
712 let big = BigInt::parse_bytes(big.as_bytes(), 10).unwrap();
713 assert_eq!(bignum::compare_bigint_to_limit(&big, limit), Some(expected));
714 }
715
716 #[test_case("-18446744073709551616", i64::MIN, Ordering::Less; "below i64 limit")]
717 fn compare_bigint_to_i64_limit(big: &str, limit: i64, expected: Ordering) {
718 let big = BigInt::parse_bytes(big.as_bytes(), 10).unwrap();
719 assert_eq!(bignum::compare_bigint_to_limit(&big, limit), Some(expected));
720 }
721
722 #[test_case(f64::INFINITY, Some(Ordering::Less); "infinity limit")]
725 #[test_case(f64::NEG_INFINITY, Some(Ordering::Greater); "negative infinity limit")]
726 #[test_case(0.5, None; "no exact integer form")]
727 fn compare_bigint_to_f64_limit(limit: f64, expected: Option<Ordering>) {
728 let big = BigInt::parse_bytes(b"18446744073709551616", 10).unwrap();
729 assert_eq!(bignum::compare_bigint_to_limit(&big, limit), expected);
730 }
731
732 #[test]
733 fn bigint_parses_scientific_integer() {
734 let num = number_from_str("1e19");
735 let parsed = bignum::try_parse_bigint(&num).expect("parsed bigint");
736 assert_eq!(
737 parsed,
738 BigInt::parse_bytes(b"10000000000000000000", 10).unwrap()
739 );
740 }
741
742 #[test]
743 fn bigint_rejects_non_integer_scientific() {
744 let num = number_from_str("1.25e1");
745 assert!(bignum::try_parse_bigint(&num).is_none());
746 }
747
748 #[test]
749 fn bigfraction_parses_scientific_decimal() {
750 let num = number_from_str("1.5e-5");
751 let parsed = bignum::try_parse_bigfraction(&num).expect("parsed bigfraction");
752 let expected =
753 BigFraction::from(BigInt::from(3)) / BigFraction::from(BigInt::from(200_000));
754 assert_eq!(parsed, expected);
755 }
756
757 #[test]
758 fn bigfraction_skips_scientific_integer() {
759 let num = number_from_str("3e4");
760 assert!(bignum::try_parse_bigfraction(&num).is_none());
761 }
762}
763
764#[cfg(all(test, feature = "arbitrary-precision"))]
765mod exact_multiple_of_tests {
766 use super::is_multiple_of_integer;
767 use serde_json::{Number, Value};
768 use test_case::test_case;
769
770 fn number(raw: &str) -> Number {
771 match serde_json::from_str::<Value>(raw).expect("valid JSON number") {
772 Value::Number(num) => num,
773 _ => unreachable!(),
774 }
775 }
776
777 #[test_case("135107988821114880000000000000", 3.0, true; "multiple of three")]
780 #[test_case("135107988821114880000000000001", 3.0, false; "one past a multiple of three")]
781 #[test_case("135107988821114880000000000002", 3.0, false; "two past a multiple of three")]
782 #[test_case("18446744073709551617", 2.0, false; "odd just past u64")]
783 #[test_case("18446744073709551618", 2.0, true; "even just past u64")]
784 #[test_case("1e30", 3.0, false; "scientific not a multiple")]
785 #[test_case("1e30", 2.0, true; "scientific is a multiple")]
786 fn exact_beyond_u64(value: &str, divisor: f64, expected: bool) {
787 assert_eq!(is_multiple_of_integer(&number(value), divisor), expected);
788 }
789}