Utilities

UnitConverter

Converts length, mass, volume, area, speed, temperature, data and time with exact decimal factors, either way as you type, and shows the formula.

#smart#interactive#converter#units#calculator#temperature

Demo

Preview

Unit converter

26.2 mi = 42.1648128 km

km = mi × 1.609344

Oven temperature

100 °C = 212 °F

°F = °C × 9/5 + 32

Download size

1 GiB = 1.073741824 GB

GB = GiB × 1.073741824

Change it to see on_change.

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/unit_converter.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/unit_converter.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/unit_converter.qwe
component UnitConverterDemoLive() {    state seen = null    <div.demo-stack>        <UnitConverter id="unit-converter-demo-data" title="Download size" category="data" from="gib" to="gb" value="1" name="size"                       on_change={(c) => { seen = c }}/>        <p.demo-note>{seen == null ? "Change it to see on_change." : `on_change: ${seen.from_value} ${seen.from_unit} is ${seen.to_value} ${seen.to_unit}.`}</p>    </div>}view UnitConverterDemo() =    <div.demo-stack>        <UnitConverter id="unit-converter-demo" value="26.2" category="length" from="mi" to="km"/>        <UnitConverter id="unit-converter-demo-heat" title="Oven temperature" category="temperature" from="c" to="f" value="100"/>        <UnitConverterDemoLive/>    </div>

Source

ui/unit_converter.qwe
// UnitConverter: length, mass, volume, area, speed, temperature, data and time,// in exact decimals. Each unit is a fraction of its kind's base unit (a mile is// 1609.344 m, a km/h is 1000/3600 m/s), and a conversion multiplies before it// divides, so 3.6 km/h is 1 m/s and not 0.9999...; an answer with digits past what// can be held is rounded and shown with ≈. Either field can be typed in and the// other follows. Temperatures have an offset, so 100 °C is 212 °F; "a difference"// turns the offset off, so a rise of 100 °C is a rise of 180 °F. A reading below// absolute zero is refused. It shows the formula for the pair of units.////   <UnitConverter/>//   <UnitConverter category="temperature" from="c" to="f" value="100"/>//   <UnitConverter category="data" from="gib" to="gb" value="1"//                  on_change={(c) => { size = c.to_value }}/>//// category: "length" (the default), "mass", "volume", "area", "speed",// "temperature", "data" or "time". from, to: the unit ids to start on (see// unit_converter_kinds below; else the kind's first two). value: what the first// field starts with (number or text; "1"). title: the heading ("Unit converter").// id: a prefix for its fields' ids ("unit-converter"), unique on the page. The// fields are named category, from_value, from_unit, to_value, to_unit and difference, so a// form around it sends them; name: a prefix for those names. on_change(c): inside one of// your components, given { category, from_unit, to_unit, from_value, to_value,// exact } as text after each change, the values empty when there is nothing to// show.//// unit_converter_convert(value, category, from, to, difference) is the arithmetic,// for a program's own use: value is a decimal, the answer one, or an error value.// It gives what fits in 28 places; volumes in US units are the US gallon's (231// cubic inches), imperial ones say so, and the year is 365.25 days.import ui/theme.qwe use ui_clslet unit_converter_css = css`.ui-unit-conv { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; min-width: 0; max-width: 620px; padding: 20px 22px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius); background: var(--ui-surface); color: var(--ui-fg); font: 14px/1.45 var(--ui-font); }.ui-unit-conv .ui-unit-conv-title { display: block; margin: 0; font-size: 16px; font-weight: 650; }.ui-unit-conv-field { display: grid; gap: 5px; min-width: 0; }.ui-unit-conv-field label { font-size: 13px; font-weight: 500; }.ui-unit-conv select, .ui-unit-conv-field input { box-sizing: border-box; width: 100%; min-width: 0; height: 38px; padding: 0 10px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius-sm); background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font); }.ui-unit-conv-field input { font-variant-numeric: tabular-nums; }.ui-unit-conv select:focus-visible, .ui-unit-conv-field input:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-unit-conv-bad input { border-color: var(--ui-danger); }.ui-unit-conv-pair { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); gap: 12px; align-items: center; }.ui-unit-conv-side { display: grid; gap: 8px; min-width: 0; }.ui-unit-conv-swap { box-sizing: border-box; width: 38px; height: 38px; padding: 0; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-surface);  color: var(--ui-fg); font: 16px/1 var(--ui-font); cursor: pointer; }.ui-unit-conv-swap:hover { border-color: var(--ui-strong); background: var(--ui-subtle); }.ui-unit-conv-swap:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-unit-conv-check { display: inline-flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; }.ui-unit-conv-check input { margin: 0; accent-color: var(--ui-accent); }.ui-unit-conv-out { display: grid; gap: 4px; margin: 0; padding: 10px 14px; border-radius: var(--ui-radius-sm); background: var(--ui-subtle); overflow-wrap: anywhere; }.ui-unit-conv-answer { margin: 0; font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }.ui-unit-conv-formula { margin: 0; color: var(--ui-muted); font-size: 13px; font-variant-numeric: tabular-nums; }.ui-unit-conv-error { margin: 0; color: var(--ui-danger); font-size: 13px; }@media (max-width: 480px) { .ui-unit-conv-pair { grid-template-columns: minmax(0, 1fr); justify-items: stretch; } .ui-unit-conv-swap { justify-self: center; transform: rotate(90deg); } }`// Each unit as n/d of the kind's base unit, as text so nothing passes through a// float. Temperature's base is the kelvin, with `off` added first.let unit_converter_kinds = [    { id: "length", name: "Length", units: [        { id: "m", name: "metre", sym: "m", n: "1", d: "1" },        { id: "km", name: "kilometre", sym: "km", n: "1000", d: "1" },        { id: "cm", name: "centimetre", sym: "cm", n: "1", d: "100" },        { id: "mm", name: "millimetre", sym: "mm", n: "1", d: "1000" },        { id: "um", name: "micrometre", sym: "µm", n: "1", d: "1000000" },        { id: "in", name: "inch", sym: "in", n: "0.0254", d: "1" },        { id: "ft", name: "foot", sym: "ft", n: "0.3048", d: "1" },        { id: "yd", name: "yard", sym: "yd", n: "0.9144", d: "1" },        { id: "mi", name: "mile", sym: "mi", n: "1609.344", d: "1" },        { id: "nmi", name: "nautical mile", sym: "nmi", n: "1852", d: "1" }] },    { id: "mass", name: "Mass", units: [        { id: "kg", name: "kilogram", sym: "kg", n: "1", d: "1" },        { id: "lb", name: "pound", sym: "lb", n: "0.45359237", d: "1" },        { id: "g", name: "gram", sym: "g", n: "1", d: "1000" },        { id: "mg", name: "milligram", sym: "mg", n: "1", d: "1000000" },        { id: "t", name: "tonne", sym: "t", n: "1000", d: "1" },        { id: "oz", name: "ounce", sym: "oz", n: "0.028349523125", d: "1" },        { id: "st", name: "stone", sym: "st", n: "6.35029318", d: "1" },        { id: "ton", name: "US ton", sym: "ton", n: "907.18474", d: "1" }] },    { id: "volume", name: "Volume", units: [        { id: "l", name: "litre", sym: "L", n: "1", d: "1" },        { id: "ml", name: "millilitre", sym: "mL", n: "1", d: "1000" },        { id: "m3", name: "cubic metre", sym: "m³", n: "1000", d: "1" },        { id: "tsp", name: "US teaspoon", sym: "tsp", n: "0.00492892159375", d: "1" },        { id: "tbsp", name: "US tablespoon", sym: "tbsp", n: "0.01478676478125", d: "1" },        { id: "floz", name: "US fluid ounce", sym: "fl oz", n: "0.0295735295625", d: "1" },        { id: "cup", name: "US cup", sym: "cup", n: "0.2365882365", d: "1" },        { id: "pt", name: "US pint", sym: "pt", n: "0.473176473", d: "1" },        { id: "qt", name: "US quart", sym: "qt", n: "0.946352946", d: "1" },        { id: "gal", name: "US gallon", sym: "gal", n: "3.785411784", d: "1" },        { id: "imp_pt", name: "imperial pint", sym: "imp pt", n: "0.56826125", d: "1" },        { id: "imp_gal", name: "imperial gallon", sym: "imp gal", n: "4.54609", d: "1" }] },    { id: "area", name: "Area", units: [        { id: "m2", name: "square metre", sym: "m²", n: "1", d: "1" },        { id: "km2", name: "square kilometre", sym: "km²", n: "1000000", d: "1" },        { id: "ha", name: "hectare", sym: "ha", n: "10000", d: "1" },        { id: "cm2", name: "square centimetre", sym: "cm²", n: "1", d: "10000" },        { id: "in2", name: "square inch", sym: "in²", n: "0.00064516", d: "1" },        { id: "ft2", name: "square foot", sym: "ft²", n: "0.09290304", d: "1" },        { id: "yd2", name: "square yard", sym: "yd²", n: "0.83612736", d: "1" },        { id: "acre", name: "acre", sym: "ac", n: "4046.8564224", d: "1" },        { id: "mi2", name: "square mile", sym: "mi²", n: "2589988.110336", d: "1" }] },    { id: "speed", name: "Speed", units: [        { id: "ms", name: "metre a second", sym: "m/s", n: "1", d: "1" },        { id: "kmh", name: "kilometre an hour", sym: "km/h", n: "1000", d: "3600" },        { id: "mph", name: "mile an hour", sym: "mph", n: "0.44704", d: "1" },        { id: "kn", name: "knot", sym: "kn", n: "1852", d: "3600" },        { id: "fts", name: "foot a second", sym: "ft/s", n: "0.3048", d: "1" }] },    { id: "temperature", name: "Temperature", units: [        { id: "c", name: "Celsius", sym: "°C", n: "1", d: "1", off: "273.15" },        { id: "f", name: "Fahrenheit", sym: "°F", n: "5", d: "9", off: "459.67" },        { id: "k", name: "kelvin", sym: "K", n: "1", d: "1", off: "0" },        { id: "r", name: "Rankine", sym: "°R", n: "5", d: "9", off: "0" }] },    { id: "data", name: "Data", units: [        { id: "b", name: "byte", sym: "B", n: "1", d: "1" },        { id: "bit", name: "bit", sym: "bit", n: "1", d: "8" },        { id: "kb", name: "kilobyte", sym: "kB", n: "1000", d: "1" },        { id: "mb", name: "megabyte", sym: "MB", n: "1000000", d: "1" },        { id: "gb", name: "gigabyte", sym: "GB", n: "1000000000", d: "1" },        { id: "tb", name: "terabyte", sym: "TB", n: "1000000000000", d: "1" },        { id: "kib", name: "kibibyte", sym: "KiB", n: "1024", d: "1" },        { id: "mib", name: "mebibyte", sym: "MiB", n: "1048576", d: "1" },        { id: "gib", name: "gibibyte", sym: "GiB", n: "1073741824", d: "1" },        { id: "tib", name: "tebibyte", sym: "TiB", n: "1099511627776", d: "1" }] },    { id: "time", name: "Time", units: [        { id: "s", name: "second", sym: "s", n: "1", d: "1" },        { id: "min", name: "minute", sym: "min", n: "60", d: "1" },        { id: "h", name: "hour", sym: "h", n: "3600", d: "1" },        { id: "day", name: "day", sym: "d", n: "86400", d: "1" },        { id: "wk", name: "week", sym: "wk", n: "604800", d: "1" },        { id: "yr", name: "year (365.25 days)", sym: "yr", n: "31557600", d: "1" },        { id: "ms", name: "millisecond", sym: "ms", n: "1", d: "1000" },        { id: "us", name: "microsecond", sym: "µs", n: "1", d: "1000000" }] },]// Written out because a formula worked out from the offsets would need a repeating// decimal for each pair: °F to °C would read `× 5/9 − 17.77...`.let unit_converter_heat = {    "c>f": "°F = °C × 9/5 + 32", "f>c": "°C = (°F − 32) × 5/9",    "c>k": "K = °C + 273.15", "k>c": "°C = K − 273.15",    "c>r": "°R = (°C + 273.15) × 9/5", "r>c": "°C = °R × 5/9 − 273.15",    "f>k": "K = (°F + 459.67) × 5/9", "k>f": "°F = K × 9/5 − 459.67",    "f>r": "°R = °F + 459.67", "r>f": "°F = °R − 459.67",    "k>r": "°R = K × 9/5", "r>k": "K = °R × 5/9",}// The biggest amount taken: with the largest factor it stays inside a decimal.let unit_converter_limit = 1000000000000mfn unit_converter_kind(category: str) = unit_converter_kinds | find((k) => k.id == category)fn unit_converter_unit(category: str, id: str) = (unit_converter_kind(category).units ?? []) | find((u) => u.id == id)// A whole unit of `from` in units of `to`, as a fraction with no offsets.fn unit_converter_ratio(f, t) = { top: dec(f.n) * dec(t.d), bottom: dec(f.d) * dec(t.n) }// value of `from` in `to`, as a decimal, or an error value that says why not.fn unit_converter_convert(value, category: str, from: str, to: str, difference: bool = false) {    let f = unit_converter_unit(category, from)    let t = unit_converter_unit(category, to)    if f == null || t == null { return err(`no unit ${f == null ? from : to} in ${category}`) }    let v = dec(value)    if is_err(v) { return err("not a number") }    if abs(v) > unit_converter_limit { return err("too large") }    let r = unit_converter_ratio(f, t)    if category != "temperature" || difference { return v * r.top / r.bottom }    let k = v + dec(f.off)    if k < 0 { return err("below absolute zero") }    k * r.top / r.bottom - dec(t.off)}// What was typed as a decimal: null when empty, an error value when it is not a number.fn unit_converter_read(text) {    let t = unit_converter_tidy(text)    if t == "" { return null }    let d = dec(t)    if is_err(d) { return err(`"${slice(trim(str(text)), 0, 24)}" is not a number`) }    d}fn unit_converter_tidy(text) = re.replace(replace(trim(str(text ?? "")), "−", "-"), "[,\\s]", "")fn unit_converter_strip(s: str) = contains(s, ".") ? re.replace(s, "\\.?0+$", "") : s// A decimal for the fields: every digit while there are few enough, as an exact answer// has; past that it is a repeating one, cut to twelve significant digits.fn unit_converter_show(d) {    let text = unit_converter_strip(str(d))    let bare = re.replace(text, "^-|\\.", "")    let digits = re.replace(bare, "^0+", "")    if len(digits) <= 20 { return { text, exact: true } }    let a = abs(d)    let lead = a >= 1m ? 0 : len(bare) - len(digits)    let places = a >= 1m ? max([0, 12 - len(str(int(a)))]) : lead + 11    { text: unit_converter_strip(str(round(d, min([places, 28]), "half_even"))), exact: false }}// How to get `to` from `from`, for the formula and the worked sum.fn unit_converter_formula(category: str, from: str, to: str, difference: bool = false) {    let f = unit_converter_unit(category, from)    let t = unit_converter_unit(category, to)    if f == null || t == null { return "" }    if from == to { return `${t.sym} = ${f.sym}` }    if category == "temperature" && !difference { return unit_converter_heat[`${from}>${to}`] ?? "" }    let r = unit_converter_ratio(f, t)    let times = unit_converter_show(r.top / r.bottom)    if times.exact && len(times.text) <= 14 { return `${t.sym} = ${f.sym} × ${times.text}` }    let over = unit_converter_show(r.bottom / r.top)    if over.exact { return `${t.sym} = ${f.sym} ÷ ${over.text}` }    `${t.sym} = ${f.sym} × ${times.exact ? times.text : `${times.text}...`}`}component UnitConverter(category: str = "length", from: str? = null, to: str? = null, value = "1", title: str = "Unit converter",                        id: str = "unit-converter", name: str? = null, on_change = null, class: str? = null) {    style global unit_converter_css    let start = unit_converter_kind(category) ?? unit_converter_kinds[0]    state kind = start.id    state left_unit = unit_converter_unit(start.id, from ?? "") != null ? from : start.units[0].id    state right_unit = unit_converter_unit(start.id, to ?? "") != null ? to : start.units[1].id    state typed = str(value ?? "")    state side = "left"    state difference = false    let units = unit_converter_kind(kind).units    let named = (n: str) => name != null ? `${name}_${n}` : n    let read = unit_converter_read(typed)    let is_heat = kind == "temperature"    let on_left = side == "left"    let source = on_left ? left_unit : right_unit    let target = on_left ? right_unit : left_unit    let answer = read == null || is_err(read) ? read : unit_converter_convert(read, kind, source, target, difference)    let shown = answer == null || is_err(answer) ? null : unit_converter_show(answer)    let sym = (u: str) => unit_converter_unit(kind, u).sym    let problem = is_err(read) ? read.error        : is_err(answer) ? (answer.error == "too large" ? "That is too large to convert exactly: keep it within a trillion."            : answer.error == "below absolute zero" ? `That is below absolute zero, which is −273.15 °C, −459.67 °F, 0 K or 0 °R.` : answer.error)        : null    let equals = shown != null && !shown.exact ? "≈" : "="    let sentence = shown == null ? "" : `${unit_converter_tidy(typed)} ${sym(source)} ${equals} ${shown.text} ${sym(target)}`    let tell = () => {        if on_change != null {            let r = unit_converter_read(typed)            let a = r == null || is_err(r) ? r : unit_converter_convert(r, kind, source, target, difference)            let s = a == null || is_err(a) ? null : unit_converter_show(a)            let other = s == null ? "" : s.text            on_change({ category: kind, from_unit: left_unit, to_unit: right_unit, from_value: on_left ? typed : other,                        to_value: on_left ? other : typed, exact: s != null && s.exact })        }    }    let type_on = (where: str, text: str) => {        side = where        typed = text        tell()    }    let pick_kind = (k: str) => {        let u = unit_converter_kind(k).units        kind = k        left_unit = u[0].id        right_unit = u[1].id        difference = false        tell()    }    let swap = () => {        let l = left_unit        left_unit = right_unit        right_unit = l        tell()    }    <section class={ui_cls(["ui-unit-conv", class])} aria-labelledby={`${id}-title`}>        <h3.ui-unit-conv-title id={`${id}-title`}>{title}</h3>        <div.ui-unit-conv-field>            <label for={`${id}-kind`}>Measure</label>            <select id={`${id}-kind`} name={named("category")} @change={(e) => { pick_kind(e.value) }}>                {for k in unit_converter_kinds => <option key={k.id} value={k.id} selected={k.id == kind}>{k.name}</option>}            </select>        </div>        <div.ui-unit-conv-pair>            <div.ui-unit-conv-side>                <div class={ui_cls(["ui-unit-conv-field", on_left && problem != null && "ui-unit-conv-bad"])}>                    <label for={`${id}-from`}>From</label>                    <input type="text" inputmode="decimal" id={`${id}-from`} name={named("from_value")} autocomplete="off"                           value={on_left ? typed : (shown.text ?? "")} aria-invalid={on_left && problem != null ? "true" : null}                           aria-describedby={problem != null ? `${id}-error` : null} @input={(e) => { type_on("left", e.value) }}/>                </div>                <div.ui-unit-conv-field>                    <label for={`${id}-from-unit`}>From unit</label>                    <select id={`${id}-from-unit`} name={named("from_unit")} @change={(e) => { left_unit = e.value; tell() }}>                        {for u in units => <option key={u.id} value={u.id} selected={u.id == left_unit}>{`${u.name} (${u.sym})`}</option>}                    </select>                </div>            </div>            <button.ui-unit-conv-swap type="button" aria-label="Swap the units" @click={swap}><span aria-hidden="true">⇄</span></button>            <div.ui-unit-conv-side>                <div class={ui_cls(["ui-unit-conv-field", !on_left && problem != null && "ui-unit-conv-bad"])}>                    <label for={`${id}-to`}>To</label>                    <input type="text" inputmode="decimal" id={`${id}-to`} name={named("to_value")} autocomplete="off"                           value={on_left ? (shown.text ?? "") : typed} aria-invalid={!on_left && problem != null ? "true" : null}                           aria-describedby={problem != null ? `${id}-error` : null} @input={(e) => { type_on("right", e.value) }}/>                </div>                <div.ui-unit-conv-field>                    <label for={`${id}-to-unit`}>To unit</label>                    <select id={`${id}-to-unit`} name={named("to_unit")} @change={(e) => { right_unit = e.value; tell() }}>                        {for u in units => <option key={u.id} value={u.id} selected={u.id == right_unit}>{`${u.name} (${u.sym})`}</option>}                    </select>                </div>            </div>        </div>        {is_heat && <label.ui-unit-conv-check>            <input type="checkbox" name={named("difference")} checked={difference} @change={(e) => { difference = e.checked; tell() }}/>            A difference between temperatures, not a reading        </label>}        {problem != null && <p.ui-unit-conv-error id={`${id}-error`} role="alert">{problem}</p>}        <div.ui-unit-conv-out aria-live="polite">            {sentence != "" && <p.ui-unit-conv-answer>{sentence}</p>}            <p.ui-unit-conv-formula>{unit_converter_formula(kind, source, target, difference)}</p>        </div>    </section>}
Thread
36Useful? Like it, or add to its thread below.

Thread 0 posts

On UnitConverter: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.