Repository: mui/material-ui (TypeScript)
Date: 2019-06-15 18:25:45+00:00
Task type: Bug Fix

Issue:
Slider stepValue < 1 gives rounding errors
<!-- Checked checkbox should look like this: [x] -->
- [x ] This is not a v0.x issue. <!-- (v0.x is no longer maintained) -->
- [x ] I have searched the [issues](https://github.com/mui-org/material-ui/issues) of this repository and believe that this is not a duplicate.

## Expected Behavior 🤔
Setting my slider with a stepValue < 1 (like 0.1) should only give values that are multiplication of the stepValue

## Current Behavior 😯
Some stepValues have rounding errors like 0.7000000000000001

## Steps to Reproduce 🕹

See https://codesandbox.io/s/create-react-app-ehgqw


## Your Environment 🌎
Windows 10 18.03
Node 8.15.1

| Tech         | Version |
|--------------|---------|
| Material-UI  | v4.1.0  |
| React        |  16.8.6       |
| Browser      | latest Chrome and Firefox      |
| TypeScript   |   -      |
| etc.         |         |



Issue discussion:
This is due to the way javascript [stores decimal numbers](https://modernweb.com/what-every-javascript-developer-should-know-about-floating-points/)

If you go to your console right now and type 1.1 * 0.1 it will print 0.11000000000000001.

To get around this you can change the slider `valueReducer` passing a custom one by prop.

Although i believe this is something that should be fixed in the core.

I was able to fix it in the following diff by detecting the amount of decimal places `step` has and rounding the final result to that number of decimal places.

```diff --git a/packages/material-ui-lab/src/Slider/Slider.js b/packages/material-ui-lab/src/Slider/Slider.js
index 15ae09f3e..03111aa54 100644
--- a/packages/material-ui-lab/src/Slider/Slider.js
+++ b/packages/material-ui-lab/src/Slider/Slider.js
@@ -171,8 +171,17 @@ function percentToValue(percent, min, max) {
   return ((max - min) * percent) / 100 + min;
 }
 
+function getDecimalPlaces(number) {
+  const numberString = number.toString();
+  const dotIndex = numberString.indexOf('.');
+
+  return dotIndex === -1 ? 0 : numberString.length - dotIndex - 1;
+}
+
 function roundToStep(number, step) {
-  return Math.round(number / step) * step;
+  const value = Math.round(number / step) * step;
+
+  return Number(value.toFixed(getDecimalPlaces(step)));
 }
```

Do you guys feel this is a good approach? :thinking: 
@rejas Could you explain how it's a problem in your application?
@joaosilvalopes The slider is being reworked in #15703.
@oliviertassinari it isnt a critical problem since I can do the rounding indeed myself before displaying it or using the value somewhere else. Its just some unexpected stuff...
I think it’s probably okay for this to be fixed in core, although I’m wondering if this creates unexpected JS behaviour 
What determines the precision we return? Assuming I use `1/3` as a step value. How is the fix going to look like?
In [`rc-slider`](https://www.npmjs.com/package/rc-slider) they always round it to step precision as well which i believe is the best approach to cover non-repeating decimals as step use cases.

For repeating decimals like `1/3` i don't think there is anything we can do other than rounding it to all the decimal places `1/3` has.
> In [`rc-slider`](https://www.npmjs.com/package/rc-slider) they always round it to step precision as well which i believe is the best approach to cover non-repeating decimals as step use cases.
> 
> For repeating decimals like `1/3` i don't think there is anything we can do other than rounding it to all the decimal places `1/3` has.

Context: https://github.com/react-component/slider/blob/master/src/utils.js#L73
This sounds like a good idea then. To be explored after #15703.
Can somebody have a look at what the solution will look like since #15703 was merged?
@oliviertassinari I can start analyzing possible solutions over this weekend, i will share my updates on this thread.
@joaosilvalopes Awesome!
```diff --git a/packages/material-ui-lab/src/Slider/Slider.js b/packages/material-ui-lab/src/Slider/Slider.js
index 9fde8f282..309161623 100644
--- a/packages/material-ui-lab/src/Slider/Slider.js
+++ b/packages/material-ui-lab/src/Slider/Slider.js
@@ -67,8 +67,15 @@ function percentToValue(percent, min, max) {
   return (max - min) * percent + min;
 }
 
+function ensurePrecision(value, step) {
+  const stepDecimalPart = step.toString().split('.')[1];
+  const stepPrecision = stepDecimalPart ? stepDecimalPart.length : 0;
+
+  return Number(value.toFixed(stepPrecision));
+}
+
 function roundValueToStep(value, step) {
-  return Math.round(value / step) * step;
+  return ensurePrecision(Math.round(value / step) * step, step);
 }
```

This solution seems to do the job, i have been looking through the `Slider` code trying to find any other possible causes of floating point errors that would reflect on the slider's value but couldn't seem to find any.

If you guys feel this is an appropriate approach i will write some tests and open a pr right away :smile:
The logic looks great to me. Is this something we can write a test for?
I wrote a simple one

```diff --git a/packages/material-ui-lab/src/Slider/Slider.test.js b/packages/material-ui-lab/src/Slider/Slider.test.js
index 75b9ae43d..3c780bb8d 100644
--- a/packages/material-ui-lab/src/Slider/Slider.test.js
+++ b/packages/material-ui-lab/src/Slider/Slider.test.js
@@ -265,6 +265,13 @@ describe('<Slider />', () => {
       thumb.simulate('keydown', moveRightEvent);
       assert.strictEqual(wrapper.props().value, 30);
     });
+
+    it('should round value to step precision', () => {
+      wrapper.setProps({ value: 0.2, step: 0.1, min: 0 });
+      const thumb = findThumb(wrapper);
+      thumb.simulate('keydown', moveRightEvent);
+      assert.strictEqual(wrapper.props().value, 0.3);
+    });
   });
 
   describe('markActive state', () => {
```

Which throws `AssertionError: expected 0.30000000000000004 to equal 0.3` before the fix.

Do you feel this test adds any value?
I think that it does. We don't want to see a regression.

Solution patch:
diff --git a/packages/material-ui-lab/src/Slider/Slider.js b/packages/material-ui-lab/src/Slider/Slider.js
--- a/packages/material-ui-lab/src/Slider/Slider.js
+++ b/packages/material-ui-lab/src/Slider/Slider.js
@@ -67,8 +67,15 @@ function percentToValue(percent, min, max) {
   return (max - min) * percent + min;
 }
 
+function ensurePrecision(value, step) {
+  const stepDecimalPart = step.toString().split('.')[1];
+  const stepPrecision = stepDecimalPart ? stepDecimalPart.length : 0;
+
+  return Number(value.toFixed(stepPrecision));
+}
+
 function roundValueToStep(value, step) {
-  return Math.round(value / step) * step;
+  return ensurePrecision(Math.round(value / step) * step, step);
 }
 
 function setValueIndex({ values, source, newValue, index }) {


Test patch:
diff --git a/packages/material-ui-lab/src/Slider/Slider.test.js b/packages/material-ui-lab/src/Slider/Slider.test.js
--- a/packages/material-ui-lab/src/Slider/Slider.test.js
+++ b/packages/material-ui-lab/src/Slider/Slider.test.js
@@ -265,6 +265,13 @@ describe('<Slider />', () => {
       thumb.simulate('keydown', moveRightEvent);
       assert.strictEqual(wrapper.props().value, 30);
     });
+
+    it('should round value to step precision', () => {
+      wrapper.setProps({ value: 0.2, step: 0.1, min: 0 });
+      const thumb = findThumb(wrapper);
+      thumb.simulate('keydown', moveRightEvent);
+      assert.strictEqual(wrapper.props().value, 0.3);
+    });
   });
 
   describe('markActive state', () => {