Repository: mui/material-ui (TypeScript)
Date: 2019-08-15 15:22:09+00:00
Task type: Bug Fix

Issue:
@material-ui/lab/Rating - wrong rendering for value of 3.9 and precision of 0.2
Like the title says, when you have a precision of 0.2 and value of 3.9, there are only 3 starts filled instead of 4. 

The following code:
```jsx
import React from 'react';
import Rating from '@material-ui/lab/Rating';

export default function HalfRating() {
  return (
    <div>
      <Rating name="half-rating-1" value={3.6} precision={0.2} />
      <Rating name="half-rating-2" value={3.9} precision={0.2} />
    </div>
  );
}

```
Produces:
![image](https://user-images.githubusercontent.com/5186871/62740352-3b0bbc00-b9fd-11e9-8c46-2ac5eb4bcd15.png)

First one is supposed to be less stars than second one.

Example here:
https://codesandbox.io/s/material-demo-f25jl?fontsize=14



Issue discussion:
@neupsh Thank you for the report. You are right, I have designed/tested the component with two precisions values: 1 and 0.5. I haven't looked much at the 0.x variations. But it's definitely something worth supporting. We had to solve a similar issue in the past with the slider component. I believe that we can apply the same approach:

```diff
diff --git a/packages/material-ui-lab/src/Rating/Rating.js b/packages/material-ui-lab/src/Rating/Rating.js
index 26230b975..fd17dbd86 100644
--- a/packages/material-ui-lab/src/Rating/Rating.js
+++ b/packages/material-ui-lab/src/Rating/Rating.js
@@ -15,6 +15,16 @@ function clamp(value, min, max) {
   return value;
 }

+function getDecimalPrecision(num) {
+  const decimalPart = num.toString().split('.')[1];
+  return decimalPart ? decimalPart.length : 0;
+}
+
+function roundValueToPrecision(value, precision) {
+  const nearest = Math.round(value / precision) * precision;
+  return Number(nearest.toFixed(getDecimalPrecision(precision)));
+}
+
 export const styles = theme => ({
   /* Styles applied to the root element. */
   root: {
@@ -143,7 +153,7 @@ const Rating = React.forwardRef(function Rating(props, ref) {
     focus: -1,
   });

-  let value = valueProp;
+  let value = roundValueToPrecision(valueProp, precision);
   if (hover !== -1) {
     value = hover;
   }
@@ -174,7 +184,7 @@ const Rating = React.forwardRef(function Rating(props, ref) {
       percent = (event.pageX - left - ownerWindow(rootNode).pageXOffset) / (width * max);
     }

-    let newHover = Math.ceil((max * percent) / precision) * precision;
+    let newHover = roundValueToPrecision(max * percent, precision);
     newHover = clamp(newHover, precision, max);
     setState(prev =>
       prev.hover === newHover && prev.focus === newHover
@@ -380,7 +390,7 @@ const Rating = React.forwardRef(function Rating(props, ref) {
             filled: itemValue <= value,
             hover: itemValue <= hover,
             focus: itemValue <= focus,
-            checked: itemValue === Math.round(valueProp),
+            checked: itemValue === valueProp,
           },
         );
       })}
```

What do you think of these changes? Do you want to submit a pull request? :)
Thanks @oliviertassinari , I will try to create a pull request.

Solution patch:
diff --git a/packages/material-ui-lab/src/Rating/Rating.js b/packages/material-ui-lab/src/Rating/Rating.js
--- a/packages/material-ui-lab/src/Rating/Rating.js
+++ b/packages/material-ui-lab/src/Rating/Rating.js
@@ -15,6 +15,16 @@ function clamp(value, min, max) {
   return value;
 }
 
+function getDecimalPrecision(num) {
+  const decimalPart = num.toString().split('.')[1];
+  return decimalPart ? decimalPart.length : 0;
+}
+
+function roundValueToPrecision(value, precision) {
+  const nearest = Math.round(value / precision) * precision;
+  return Number(nearest.toFixed(getDecimalPrecision(precision)));
+}
+
 export const styles = theme => ({
   /* Styles applied to the root element. */
   root: {
@@ -133,10 +143,11 @@ const Rating = React.forwardRef(function Rating(props, ref) {
     precision = 1,
     readOnly = false,
     size = 'medium',
-    value: valueProp = null,
+    value: valueProp2 = null,
     ...other
   } = props;
 
+  const valueProp = roundValueToPrecision(valueProp2, precision);
   const theme = useTheme();
   const [{ hover, focus }, setState] = React.useState({
     hover: -1,
@@ -174,7 +185,7 @@ const Rating = React.forwardRef(function Rating(props, ref) {
       percent = (event.pageX - left - ownerWindow(rootNode).pageXOffset) / (width * max);
     }
 
-    let newHover = Math.ceil((max * percent) / precision) * precision;
+    let newHover = roundValueToPrecision(max * percent, precision);
     newHover = clamp(newHover, precision, max);
     setState(prev =>
       prev.hover === newHover && prev.focus === newHover
@@ -341,7 +352,10 @@ const Rating = React.forwardRef(function Rating(props, ref) {
               })}
             >
               {items.map(($, indexDecimal) => {
-                const itemDeciamlValue = itemValue - 1 + (indexDecimal + 1) * precision;
+                const itemDeciamlValue = roundValueToPrecision(
+                  itemValue - 1 + (indexDecimal + 1) * precision,
+                  precision,
+                );
 
                 return item(
                   {
@@ -380,7 +394,7 @@ const Rating = React.forwardRef(function Rating(props, ref) {
             filled: itemValue <= value,
             hover: itemValue <= hover,
             focus: itemValue <= focus,
-            checked: itemValue === Math.round(valueProp),
+            checked: itemValue === valueProp,
           },
         );
       })}


Test patch:
diff --git a/packages/material-ui-lab/src/Rating/Rating.test.js b/packages/material-ui-lab/src/Rating/Rating.test.js
--- a/packages/material-ui-lab/src/Rating/Rating.test.js
+++ b/packages/material-ui-lab/src/Rating/Rating.test.js
@@ -10,6 +10,7 @@ describe('<Rating />', () => {
   const render = createClientRender({ strict: true });
   let classes;
   const defaultProps = {
+    name: 'rating-test',
     value: 2,
   };
 
@@ -39,4 +40,14 @@ describe('<Rating />', () => {
 
     expect(container.firstChild).to.have.class(classes.root);
   });
+
+  it('should round the value to the provided precision', () => {
+    const { container, getByLabelText } = render(
+      <Rating {...defaultProps} value={3.9} precision={0.2} />,
+    );
+    const input = getByLabelText('4 Stars');
+    const checked = container.querySelector('input[name="rating-test"]:checked');
+    expect(input).to.equal(checked);
+    expect(input.value).to.equal('4');
+  });
 });