Repository: remeda/remeda (ts)

Issue:
The negative precision `floor` returns floating point result
Hey there 👋 

Thank you for developing such a nice library!

During development I faced an unexpected result caused by the `floor` function when applied with the negative precision:

```ts
expects(floor(123456, -5)).toBe(100000)
// actual is 100000.00000000001
```

I believe the rootcause is somewhere in the [`internal/withPrecision.ts`](https://github.com/remeda/remeda/blob/main/src/internal/withPrecision.ts) file during division operation.


Pull request description:
fix(floor, ceil, round): do not divide and multiply by a floating point number
The changes aim to increase the accuracy of the `floor`, `cail`, and `round` functions by using only integers when dividing and multiplying, avoiding floating point numbers.

Resolves #1003 


---

Make sure that you:

- [ ] ~Typedoc added for new methods and updated for changed~
- [x] Tests added for new methods and updated for changed
- [ ] ~New methods added to `src/index.ts`~
- [ ] ~New methods added to `docs/src/content/mapping` for both Lodash and Ramda.~

---

<details><summary>We use semantic PR titles to automate the release process!</summary>

https://conventionalcommits.org

PRs should be titled following using the format: `< TYPE >(< scope >)?: description`

### Available Types:

- `feat`: new functions, and changes to a function's type that would impact users.
- `fix`: changes to the runtime behavior of an existing function, or refinements to it's type that shouldn't impact most users.
- `test`: tests-only changes (transparent to users of the function).
- `docs`: changes to the documentation of a function **or the documentation site**.
- `build`, `ci`, `chore`, and `revert`: are only relevant for the internals of the library.

For scope put the name of the function you are working on (either new or
existing).

</details>


Patch:
diff --git a/src/internal/withPrecision.ts b/src/internal/withPrecision.ts
index f289cce..fc1a8b9 100644
--- a/src/internal/withPrecision.ts
+++ b/src/internal/withPrecision.ts
@@ -26,7 +26,13 @@ export function withPrecision(roundingFn: (value: number) => number) {
       return roundingFn(value);
     }
 
-    const multiplier = RADIX ** precision;
-    return roundingFn(value * multiplier) / multiplier;
+    if (precision > 0) {
+      const multiplier = RADIX ** precision;
+      return roundingFn(value * multiplier) / multiplier;
+    }
+
+    // Avoid losing precision by dividing first.
+    const divisor = RADIX ** -precision;
+    return roundingFn(value / divisor) * divisor;
   };
 }


Test patch:
diff --git a/src/ceil.test.ts b/src/ceil.test.ts
index 318cdae..7a59f94 100644
--- a/src/ceil.test.ts
+++ b/src/ceil.test.ts
@@ -12,6 +12,11 @@ describe("data-first", () => {
     expect(ceil(8123.4317, -4)).toBe(10_000);
   });
 
+  // Makes sure the issue github.com/remeda/remeda/issues/1003 doesn't happen.
+  test("does not output floating point number for negative precision", () => {
+    expect(ceil(123_456, -5)).toBe(200_000);
+  });
+
   test("should work with precision = 0", () => {
     expect(ceil(8123.4317, 0)).toBe(8124);
   });
diff --git a/src/floor.test.ts b/src/floor.test.ts
index cbdaa2e..f026757 100644
--- a/src/floor.test.ts
+++ b/src/floor.test.ts
@@ -12,6 +12,11 @@ describe("data-first", () => {
     expect(floor(8123.4317, -4)).toBe(0);
   });
 
+  // Makes sure the issue github.com/remeda/remeda/issues/1003 doesn't happen.
+  test("does not output floating point number for negative precision", () => {
+    expect(floor(123_456, -5)).toBe(100_000);
+  });
+
   test("should work with precision = 0", () => {
     expect(floor(8123.4317, 0)).toBe(8123);
   });
diff --git a/src/round.test.ts b/src/round.test.ts
index c486fe8..7a5425b 100644
--- a/src/round.test.ts
+++ b/src/round.test.ts
@@ -12,6 +12,11 @@ describe("data-first", () => {
     expect(round(8123.4317, -4)).toBe(10_000);
   });
 
+  // Makes sure the issue github.com/remeda/remeda/issues/1003 doesn't happen.
+  test("does not output floating point number for negative precision", () => {
+    expect(round(123_456, -5)).toBe(100_000);
+  });
+
   test("should work with precision = 0", () => {
     expect(round(8123.4317, 0)).toBe(8123);
   });