Skip to content
lingo

Parse

Turn text into a canonical, validated value.

Prefer the live demos? Open this section in the interactive docs.

Turn any text into a canonical, validated value, then read it back. Use lingo() for flexible fields; use shaped helpers for fixed contracts.

import { lingo, parseQuantity, parseRange } from "@pascal-app/lingo"

const height = parseQuantity("5'11\"", { kind: "length" })
if (height.ok) {
  height.quantity.to("m").value
  height.quantity.format({ compound: ["ft", "in"] })
}

const any = lingo("72 in to cm")
const range = parseRange("between 5 and 10 kg", { kind: "mass" })

Captions:

  • Universal parse: Conversion requests return target units without losing original spans.
  • Parse readout: Warnings can succeed; only errors block the value.
  • System and number format variants: System picks the gallon family; numberFormat resolves separator ambiguity.

Canonical examples from the public docs:

  • 2 ft parses as a length quantity with base 0.6096 m.
  • 5'11" parses as approximately 1.8034 m with feet/inches parts.
  • 72 in to cm parses as a conversion with converted value 182.88 cm.
  • between 5 and 10 kg parses as a mass range.
  • a few minutes parses as an approximate duration range.
  • it's hot parses as a temperature fuzzy range when kind: "temperature" is supplied.

Autocomplete anything

completions(text, opts?) from @pascal-app/lingo/complete returns ranked, fully-parsed canonical readings of partial or ambiguous input — unit-prefix fan-out, unit-ambiguity forks, number alternatives, implied units, and range tails — each a canonical text plus a successful quantity/range/conversion result. A completion is none of the other three: it is not a failure candidate, a success alternative, or an issue suggestion.

import { completions } from "@pascal-app/lingo/complete"
import { parseDate, parseDateRange } from "@pascal-app/lingo/date"
import { lingoInput } from "@pascal-app/lingo/dom"
import { useLingoInput } from "@pascal-app/lingo/react"

// Debounce in your UI (~120–150ms) — completions() re-parses on every call
const items = completions("10 kg to 16", { kind: "mass", limit: 8 })
// range tails fan out: 10–16 kg, 10–16 lb, …

completions("10", { units: ["kg", "lb", "m", "ft"] }) // optimistic without kind

// Inject the date engine (kept out of ./complete for size) — covers
// "noon tomorrow", "next month", and "3 days starting tomorrow"
const withDates = completions("noon tomorrow", {
  date: (text) => {
    const now = new Date()
    const single = parseDate(text, { now })
    return single.ok ? single : parseDateRange(text, { now })
  },
})

lingoInput(input, {
  kind: "mass",
  complete: (text) => completions(text, { kind: "mass", limit: 8 }),
  onComplete: (list) => renderGroupedDropdown(list),
})

// React exposes the same injected list plus headless highlight/selection state
const field = useLingoInput({
  kind: "mass",
  listboxId: "mass-options",
  complete: (text) => completions(text, { kind: "mass", limit: 8 }),
})
field.completions
field.setHighlightedIndex(1)
field.selectCompletion()

Inject it into a field with lingoInput({ complete, onComplete }) or useLingoInput({ complete }); the engine is never bundled into @pascal-app/lingo/dom or /react, and no popup UI ships. Pass date to opt into date completions without pulling @pascal-app/lingo/date into /complete.

Find values in text

findQuantities(text, opts?) scans free text and returns every quantity, range, and conversion it finds, each with a span pointing at the exact characters in the original string.

import { findQuantities } from "@pascal-app/lingo"

const found = findQuantities("ship 2 boxes at 5 kg each by friday")
// [{ result, span: { start, end } }, ...] — offsets into the original text
for (const { result, span } of found) {
  if (result.type === "quantity") highlight(span, result.quantity)
}