Repository: h2oai/wave
File: ui/src/spinbox.tsx (TypeScript)

Code before:

function calculatePrecision(value: number | string): number {
  /**
   * Group 1:
   * [1-9]([0]+$) matches trailing zeros
   * Group 2:
   * \.([0-9]*) matches all digits after a decimal point.
   */
  const groups = /[1-9]([0]+$)|\.([0-9]*)/.exec(String(value))
  if (!groups) {
    return 0
  }
  if (groups[1]) {
    return -groups[1].length
  }
  if (groups[2]) {
    return groups[2].length
  }
  return 0
}

function precisionRound(value: number, precision: number, base = 10): number {
  const exp = Math.pow(base, precision)
  return Math.round(value * exp) / exp
}

export const
  XSpinbox = ({ model: { name, trigger, label, disabled, min = 0, max = 100, step = 1, value = 0 } }: { model: Spinbox }) => {
    const
      precision = Math.max(calculatePrecision(step), 0),
      [val, setVal] = React.useState<{ val?: S }>(), // Use primitive wrapper to always force a React update.
      parseValue = (v: F) => {
        return (!isNaN(v) && isFinite(v)) ? v : value
      },
      onIncrement = (v: S) => {
        let newValue = clampValue(Number(v) + Number(step), { max })
        newValue = precisionRound(newValue, precision)
        wave.args[name] = newValue
        if (trigger) wave.push()
        return String(newValue)
      },
      onDecrement = (v: S) => {
        let newValue = clampValue(Number(v) - Number(step), { min })
        newValue = precisionRound(newValue, precision)
        wave.args[name] = newValue
        if (trigger) wave.push()
        return String(newValue)
      },
      handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        // If user entered "." or 0 as the last number after "." then don't parse
        if (/\.$|\.\d*0+$/.test(e.target.value)) return

Review comment (bug):
Is there any semantic difference when compared with original code? This is just a straight verbose copy-paste if I see correctly (correct me if I am wrong).

What I meant by _let's see how Fluent handles this_ I meant the input handling and showing the value, precision rounding stuff is already implemented as they do.

Code after:
     * Group 2:
     * \.([0-9]*) matches all digits after a decimal point.
     */
    const groups = /[1-9]([0]+$)|\.([0-9]*)/.exec(String(value))
    if (!groups) return 0
    return -groups[1]?.length || groups[2]?.length || 0
  }
export const
  XSpinbox = ({ model: { name, trigger, label, disabled, min = 0, max = 100, step = 1, value = 0 } }: { model: Spinbox }) => {
    const
      [val, setVal] = React.useState<{ val?: S }>(), // Use primitive wrapper to always force a React update.
      precision = Math.max(calculatePrecision(step), 0),
      parseValue = (v: F) => {
        const x = precisionRound(v, precision)
        return (!isNaN(x) && isFinite(x)) ? x : value
      },
      onIncrement = (v: S) => {
        const newValue = Math.min(parseValue(Number(v) + step), max)
        wave.args[name] = newValue
        if (trigger) wave.push()
        setVal({ val: String(newValue) })
        return String(newValue)
      },
      onDecrement = (v: S) => {
        const newValue = Math.max(parseValue(Number(v) - step), min)
        wave.args[name] = newValue
        if (trigger) wave.push()
        setVal({ val: String(newValue) })
        return String(newValue)
      },
      onBlur = () => setVal({ ...val }), //HACK: Fluent bug in v7, remove after upgrade.
      handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const
          val = e.target.value,
          isLastCharDotOrTraillingZero = /\.$|\.\d*0+$/,
          value = parseValue(Number(val)),
          newValue = value > max
            ? max
            : value < min
              ? min
              : value
        wave.args[name] = newValue
        if (trigger) wave.push()
        if (precision > 0 && isLastCharDotOrTraillingZero.test(val)) {
          // We can't use parseValue because it requires casting to number which will remove the trailling zeros.
          const [head, tail = ''] = val.split('.')
          setVal({ val: `${head}.${tail.slice(0, precision)}` })
        } else {
          setVal({ val: String(newValue) })
        }
      },