Skip to content

Number helpers ​

Seven helpers for the arithmetic an admin screen actually does, each guarding the case that would otherwise reach the DOM as NaN.

Open in demo
Open in demo

Import ​

ts
import { clamp, formatBytes, percentage, randomInt, round } from '@shared/utils/number'
import { onNumericKeydown, onNumericPaste } from '@shared/utils/numericInput'

clamp ​

ts
function clamp(value: number, min: number, max: number): number
ts
clamp(120, 0, 100) // 100

min > max yields min rather than throwing. This runs inside computeds for slider and progress widths, where an exception takes the page down.

round ​

ts
function round(value: number, precision?: number): number
ts
round(1.005, 2) // 1.01
round(1234.5, -2) // 1200

It shifts the decimal point in the string form. Multiplying by 100 moves 1.005's stored floating-point error to where rounding can see it, and yields 1. Halves round away from zero, so a column of mixed signs rounds symmetrically.

precision defaults to 0 and accepts negatives.

formatBytes ​

ts
function formatBytes(bytes: number, options?: FormatBytesOptions): string

interface FormatBytesOptions {
  decimals?: number
  binary?: boolean
}
ts
formatBytes(1536) // '1.5 KB' — '1,5 KB' in de
formatBytes(1536, { binary: false }) // '1.5 kB'

Defaults: decimals 1, binary true (scaling by 1024, KB). binary: false scales by 1000 and labels kB.

The digits follow the app locale, so a German dialog reads 1,5 MB. Scaling is repeated division, not Math.log(n) / Math.log(1024), which reports 1024 bytes as 0.9999… KB. Bytes themselves are always whole.

percentage ​

ts
function percentage(value: number, total: number, options?: PercentageOptions): number

interface PercentageOptions {
  decimals?: number
  clamp?: boolean
}
ts
percentage(37, 200) // 19
percentage(1, 3, { decimals: 1 }) // 33.3

Returns a number, so the caller decides how to render it.

A zero total yields 0. That guard is the reason this exists: NaN as a CSS width silently produces a full bar rather than an empty one.

clamp is off by default, because over 100% is often real.

randomInt ​

ts
function randomInt(min: number, max: number): number
ts
randomInt(1, 6) // 1 … 6

Inclusive at both ends; reversed bounds are swapped. Not cryptographically secure — for tokens use crypto.getRandomValues.

onNumericKeydown and onNumericPaste ​

ts
import { onNumericKeydown, onNumericPaste } from '@shared/utils/numericInput'
ts
function onNumericKeydown(event: KeyboardEvent, options?: { decimal?: boolean, sign?: boolean }): void
function onNumericPaste(event: ClipboardEvent, options?: { decimal?: boolean, sign?: boolean }): void

A keydown filter, not a value sanitizer. Stripping after the fact fights the caret and loses an IME's composition.

vue
<template>
  <input
    type="text"
    inputmode="numeric"
    @keydown="onNumericKeydown($event, { decimal: true })"
    @paste="onNumericPaste($event, { decimal: true })"
  >
</template>

decimal also allows ., sign a leading -; both default off. Editing and navigation keys always pass, as do Ctrl/Cmd shortcuts, so copy, paste and select-all keep working.

Why not type="number"? It accepts e, E, + and -, and then reports itself empty. inputmode="numeric" enforces nothing at all — it only picks the on-screen keyboard.