diff --git a/src/components/fields/Percentage/DisplayCell.tsx b/src/components/fields/Percentage/DisplayCell.tsx index 532103e4..a6e5700a 100644 --- a/src/components/fields/Percentage/DisplayCell.tsx +++ b/src/components/fields/Percentage/DisplayCell.tsx @@ -2,6 +2,7 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { useTheme } from "@mui/material"; import { resultColorsScale } from "@src/utils/color"; +import { multiply100WithPrecision } from "./utils"; export default function Percentage({ column, value }: IDisplayCellProps) { const theme = useTheme(); @@ -34,7 +35,7 @@ export default function Percentage({ column, value }: IDisplayCellProps) { zIndex: 1, }} > - {Math.round(percentage * 100)}% + {multiply100WithPrecision(percentage)}% ); diff --git a/src/components/fields/Percentage/EditorCell.tsx b/src/components/fields/Percentage/EditorCell.tsx index 1e3a7e96..54247f55 100644 --- a/src/components/fields/Percentage/EditorCell.tsx +++ b/src/components/fields/Percentage/EditorCell.tsx @@ -1,13 +1,20 @@ import type { IEditorCellProps } from "@src/components/fields/types"; import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; +import { multiply100WithPrecision, divide100WithPrecision } from "./utils"; export default function Percentage(props: IEditorCellProps) { return ( props.onChange(Number(v) / 100)} + value={ + typeof props.value === "number" + ? multiply100WithPrecision(props.value) + : props.value + } + onChange={(v) => { + props.onChange(divide100WithPrecision(Number(v))); + }} /> ); } diff --git a/src/components/fields/Percentage/Settings.tsx b/src/components/fields/Percentage/Settings.tsx index e28dde2c..b2552fc2 100644 --- a/src/components/fields/Percentage/Settings.tsx +++ b/src/components/fields/Percentage/Settings.tsx @@ -16,6 +16,7 @@ import { ISettingsProps } from "@src/components/fields/types"; import { Color, toColor } from "react-color-palette"; import { fieldSx } from "@src/components/SideDrawer/utils"; import { resultColorsScale, defaultColors } from "@src/utils/color"; +import { multiply100WithPrecision } from "./utils"; const colorLabels: { [key: string]: string } = { 0: "Start", @@ -160,7 +161,7 @@ const Preview = ({ colors }: { colors: any }) => { }} /> - {Math.floor(value * 100)}% + {multiply100WithPrecision(value)}% ); diff --git a/src/components/fields/Percentage/SideDrawerField.tsx b/src/components/fields/Percentage/SideDrawerField.tsx index a6cc4366..feaf5673 100644 --- a/src/components/fields/Percentage/SideDrawerField.tsx +++ b/src/components/fields/Percentage/SideDrawerField.tsx @@ -3,6 +3,7 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types"; import { TextField, InputAdornment, Box, useTheme } from "@mui/material"; import { resultColorsScale } from "@src/utils/color"; import { getFieldId } from "@src/components/SideDrawer/utils"; +import { multiply100WithPrecision } from "./utils"; export default function Percentage({ column, @@ -20,7 +21,9 @@ export default function Percentage({ margin="none" onChange={(e) => onChange(Number(e.target.value) / 100)} onBlur={onSubmit} - value={typeof value === "number" ? value * 100 : value} + value={ + typeof value === "number" ? multiply100WithPrecision(value) : value + } id={getFieldId(column.key)} label="" hiddenLabel diff --git a/src/components/fields/Percentage/utils.ts b/src/components/fields/Percentage/utils.ts new file mode 100644 index 00000000..f9e4ee25 --- /dev/null +++ b/src/components/fields/Percentage/utils.ts @@ -0,0 +1,126 @@ +import { trim, trimEnd } from "lodash-es"; + +/** + * Multiply a number by 100 and return a string without floating point error + * by shifting the decimal point 2 places to the right as a string + * e.g. floating point error: 0.07 * 100 === 7.000000000000001 + * + * A few examples: + * + * let number = 0.07; + * console.log(number, multiply100WithPrecision(number)); + * --> 7 + * + * number = 0; + * console.log(number, multiply100WithPrecision(number)); + * --> 0 + * + * number = 0.1; + * console.log(number, multiply100WithPrecision(number)); + * --> 10 + * + * number = 0.001; + * console.log(number, multiply100WithPrecision(number)); + * --> 0.1 + * + * number = 0.00001; + * console.log(number, multiply100WithPrecision(number)); + * --> 0.001 + * + * number = 100; + * console.log(number, multiply100WithPrecision(number)); + * --> 10000 + * + * number = 1999.99; + * console.log(number, multiply100WithPrecision(number)); + * --> 199999 + * + * number = 1999.999; + * console.log(number, multiply100WithPrecision(number)); + * --> 199999.9 + * + * number = 0.25; + * console.log(number, multiply100WithPrecision(number)); + * --> 25 + * + * number = 0.15; + * console.log(number, multiply100WithPrecision(number)); + * --> 15 + * + * number = 1.23456789; + * console.log(number, multiply100WithPrecision(number)); + * --> 123.456789 + * + * number = 0.0000000001; + * console.log(number, multiply100WithPrecision(number)); + * --> 1e-8 + */ +export const multiply100WithPrecision = (value: number) => { + if (value === 0) { + return 0; + } + + let valueString = value.toString(); + + // e.g 1e-10 becomes 1e-8 + if (valueString.includes("e")) { + return value * 100; + } + + // if the number is integer, add .00 + if (!valueString.includes(".")) { + valueString = valueString.concat(".00"); + } + + let [before, after] = valueString.split("."); + + // if after decimal has only 1 digit, pad a 0 + if (after.length === 1) { + after = after.concat("0"); + } + + let newNumber = `${before}${after.slice(0, 2)}.${after.slice(2)}`; + newNumber = trimEnd(trim(newNumber, "0"), "."); + if (newNumber.startsWith(".")) { + newNumber = "0" + newNumber; + } + return Number(newNumber); +}; + +/** + * Divide a number by 100 and return a string without floating point error + * by shifting the decimal point 2 places to the left as a string + */ +export const divide100WithPrecision = (value: number) => { + if (value === 0) { + return 0; + } + + let valueString = value.toString(); + + // e.g 1e-10 becomes 1e-8 + if (valueString.includes("e")) { + return value / 100; + } + + // add decimal if integer + if (!valueString.includes(".")) { + valueString = valueString + "."; + } + + let [before, after] = valueString.split("."); + + // if before decimal has less than digit, pad 0 + if (before.length < 2) { + before = "00" + before; + } + + let newNumber = `${before.slice(0, before.length - 2)}.${before.slice( + before.length - 2 + )}${after}`; + newNumber = trimEnd(trimEnd(newNumber, "0"), "."); + if (newNumber.startsWith(".")) { + newNumber = "0" + newNumber; + } + return Number(newNumber); +};