Repository: paupino/rust-decimal (Rust)

Issue:
Feature request: `Decimal::is_integer()`
I recently integrated `rust_decimal::Decimal` into a large codebase and found that most of the API was intuitive and more complete than that of `bigdecimal::BigDecimal`. One thing I missed is the [`.is_integer()` function](https://docs.rs/bigdecimal/latest/bigdecimal/struct.BigDecimal.html#method.is_integer). I (and the internals of this crate) end up writing `.scale() == 0` everywhere, which is far less intuitive (and possible less correct given the possibility of non-normalized decimals?) than `.is_integer()`.


Discussion:
`.scale() == 0` misses the cases where `.scale() > 0` but the raw number itself has zeros at the end.  Such as: `1000, scale=3` is equivalent to `1, scale=0`.
> `.scale() == 0` misses the cases where `.scale() > 0` but the raw number itself has zeros at the end. Such as: `1000, scale=3` is equivalent to `1, scale=0`.

Thanks, that's what I was worried about when I said `scale() == 0` might be *possibly* less correct. I think that's another reason to add a (correct) `is_integer` function.
Fortunately, it's a fairly easy function to implement so I've put something together in the attached PR. 

> I (and the internals of this crate) end up writing .scale() == 0 everywhere

Rest assured, we don't use scale to identify an integer like this within the crate. When we check for scale being zero we're actually intending to check that the mantissa is a whole number without any "manipulation" required (e.g., we may want to transform `1.000` into `1`).



Patch:
diff --git a/src/decimal.rs b/src/decimal.rs
index 2a979e37..8730d2c8 100644
--- a/src/decimal.rs
+++ b/src/decimal.rs
@@ -742,6 +742,46 @@ impl Decimal {
         self.lo == 0 && self.mid == 0 && self.hi == 0
     }
 
+    /// Returns true if this Decimal number has zero fractional part (is equal to an integer)
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// # use rust_decimal::prelude::*;
+    /// # use rust_decimal_macros::dec;
+    /// #
+    /// assert_eq!(dec!(5).is_integer(), true);
+    /// // Trailing zeros are also ignored
+    /// assert_eq!(dec!(5.0000).is_integer(), true);
+    /// // If there is a fractional part then it is not an integer
+    /// assert_eq!(dec!(5.1).is_integer(), false);
+    /// ```
+    #[must_use]
+    pub fn is_integer(&self) -> bool {
+        let scale = self.scale();
+        if scale == 0 || self.is_zero() {
+            return true;
+        }
+
+        // Check if it can be divided by 10^scale without remainder
+        let mut bits = self.mantissa_array3();
+        let mut scale = scale;
+        while scale > 0 {
+            let remainder = if scale > 9 {
+                scale -= 10;
+                ops::array::div_by_u32(&mut bits, POWERS_10[9])
+            } else {
+                let power = POWERS_10[scale as usize];
+                scale = 0;
+                ops::array::div_by_u32(&mut bits, power)
+            };
+            if remainder > 0 {
+                return false;
+            }
+        }
+        true
+    }
+
     /// An optimized method for changing the sign of a decimal number.
     ///
     /// # Arguments
@@ -939,9 +979,9 @@ impl Decimal {
         if raw.scale() > MAX_PRECISION_U32 {
             let mut bits = raw.mantissa_array3();
             let remainder = match raw.scale() {
-                29 => crate::ops::array::div_by_1x(&mut bits, 1),
-                30 => crate::ops::array::div_by_1x(&mut bits, 2),
-                31 => crate::ops::array::div_by_1x(&mut bits, 3),
+                29 => ops::array::div_by_power::<1>(&mut bits),
+                30 => ops::array::div_by_power::<2>(&mut bits),
+                31 => ops::array::div_by_power::<3>(&mut bits),
                 _ => 0,
             };
             if remainder >= 5 {
diff --git a/src/ops/array.rs b/src/ops/array.rs
index e9213a7d..e058c983 100644
--- a/src/ops/array.rs
+++ b/src/ops/array.rs
@@ -260,9 +260,11 @@ pub(crate) fn div_by_u32<const N: usize>(bits: &mut [u32; N], divisor: u32) -> u
     }
 }
 
-pub(crate) fn div_by_1x(bits: &mut [u32; 3], power: usize) -> u32 {
+// This function should be used with caution. It unwraps the standard divide loop - it is intended
+// for small inputs (<10) and is optimized to be left as unchecked.
+pub(crate) fn div_by_power<const POWER: usize>(bits: &mut [u32; 3]) -> u32 {
     let mut remainder = 0u32;
-    let divisor = POWERS_10[power] as u64;
+    let divisor = POWERS_10[POWER] as u64;
     let temp = ((remainder as u64) << 32) + (bits[2] as u64);
     remainder = (temp % divisor) as u32;
     bits[2] = (temp / divisor) as u32;
@@ -315,13 +317,13 @@ mod test {
     use super::*;
     use crate::prelude::*;
 
+    fn to_mantissa_array_with_scale(value: &str) -> ([u32; 3], u32) {
+        let v = Decimal::from_str(value).unwrap();
+        (v.mantissa_array3(), v.scale())
+    }
+
     #[test]
     fn it_can_rescale_internal() {
-        fn extract(value: &str) -> ([u32; 3], u32) {
-            let v = Decimal::from_str(value).unwrap();
-            (v.mantissa_array3(), v.scale())
-        }
-
         let tests = &[
             ("1", 0, "1", 0),
             ("1", 1, "1.0", 1),
@@ -350,8 +352,8 @@ mod test {
         ];
 
         for &(value_raw, new_scale, expected_value, expected_scale) in tests {
-            let (expected_value, _) = extract(expected_value);
-            let (mut value, mut value_scale) = extract(value_raw);
+            let (expected_value, _) = to_mantissa_array_with_scale(expected_value);
+            let (mut value, mut value_scale) = to_mantissa_array_with_scale(value_raw);
             rescale_internal(&mut value, &mut value_scale, new_scale);
             assert_eq!(value, expected_value);
             assert_eq!(


Test patch:
diff --git a/tests/decimal_tests.rs b/tests/decimal_tests.rs
index 6b2a6501..d7c3c380 100644
--- a/tests/decimal_tests.rs
+++ b/tests/decimal_tests.rs
@@ -3617,6 +3617,23 @@ fn test_inv() {
     assert_eq!("0.01", Decimal::ONE_HUNDRED.inv().to_string());
 }
 
+#[test]
+fn test_is_integer() {
+    let tests = &[
+        ("0", true),
+        ("1", true),
+        ("79_228_162_514_264_337_593_543_950_335", true),
+        ("1.0", true),
+        ("1.1", false),
+        ("3.1415926535897932384626433833", false),
+        ("3.0000000000000000000000000000", true),
+    ];
+    for &(raw, integer) in tests {
+        let value = Decimal::from_str(raw).unwrap();
+        assert_eq!(value.is_integer(), integer, "value: {}", raw)
+    }
+}
+
 // Mathematical features
 #[cfg(feature = "maths")]
 mod maths {