1491 lines
63 KiB
HTML
1491 lines
63 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Wandler</title>
|
||
<link rel="stylesheet" href="/shared/boehmi.css">
|
||
<script src="/shared/boehmi.js"></script>
|
||
<style>
|
||
.smartbox { position: relative; }
|
||
#smart { width: 100%; box-sizing: border-box; padding: 14px 16px; font-size: 17px;
|
||
border: 1px solid var(--bt-line); border-radius: var(--bt-r);
|
||
background: var(--bt-surface); color: var(--bt-ink); font-weight: 600; }
|
||
#smart:focus { outline: none; border-color: var(--bt-accent);
|
||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--bt-accent) 18%, transparent); }
|
||
.examples { margin: 8px 0 0; display: flex; gap: 6px; flex-wrap: wrap; }
|
||
.examples button { padding: 3px 9px; font-size: 11.5px; border-radius: 999px;
|
||
color: var(--bt-muted); background: var(--bt-bg-2);
|
||
border: 1px solid var(--bt-line-soft); cursor: pointer; }
|
||
.examples button:hover { color: var(--bt-ink); }
|
||
|
||
.res { margin-top: 14px; }
|
||
.res .main { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
|
||
font-size: 26px; font-weight: 800; letter-spacing: -.02em; }
|
||
.res .main .unit { font-size: 17px; font-weight: 650; color: var(--bt-muted); }
|
||
.res .kind { font-size: 11px; font-weight: 700; letter-spacing: .06em;
|
||
text-transform: uppercase; color: var(--bt-accent); margin-bottom: 4px; }
|
||
.res .alts { margin-top: 10px; display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 4px; }
|
||
.res .err { color: var(--bt-err); font-size: 13px; }
|
||
|
||
.kv { display: flex; justify-content: space-between; gap: 10px; align-items: baseline;
|
||
padding: 5px 9px; border-radius: var(--bt-r-sm); font-size: 13px; cursor: pointer;
|
||
border: 1px solid transparent; }
|
||
.kv:hover { background: var(--bt-bg-2); border-color: var(--bt-line-soft); }
|
||
.kv .k { color: var(--bt-muted); font-size: 12px; }
|
||
.kv .v { font-weight: 700; font-variant-numeric: tabular-nums; text-align: right;
|
||
word-break: break-all; }
|
||
|
||
.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px; }
|
||
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||
.row input[type=text], .row input[type=number], .row select {
|
||
padding: 7px 9px; font-size: 13px; border: 1px solid var(--bt-line);
|
||
border-radius: var(--bt-r-sm); background: var(--bt-surface); color: var(--bt-ink); }
|
||
.row input.num { width: 110px; }
|
||
.row .grow { flex: 1; min-width: 90px; }
|
||
textarea.code { width: 100%; box-sizing: border-box; min-height: 130px; padding: 10px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px;
|
||
border: 1px solid var(--bt-line); border-radius: var(--bt-r-sm);
|
||
background: var(--bt-surface); color: var(--bt-ink); resize: vertical; }
|
||
.swatch { width: 34px; height: 34px; border-radius: var(--bt-r-sm);
|
||
border: 1px solid var(--bt-line); flex: none; }
|
||
.pins { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
|
||
.pins .pin { display: inline-flex; align-items: center; gap: 6px; padding: 3px 6px 3px 10px;
|
||
font-size: 12px; border-radius: 999px; background: var(--bt-accent-soft);
|
||
color: var(--bt-accent); border: 1px solid transparent; cursor: pointer; }
|
||
.pins .pin b { font-weight: 700; }
|
||
.pins .pin .x { opacity: .6; padding: 0 3px; }
|
||
.pins .pin .x:hover { opacity: 1; }
|
||
.hist { font-size: 12.5px; color: var(--bt-muted); }
|
||
.hist div { padding: 3px 0; cursor: pointer; }
|
||
.hist div:hover { color: var(--bt-ink); }
|
||
.note { font-size: 11.5px; color: var(--bt-muted); margin: 6px 0 0; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main class="bt-main wide">
|
||
<div class="bt-pagehead">
|
||
<h1>🔀 Wandler</h1>
|
||
<p>Einheiten, Formate, Farben und Zeit. Tipp einfach, was du meinst —
|
||
gerechnet wird sofort im Browser, ohne Server und ohne Internet.</p>
|
||
</div>
|
||
|
||
<div class="bt-card smartbox">
|
||
<input type="text" id="smart" placeholder="z. B. 2 cups Mehl in g · 180 C in F · #3b82f6 · 1699999999"
|
||
autocomplete="off" spellcheck="false">
|
||
<div class="examples" id="examples"></div>
|
||
<div class="res" id="res"></div>
|
||
<div class="pins" id="pins"></div>
|
||
</div>
|
||
|
||
<div class="bt-tabs" id="tabs">
|
||
<button class="bt-tab active" data-tab="kueche">Küche</button>
|
||
<button class="bt-tab" data-tab="masse">Maße</button>
|
||
<button class="bt-tab" data-tab="technik">Technik</button>
|
||
<button class="bt-tab" data-tab="formate">Formate</button>
|
||
<button class="bt-tab" data-tab="zeit">Zeit</button>
|
||
</div>
|
||
|
||
<!-- ─────────────────────────────────────────────────── Küche -->
|
||
<section id="tab-kueche" class="tab">
|
||
<div class="grid2">
|
||
<div class="bt-card">
|
||
<h2>Zutat umrechnen</h2>
|
||
<p class="hint">Volumen ↔ Gewicht hängt von der Zutat ab: 1 Cup Mehl wiegt
|
||
deutlich weniger als 1 Cup Zucker.</p>
|
||
<div class="row">
|
||
<input type="text" class="num" id="kMenge" value="1">
|
||
<select id="kVon"></select>
|
||
<span>→</span>
|
||
<select id="kNach"></select>
|
||
</div>
|
||
<div class="row" style="margin-top:8px">
|
||
<label class="bt-inline">Zutat: <select id="kZutat"></select></label>
|
||
</div>
|
||
<div id="kOut" class="res"></div>
|
||
</div>
|
||
|
||
<div class="bt-card">
|
||
<h2>Ofen</h2>
|
||
<div class="row">
|
||
<input type="text" class="num" id="oTemp" value="180">
|
||
<select id="oEinheit">
|
||
<option value="c">°C (Ober-/Unterhitze)</option>
|
||
<option value="f">°F</option>
|
||
<option value="umluft">°C (Umluft)</option>
|
||
<option value="gas">Gasstufe</option>
|
||
</select>
|
||
</div>
|
||
<div id="oOut" class="res"></div>
|
||
<p class="note">Umluft ≈ 20 °C weniger als Ober-/Unterhitze. Gasstufen nach
|
||
britischer Skala (Gas Mark) — bei deutschen Geräten kann das abweichen.</p>
|
||
</div>
|
||
|
||
<div class="bt-card">
|
||
<h2>Löffel & Tassen</h2>
|
||
<div id="loeffel"></div>
|
||
<p class="note">EL = 15 ml, TL = 5 ml, Msp ≈ 0,5 ml, Prise ≈ 0,3 ml.
|
||
US-Cup = 236,6 ml (nicht 250 ml).</p>
|
||
</div>
|
||
|
||
<div class="bt-card">
|
||
<h2>Portionen umrechnen</h2>
|
||
<div class="row">
|
||
<label class="bt-inline">von <input type="text" class="num" id="pVon" value="4"></label>
|
||
<label class="bt-inline">auf <input type="text" class="num" id="pNach" value="6"></label>
|
||
</div>
|
||
<div class="row" style="margin-top:8px">
|
||
<input type="text" class="grow" id="pMenge" placeholder="Menge, z. B. 250 g oder 1 1/2 EL">
|
||
</div>
|
||
<div id="pOut" class="res"></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ──────────────────────────────────────────────────── Maße -->
|
||
<section id="tab-masse" class="tab bt-hidden">
|
||
<div class="bt-card">
|
||
<div class="row">
|
||
<select id="mDim"></select>
|
||
<input type="text" class="num" id="mWert" value="1">
|
||
<select id="mVon"></select>
|
||
</div>
|
||
<div id="mOut" class="res"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ───────────────────────────────────────────────── Technik -->
|
||
<section id="tab-technik" class="tab bt-hidden">
|
||
<div class="grid2">
|
||
<div class="bt-card">
|
||
<h2>Datenmengen</h2>
|
||
<div class="row">
|
||
<input type="text" class="num" id="dWert" value="1">
|
||
<select id="dVon"></select>
|
||
</div>
|
||
<div id="dOut" class="res"></div>
|
||
<p class="note">MB (1000) und MiB (1024) werden sauber getrennt — genau da
|
||
kommen die „fehlenden“ Gigabyte auf Festplatten her.</p>
|
||
</div>
|
||
|
||
<div class="bt-card">
|
||
<h2>Zahlensysteme</h2>
|
||
<div class="row">
|
||
<input type="text" class="grow" id="zWert" value="255" spellcheck="false">
|
||
<select id="zVon">
|
||
<option value="10">Dezimal</option>
|
||
<option value="16">Hex</option>
|
||
<option value="2">Binär</option>
|
||
<option value="8">Oktal</option>
|
||
</select>
|
||
</div>
|
||
<div id="zOut" class="res"></div>
|
||
</div>
|
||
|
||
<div class="bt-card">
|
||
<h2>Farben</h2>
|
||
<div class="row">
|
||
<span class="swatch" id="fSwatch"></span>
|
||
<input type="text" class="grow" id="fWert" value="#3b82f6" spellcheck="false">
|
||
<input type="color" id="fPick" value="#3b82f6" style="width:38px;height:34px;padding:2px;border:1px solid var(--bt-line);border-radius:var(--bt-r-sm);background:var(--bt-surface)">
|
||
</div>
|
||
<div id="fOut" class="res"></div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ───────────────────────────────────────────────── Formate -->
|
||
<section id="tab-formate" class="tab bt-hidden">
|
||
<div class="bt-card">
|
||
<div class="row">
|
||
<select id="tOp">
|
||
<optgroup label="Kodierung">
|
||
<option value="b64e">Base64 kodieren</option>
|
||
<option value="b64d">Base64 dekodieren</option>
|
||
<option value="urle">URL kodieren</option>
|
||
<option value="urld">URL dekodieren</option>
|
||
</optgroup>
|
||
<optgroup label="Text">
|
||
<option value="slug">Slug (dateiname-tauglich)</option>
|
||
<option value="upper">GROSSBUCHSTABEN</option>
|
||
<option value="lower">kleinbuchstaben</option>
|
||
<option value="title">Erste Buchstaben Groß</option>
|
||
<option value="camel">camelCase</option>
|
||
<option value="snake">snake_case</option>
|
||
<option value="kebab">kebab-case</option>
|
||
<option value="trimlines">Leerzeilen & Leerraum säubern</option>
|
||
<option value="stats">Zeichen zählen</option>
|
||
</optgroup>
|
||
<optgroup label="Daten">
|
||
<option value="jsonp">JSON schön formatieren</option>
|
||
<option value="jsonm">JSON kompakt</option>
|
||
<option value="json2yaml">JSON → YAML</option>
|
||
<option value="yaml2json">YAML → JSON</option>
|
||
<option value="json2csv">JSON → CSV</option>
|
||
<option value="csv2json">CSV → JSON</option>
|
||
</optgroup>
|
||
</select>
|
||
<button class="ghost" id="tSwap" title="Ergebnis nach oben übernehmen">↑ übernehmen</button>
|
||
<span class="bt-spacer"></span>
|
||
<button class="ghost" id="tCopy">Ergebnis kopieren</button>
|
||
</div>
|
||
<div style="margin-top:10px">
|
||
<textarea class="code" id="tIn" spellcheck="false" placeholder="Text hier einfügen …"></textarea>
|
||
</div>
|
||
<div style="margin-top:8px">
|
||
<textarea class="code" id="tOut" spellcheck="false" readonly placeholder="Ergebnis"></textarea>
|
||
</div>
|
||
<p class="note" id="tNote"></p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ──────────────────────────────────────────────────── Zeit -->
|
||
<section id="tab-zeit" class="tab bt-hidden">
|
||
<div class="grid2">
|
||
<div class="bt-card">
|
||
<h2>Zeitstempel</h2>
|
||
<div class="row">
|
||
<input type="text" class="grow" id="tsWert" spellcheck="false"
|
||
placeholder="1699999999 oder 2024-11-14 20:30">
|
||
<button class="ghost" id="tsNow">Jetzt</button>
|
||
</div>
|
||
<div id="tsOut" class="res"></div>
|
||
</div>
|
||
|
||
<div class="bt-card">
|
||
<h2>Zeitzonen</h2>
|
||
<div class="row">
|
||
<input type="text" class="grow" id="tzWert" spellcheck="false" placeholder="leer = jetzt">
|
||
<select id="tzZone"></select>
|
||
</div>
|
||
<div id="tzOut" class="res"></div>
|
||
</div>
|
||
|
||
<div class="bt-card">
|
||
<h2>Datumsdifferenz</h2>
|
||
<div class="row">
|
||
<input type="date" id="dvA"><span>→</span><input type="date" id="dvB">
|
||
</div>
|
||
<div id="dvOut" class="res"></div>
|
||
</div>
|
||
|
||
<div class="bt-card">
|
||
<h2>Cron im Klartext</h2>
|
||
<div class="row">
|
||
<input type="text" class="grow" id="crWert" value="0 3 * * 1" spellcheck="false">
|
||
</div>
|
||
<div id="crOut" class="res"></div>
|
||
<p class="note">Reihenfolge: Minute Stunde Tag Monat Wochentag.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="bt-card">
|
||
<h2>Zuletzt</h2>
|
||
<div class="hist" id="hist"><span class="bt-muted">Noch nichts umgerechnet.</span></div>
|
||
</div>
|
||
</main>
|
||
|
||
<script>
|
||
/* ═════════════════════════════════════════════ ENGINE START ═══════════
|
||
Reine Rechenlogik ohne DOM — damit sie sich einzeln testen lässt.
|
||
Alles hier ist deterministisch: gleiche Eingabe, gleiche Ausgabe, sofort.
|
||
(Bewusst keine KI — ein Wandler muss verlässlich sein, nicht kreativ.) */
|
||
const BTC = (function () {
|
||
|
||
/* ---------------------------------------------------------- Zahlen */
|
||
const VULGAR = { "½":.5, "⅓":1/3, "⅔":2/3, "¼":.25, "¾":.75, "⅕":.2, "⅖":.4,
|
||
"⅗":.6, "⅘":.8, "⅙":1/6, "⅚":5/6, "⅛":.125, "⅜":.375,
|
||
"⅝":.625, "⅞":.875 };
|
||
|
||
function parseNumber(raw) {
|
||
if (raw === null || raw === undefined) return NaN;
|
||
let s = String(raw).trim();
|
||
if (!s) return NaN;
|
||
let vorzeichen = 1;
|
||
if (s[0] === "-") { vorzeichen = -1; s = s.slice(1).trim(); }
|
||
|
||
// Unicode-Brüche, auch gemischt: "1½"
|
||
for (const ch in VULGAR) {
|
||
if (s.indexOf(ch) >= 0) {
|
||
const rest = s.split(ch).join("").trim();
|
||
const ganz = rest ? parseNumber(rest) : 0;
|
||
if (isNaN(ganz)) return NaN;
|
||
return vorzeichen * (ganz + VULGAR[ch]);
|
||
}
|
||
}
|
||
// "1 1/2" und "3/4"
|
||
let m = s.match(/^(\d+)\s+(\d+)\s*\/\s*(\d+)$/);
|
||
if (m) return vorzeichen * (Number(m[1]) + Number(m[2]) / Number(m[3]));
|
||
m = s.match(/^(\d+(?:[.,]\d+)?)\s*\/\s*(\d+(?:[.,]\d+)?)$/);
|
||
if (m) return vorzeichen * (parseNumber(m[1]) / parseNumber(m[2]));
|
||
|
||
s = s.replace(/\s/g, "");
|
||
const hatK = s.indexOf(",") >= 0, hatP = s.indexOf(".") >= 0;
|
||
if (hatK && hatP) {
|
||
// Das zuletzt stehende Zeichen ist das Dezimaltrennzeichen.
|
||
const dez = s.lastIndexOf(",") > s.lastIndexOf(".") ? "," : ".";
|
||
const tsd = dez === "," ? "." : ",";
|
||
s = s.split(tsd).join("");
|
||
s = s.split(dez).join(".");
|
||
} else if (hatK) {
|
||
const t = s.split(",");
|
||
s = t.length > 2 ? t.join("") : t.join(".");
|
||
} else if (hatP) {
|
||
const t = s.split(".");
|
||
if (t.length > 2) s = t.join("");
|
||
}
|
||
if (!/^\d*\.?\d+(?:e[+-]?\d+)?$/i.test(s)) return NaN;
|
||
const n = Number(s);
|
||
return isNaN(n) ? NaN : vorzeichen * n;
|
||
}
|
||
|
||
function fmt(n, stellen) {
|
||
if (n === null || n === undefined || isNaN(n)) return "—";
|
||
if (!isFinite(n)) return "∞";
|
||
if (n === 0) return "0";
|
||
const a = Math.abs(n);
|
||
let max = stellen;
|
||
if (max === undefined) {
|
||
if (a >= 1e15 || a < 1e-6) return n.toExponential(4).replace(".", ",");
|
||
max = a >= 1000 ? 2 : a >= 1 ? 4 : 6;
|
||
}
|
||
return n.toLocaleString("de-DE", { maximumFractionDigits: max });
|
||
}
|
||
|
||
/* In der Küche sind vier Nachkommastellen Unsinn: „250,7835 g Mehl“ wiegt
|
||
niemand ab. Deshalb hier bewusst grob runden. */
|
||
function fmtKueche(n) {
|
||
if (n === null || n === undefined || isNaN(n)) return "—";
|
||
const a = Math.abs(n);
|
||
const stellen = a >= 100 ? 0 : a >= 10 ? 1 : a >= 1 ? 1 : 2;
|
||
return fmt(n, stellen);
|
||
}
|
||
|
||
/* --------------------------------------------------------- Einheiten
|
||
Jede Größe hat eine Basiseinheit; „f“ ist der Faktor dorthin. */
|
||
const DIMS = {
|
||
laenge: { name: "Länge", basis: "m", units: {
|
||
mm:{f:.001,a:["millimeter"]}, cm:{f:.01,a:["zentimeter"]}, dm:{f:.1},
|
||
m:{f:1,a:["meter"]}, km:{f:1000,a:["kilometer"]},
|
||
in:{f:.0254,a:["inch","zoll","\""]}, ft:{f:.3048,a:["foot","feet","fuss","fuß"]},
|
||
yd:{f:.9144,a:["yard"]}, mi:{f:1609.344,a:["mile","meile"]},
|
||
sm:{f:1852,a:["nmi","seemeile"]},
|
||
}},
|
||
masse: { name: "Gewicht", basis: "g", units: {
|
||
mg:{f:.001,a:["milligramm"]}, g:{f:1,a:["gramm","gr"]},
|
||
dag:{f:10,a:["dkg","dekagramm"]}, kg:{f:1000,a:["kilogramm","kilo"]},
|
||
t:{f:1e6,a:["tonne","tonnen"]},
|
||
oz:{f:28.349523125,a:["unze","ounce"]},
|
||
lb:{f:453.59237,a:["pound","pounds"]},
|
||
pfund:{f:500,a:[]},
|
||
st:{f:6350.29318,a:["stone"]},
|
||
}},
|
||
volumen: { name: "Volumen", basis: "ml", units: {
|
||
ml:{f:1,a:["milliliter","cc","cm3"]}, cl:{f:10,a:["zentiliter"]},
|
||
dl:{f:100,a:["deziliter"]}, l:{f:1000,a:["liter","ltr"]},
|
||
m3:{f:1e6,a:["kubikmeter"]},
|
||
tl:{f:5,a:["teeloeffel","teelöffel","tsp","teaspoon"]},
|
||
el:{f:15,a:["essloeffel","esslöffel","tbsp","tablespoon"]},
|
||
msp:{f:.5,a:["messerspitze"]}, prise:{f:.3,a:["prisen"]},
|
||
cup:{f:236.5882365,a:["cups","tasse","tassen"]},
|
||
floz:{f:29.5735295625,a:["fl oz","fluid ounce"]},
|
||
pint:{f:473.176473,a:["pt"]}, quart:{f:946.352946,a:["qt"]},
|
||
gallone:{f:3785.411784,a:["gal","gallon"]},
|
||
}},
|
||
flaeche: { name: "Fläche", basis: "m2", units: {
|
||
mm2:{f:1e-6}, cm2:{f:1e-4}, m2:{f:1,a:["qm","quadratmeter"]},
|
||
a:{f:100,a:["ar"]}, ha:{f:1e4,a:["hektar"]}, km2:{f:1e6,a:["qkm"]},
|
||
sqft:{f:.09290304,a:["ft2"]}, sqyd:{f:.83612736}, acre:{f:4046.8564224},
|
||
}},
|
||
zeit: { name: "Zeit", basis: "s", units: {
|
||
ms:{f:.001,a:["millisekunde","millisekunden"]},
|
||
s:{f:1,a:["sek","sekunde","sekunden"]},
|
||
min:{f:60,a:["minute","minuten"]},
|
||
h:{f:3600,a:["std","stunde","stunden"]},
|
||
d:{f:86400,a:["tag","tage"]},
|
||
woche:{f:604800,a:["wochen","wk"]},
|
||
monat:{f:2629800,a:["monate"]},
|
||
jahr:{f:31557600,a:["jahre","a"]},
|
||
}},
|
||
tempo: { name: "Geschwindigkeit", basis: "m/s", units: {
|
||
"m/s":{f:1}, "km/h":{f:1/3.6,a:["kmh"]}, mph:{f:.44704},
|
||
kn:{f:.514444,a:["knoten"]}, "ft/s":{f:.3048},
|
||
}},
|
||
energie: { name: "Energie", basis: "J", units: {
|
||
J:{f:1,a:["joule"]}, kJ:{f:1000,a:["kilojoule"]},
|
||
cal:{f:4.184,a:["kalorie"]}, kcal:{f:4184,a:["kilokalorie","kalorien"]},
|
||
Wh:{f:3600,a:["wattstunde"]}, kWh:{f:3.6e6,a:["kilowattstunde"]},
|
||
}},
|
||
druck: { name: "Druck", basis: "Pa", units: {
|
||
Pa:{f:1,a:["pascal"]}, hPa:{f:100,a:["hektopascal"]}, kPa:{f:1000},
|
||
mbar:{f:100,a:["millibar"]}, bar:{f:1e5}, psi:{f:6894.757293},
|
||
atm:{f:101325}, mmHg:{f:133.322387415,a:["torr"]},
|
||
}},
|
||
daten: { name: "Datenmenge", basis: "B", units: {
|
||
bit:{f:.125,a:["bits","b"]},
|
||
B:{f:1,a:["byte","bytes"]},
|
||
kB:{f:1e3,a:["kilobyte"]}, MB:{f:1e6,a:["megabyte"]},
|
||
GB:{f:1e9,a:["gigabyte"]}, TB:{f:1e12,a:["terabyte"]}, PB:{f:1e15},
|
||
KiB:{f:1024,a:["kibibyte"]}, MiB:{f:1048576,a:["mebibyte"]},
|
||
GiB:{f:1073741824,a:["gibibyte"]}, TiB:{f:1099511627776,a:["tebibyte"]},
|
||
PiB:{f:1125899906842624},
|
||
}},
|
||
};
|
||
|
||
// Temperatur wird gesondert behandelt (Nullpunkt-Verschiebung).
|
||
const TEMP = {
|
||
C: { a:["c","°c","celsius","grad"], zuK:(v)=>v+273.15, vonK:(k)=>k-273.15 },
|
||
F: { a:["f","°f","fahrenheit"], zuK:(v)=>(v-32)*5/9+273.15, vonK:(k)=>(k-273.15)*9/5+32 },
|
||
K: { a:["k","kelvin"], zuK:(v)=>v, vonK:(k)=>k },
|
||
};
|
||
|
||
// Alias-Verzeichnis: geschriebene Form → { dim, key }
|
||
const LOOKUP = {};
|
||
function merken(alias, dim, key) {
|
||
const k = String(alias).toLowerCase();
|
||
if (!(k in LOOKUP)) LOOKUP[k] = { dim: dim, key: key };
|
||
}
|
||
for (const dim in DIMS) {
|
||
for (const key in DIMS[dim].units) {
|
||
merken(key, dim, key);
|
||
(DIMS[dim].units[key].a || []).forEach((al) => merken(al, dim, key));
|
||
}
|
||
}
|
||
for (const key in TEMP) {
|
||
merken(key, "temperatur", key);
|
||
TEMP[key].a.forEach((al) => merken(al, "temperatur", key));
|
||
}
|
||
|
||
function findeEinheit(text) {
|
||
if (!text) return null;
|
||
return LOOKUP[String(text).trim().toLowerCase()] || null;
|
||
}
|
||
|
||
function umrechnen(wert, von, nach) {
|
||
if (von.dim !== nach.dim) return null;
|
||
if (von.dim === "temperatur") {
|
||
return TEMP[nach.key].vonK(TEMP[von.key].zuK(wert));
|
||
}
|
||
const u = DIMS[von.dim].units;
|
||
return wert * u[von.key].f / u[nach.key].f;
|
||
}
|
||
|
||
/* ------------------------------------------------------ Küche/Dichte
|
||
Gramm je Milliliter. Ohne diese Tabelle wäre „1 Cup Mehl in g“ falsch. */
|
||
const DICHTE = {
|
||
"wasser": 1.0, "milch": 1.03, "sahne": 1.0, "joghurt": 1.03,
|
||
"mehl": .53, "vollkornmehl": .55, "speisestärke": .60, "backpulver": .72,
|
||
"zucker": .85, "puderzucker": .56, "brauner zucker": .93, "salz": 1.20,
|
||
"butter": .911, "margarine": .95, "öl": .918, "honig": 1.42, "sirup": 1.33,
|
||
"reis": .85, "haferflocken": .40, "couscous": .80, "linsen": .85,
|
||
"semmelbrösel": .40, "kakao": .42, "nüsse gehackt": .50, "mandeln gemahlen": .38,
|
||
"parmesan gerieben": .40, "quark": 1.05, "tomatenmark": 1.10,
|
||
};
|
||
const DICHTE_STD = 1.0;
|
||
|
||
function dichteVon(zutat) {
|
||
if (!zutat) return null;
|
||
const s = String(zutat).trim().toLowerCase();
|
||
if (!s) return null;
|
||
if (s in DICHTE) return DICHTE[s];
|
||
// längster passender Teiltreffer, damit „weizenmehl“ auf „mehl“ greift
|
||
let treffer = null;
|
||
for (const k in DICHTE) {
|
||
if (s.indexOf(k) >= 0 && (!treffer || k.length > treffer.length)) treffer = k;
|
||
}
|
||
return treffer ? DICHTE[treffer] : null;
|
||
}
|
||
|
||
/* Rechnet auch über Größen hinweg, wenn eine Dichte bekannt ist. */
|
||
function kuecheRechnen(wert, von, nach, zutat) {
|
||
if (von.dim === nach.dim) {
|
||
return { wert: umrechnen(wert, von, nach), dichte: null };
|
||
}
|
||
const paar = von.dim + ">" + nach.dim;
|
||
const d = dichteVon(zutat);
|
||
const dichte = d === null ? DICHTE_STD : d;
|
||
if (paar === "volumen>masse") {
|
||
const ml = wert * DIMS.volumen.units[von.key].f;
|
||
const g = ml * dichte;
|
||
return { wert: g / DIMS.masse.units[nach.key].f, dichte: dichte, geraten: d === null };
|
||
}
|
||
if (paar === "masse>volumen") {
|
||
const g = wert * DIMS.masse.units[von.key].f;
|
||
const ml = g / dichte;
|
||
return { wert: ml / DIMS.volumen.units[nach.key].f, dichte: dichte, geraten: d === null };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/* ------------------------------------------------------------- Ofen */
|
||
const GAS = [[1,140],[2,150],[3,170],[4,180],[5,190],[6,200],[7,220],[8,230],[9,240]];
|
||
function ofen(wert, art) {
|
||
let c;
|
||
if (art === "c") c = wert;
|
||
else if (art === "f") c = (wert - 32) * 5 / 9;
|
||
else if (art === "umluft") c = wert + 20;
|
||
else if (art === "gas") {
|
||
let best = GAS[0];
|
||
for (const g of GAS) if (Math.abs(g[0] - wert) < Math.abs(best[0] - wert)) best = g;
|
||
c = best[1];
|
||
} else return null;
|
||
let gas = null, bestAb = Infinity;
|
||
for (const g of GAS) {
|
||
const ab = Math.abs(g[1] - c);
|
||
if (ab < bestAb) { bestAb = ab; gas = g[0]; }
|
||
}
|
||
return { c: c, f: c * 9 / 5 + 32, umluft: c - 20, gas: gas, gasGenau: bestAb <= 5 };
|
||
}
|
||
|
||
/* ----------------------------------------------------------- Farben */
|
||
function hexZuRgb(hex) {
|
||
let h = String(hex).trim().replace(/^#/, "");
|
||
if (/^[0-9a-f]{3}$/i.test(h)) h = h.split("").map((c) => c + c).join("");
|
||
if (/^[0-9a-f]{4}$/i.test(h)) h = h.split("").map((c) => c + c).join("");
|
||
if (/^[0-9a-f]{8}$/i.test(h)) h = h.slice(0, 6);
|
||
if (!/^[0-9a-f]{6}$/i.test(h)) return null;
|
||
return { r: parseInt(h.slice(0,2),16), g: parseInt(h.slice(2,4),16), b: parseInt(h.slice(4,6),16) };
|
||
}
|
||
function rgbZuHex(r, g, b) {
|
||
const t = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0");
|
||
return "#" + t(r) + t(g) + t(b);
|
||
}
|
||
function rgbZuHsl(r, g, b) {
|
||
r /= 255; g /= 255; b /= 255;
|
||
const max = Math.max(r,g,b), min = Math.min(r,g,b);
|
||
let h = 0, s = 0; const l = (max + min) / 2;
|
||
if (max !== min) {
|
||
const d = max - min;
|
||
s = l > .5 ? d / (2 - max - min) : d / (max + min);
|
||
if (max === r) h = (g - b) / d + (g < b ? 6 : 0);
|
||
else if (max === g) h = (b - r) / d + 2;
|
||
else h = (r - g) / d + 4;
|
||
h *= 60;
|
||
}
|
||
return { h: h, s: s * 100, l: l * 100 };
|
||
}
|
||
function hslZuRgb(h, s, l) {
|
||
h = ((h % 360) + 360) % 360; s /= 100; l /= 100;
|
||
const c = (1 - Math.abs(2 * l - 1)) * s;
|
||
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
|
||
const m = l - c / 2;
|
||
let r = 0, g = 0, b = 0;
|
||
if (h < 60) { r = c; g = x; }
|
||
else if (h < 120) { r = x; g = c; }
|
||
else if (h < 180) { g = c; b = x; }
|
||
else if (h < 240) { g = x; b = c; }
|
||
else if (h < 300) { r = x; b = c; }
|
||
else { r = c; b = x; }
|
||
return { r: (r + m) * 255, g: (g + m) * 255, b: (b + m) * 255 };
|
||
}
|
||
function farbeLesen(text) {
|
||
const s = String(text).trim();
|
||
let m = s.match(/^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/i);
|
||
if (m) return { r: +m[1], g: +m[2], b: +m[3] };
|
||
m = s.match(/^hsla?\(\s*(-?[\d.]+)\s*(?:deg)?[,\s]+([\d.]+)%?[,\s]+([\d.]+)%?/i);
|
||
if (m) return hslZuRgb(+m[1], +m[2], +m[3]);
|
||
return hexZuRgb(s);
|
||
}
|
||
|
||
/* -------------------------------------------------------- Zeit/Cron */
|
||
function zeitstempelLesen(text) {
|
||
const s = String(text).trim();
|
||
if (/^\d{13}$/.test(s)) return new Date(Number(s));
|
||
if (/^\d{10}$/.test(s)) return new Date(Number(s) * 1000);
|
||
let m = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:[ ,]+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/);
|
||
if (m) return new Date(+m[3], +m[2]-1, +m[1], +(m[4]||0), +(m[5]||0), +(m[6]||0));
|
||
if (/^\d{4}-\d{2}-\d{2}/.test(s)) {
|
||
const d = new Date(s.length <= 10 ? s + "T00:00:00" : s);
|
||
return isNaN(d.getTime()) ? null : d;
|
||
}
|
||
return null;
|
||
}
|
||
function kalenderwoche(d) {
|
||
const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
||
const tag = t.getUTCDay() || 7;
|
||
t.setUTCDate(t.getUTCDate() + 4 - tag);
|
||
const start = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
|
||
return Math.ceil((((t - start) / 86400000) + 1) / 7);
|
||
}
|
||
|
||
const WOTAG = ["sonntags","montags","dienstags","mittwochs","donnerstags","freitags","samstags"];
|
||
const MONATE = ["Januar","Februar","März","April","Mai","Juni","Juli","August",
|
||
"September","Oktober","November","Dezember"];
|
||
|
||
function cronText(ausdruck) {
|
||
const t = String(ausdruck).trim().split(/\s+/);
|
||
if (t.length !== 5) return null;
|
||
const [mi, st, tg, mo, wt] = t;
|
||
const gueltig = /^[\d*/,\-]+$/;
|
||
if (![mi, st, tg, mo, wt].every((x) => gueltig.test(x))) return null;
|
||
|
||
let takt = "";
|
||
if (mi.startsWith("*/") && st === "*") takt = `alle ${mi.slice(2)} Minuten`;
|
||
else if (mi === "*" && st === "*") takt = "jede Minute";
|
||
else if (st.startsWith("*/")) takt = `alle ${st.slice(2)} Stunden zur Minute ${mi}`;
|
||
else if (/^\d+$/.test(mi) && /^\d+$/.test(st))
|
||
takt = `um ${String(st).padStart(2,"0")}:${String(mi).padStart(2,"0")} Uhr`;
|
||
else if (/^\d+$/.test(mi) && st === "*") takt = `stündlich zur Minute ${mi}`;
|
||
else takt = `Minute ${mi}, Stunde ${st}`;
|
||
|
||
const teile = [];
|
||
if (wt !== "*") {
|
||
const namen = wt.split(",").map((x) => WOTAG[Number(x) % 7] || `Wochentag ${x}`);
|
||
teile.push(namen.join(" und "));
|
||
}
|
||
if (tg !== "*") {
|
||
if (tg.startsWith("*/")) teile.push(`alle ${tg.slice(2)} Tage`);
|
||
else teile.push(`am ${tg}. des Monats`);
|
||
}
|
||
if (mo !== "*") {
|
||
const namen = mo.split(",").map((x) => MONATE[Number(x) - 1] || `Monat ${x}`);
|
||
teile.push("im " + namen.join(" und "));
|
||
}
|
||
if (!teile.length) teile.push(tg === "*" && wt === "*" ? "täglich" : "");
|
||
return (teile.filter(Boolean).join(", ") + " " + takt).trim();
|
||
}
|
||
|
||
/* ------------------------------------------------------ Textformate */
|
||
function slug(s) {
|
||
const map = { "ä":"ae","ö":"oe","ü":"ue","ß":"ss","Ä":"ae","Ö":"oe","Ü":"ue" };
|
||
return String(s).replace(/[äöüßÄÖÜ]/g, (c) => map[c])
|
||
.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
|
||
.toLowerCase().replace(/[^a-z0-9]+/g, "-")
|
||
.replace(/^-+|-+$/g, "");
|
||
}
|
||
function woerter(s) {
|
||
return String(s).replace(/[_\-]+/g, " ")
|
||
.replace(/([a-zäöü])([A-ZÄÖÜ])/g, "$1 $2")
|
||
.split(/\s+/).filter(Boolean);
|
||
}
|
||
function camel(s) {
|
||
const w = woerter(s).map((x) => x.toLowerCase());
|
||
return w.map((x, i) => i ? x[0].toUpperCase() + x.slice(1) : x).join("");
|
||
}
|
||
function snake(s) { return woerter(s).map((x) => x.toLowerCase()).join("_"); }
|
||
function kebab(s) { return woerter(s).map((x) => x.toLowerCase()).join("-"); }
|
||
function titel(s) {
|
||
return String(s).replace(/\S+/g, (w) => w[0].toUpperCase() + w.slice(1).toLowerCase());
|
||
}
|
||
function textStat(s) {
|
||
const t = String(s);
|
||
const w = t.trim() ? t.trim().split(/\s+/).length : 0;
|
||
return {
|
||
zeichen: t.length,
|
||
ohneLeer: t.replace(/\s/g, "").length,
|
||
woerter: w,
|
||
zeilen: t ? t.split(/\r?\n/).length : 0,
|
||
absaetze: t.trim() ? t.trim().split(/\n\s*\n/).length : 0,
|
||
bytes: (typeof TextEncoder !== "undefined") ? new TextEncoder().encode(t).length : t.length,
|
||
};
|
||
}
|
||
|
||
/* JSON → YAML (vollständig für JSON-Datentypen). */
|
||
function jsonZuYaml(wert, tiefe) {
|
||
tiefe = tiefe || 0;
|
||
const ein = " ".repeat(tiefe);
|
||
const skalar = (v) => {
|
||
if (v === null) return "null";
|
||
if (typeof v === "boolean" || typeof v === "number") return String(v);
|
||
const s = String(v);
|
||
if (s === "") return '""';
|
||
if (/^[\s]|[\s]$|[:#\-?&*!|>'"%@`{}\[\],]|^(true|false|null|yes|no|on|off)$/i.test(s)
|
||
|| /^-?\d/.test(s) || s.indexOf("\n") >= 0) {
|
||
return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n") + '"';
|
||
}
|
||
return s;
|
||
};
|
||
if (Array.isArray(wert)) {
|
||
if (!wert.length) return ein + "[]";
|
||
return wert.map((v) => {
|
||
if (v !== null && typeof v === "object") {
|
||
const inner = jsonZuYaml(v, tiefe + 1);
|
||
return ein + "-\n" + inner;
|
||
}
|
||
return ein + "- " + skalar(v);
|
||
}).join("\n");
|
||
}
|
||
if (wert !== null && typeof wert === "object") {
|
||
const keys = Object.keys(wert);
|
||
if (!keys.length) return ein + "{}";
|
||
return keys.map((k) => {
|
||
const v = wert[k];
|
||
if (v !== null && typeof v === "object" &&
|
||
(Array.isArray(v) ? v.length : Object.keys(v).length)) {
|
||
return ein + k + ":\n" + jsonZuYaml(v, tiefe + 1);
|
||
}
|
||
if (v !== null && typeof v === "object") {
|
||
return ein + k + ": " + (Array.isArray(v) ? "[]" : "{}");
|
||
}
|
||
return ein + k + ": " + skalar(v);
|
||
}).join("\n");
|
||
}
|
||
return ein + skalar(wert);
|
||
}
|
||
|
||
/* YAML → JSON, bewusst nur für den einfachen Teil der Sprache:
|
||
verschachtelte Zuordnungen, Listen und Skalare. Keine Anker,
|
||
keine mehrzeiligen Blöcke, keine Fluss-Syntax. */
|
||
function yamlZuJson(text) {
|
||
const zeilen = String(text).replace(/\r/g, "").split("\n")
|
||
.filter((z) => z.trim() && !/^\s*#/.test(z));
|
||
let i = 0;
|
||
function skalar(s) {
|
||
const t = s.trim();
|
||
if (t === "") return null;
|
||
if (/^".*"$/.test(t)) return t.slice(1, -1).replace(/\\n/g, "\n").replace(/\\"/g, '"');
|
||
if (/^'.*'$/.test(t)) return t.slice(1, -1);
|
||
if (t === "null" || t === "~") return null;
|
||
if (t === "true") return true;
|
||
if (t === "false") return false;
|
||
if (/^-?\d+$/.test(t)) return parseInt(t, 10);
|
||
if (/^-?\d*\.\d+$/.test(t)) return parseFloat(t);
|
||
if (t === "[]") return [];
|
||
if (t === "{}") return {};
|
||
return t;
|
||
}
|
||
function einzug(z) { return z.match(/^\s*/)[0].length; }
|
||
function block(minEin) {
|
||
// Liste?
|
||
if (i < zeilen.length && einzug(zeilen[i]) >= minEin && /^\s*-\s?/.test(zeilen[i])) {
|
||
const liste = [];
|
||
const ein = einzug(zeilen[i]);
|
||
while (i < zeilen.length && einzug(zeilen[i]) === ein && /^\s*-\s?/.test(zeilen[i])) {
|
||
const rest = zeilen[i].replace(/^\s*-\s?/, "");
|
||
i++;
|
||
if (!rest.trim()) {
|
||
liste.push(i < zeilen.length && einzug(zeilen[i]) > ein ? block(ein + 1) : null);
|
||
} else if (/^[^:\s][^:]*:(\s|$)/.test(rest)) {
|
||
// "- schluessel: wert" — eigene Zuordnung, Einzug künstlich erhöhen
|
||
const unter = [rest];
|
||
const tiefer = ein + 2;
|
||
while (i < zeilen.length && einzug(zeilen[i]) >= tiefer) { unter.push(zeilen[i].slice(tiefer - 0)); i++; }
|
||
liste.push(yamlZuJson(unter.join("\n")));
|
||
} else {
|
||
liste.push(skalar(rest));
|
||
}
|
||
}
|
||
return liste;
|
||
}
|
||
// Zuordnung
|
||
const obj = {};
|
||
let ein = null;
|
||
while (i < zeilen.length) {
|
||
const z = zeilen[i];
|
||
const e = einzug(z);
|
||
if (ein === null) ein = e;
|
||
if (e < ein) break;
|
||
if (e > ein) { i++; continue; }
|
||
const m = z.trim().match(/^([^:]+):\s*(.*)$/);
|
||
if (!m) { i++; continue; }
|
||
const key = m[1].trim().replace(/^["']|["']$/g, "");
|
||
const rest = m[2];
|
||
i++;
|
||
if (rest.trim() === "") {
|
||
if (i < zeilen.length && einzug(zeilen[i]) > e) obj[key] = block(e + 1);
|
||
else if (i < zeilen.length && /^\s*-\s?/.test(zeilen[i]) && einzug(zeilen[i]) === e) obj[key] = block(e);
|
||
else obj[key] = null;
|
||
} else {
|
||
obj[key] = skalar(rest);
|
||
}
|
||
}
|
||
return obj;
|
||
}
|
||
return block(0);
|
||
}
|
||
|
||
/* CSV */
|
||
function csvLesen(text, trenner) {
|
||
const t = trenner || (String(text).split("\n")[0].indexOf(";") >= 0 ? ";" : ",");
|
||
const zeilen = [];
|
||
let feld = "", zeile = [], inQ = false;
|
||
const s = String(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||
for (let i = 0; i < s.length; i++) {
|
||
const c = s[i];
|
||
if (inQ) {
|
||
if (c === '"' && s[i+1] === '"') { feld += '"'; i++; }
|
||
else if (c === '"') inQ = false;
|
||
else feld += c;
|
||
} else if (c === '"') inQ = true;
|
||
else if (c === t) { zeile.push(feld); feld = ""; }
|
||
else if (c === "\n") { zeile.push(feld); zeilen.push(zeile); zeile = []; feld = ""; }
|
||
else feld += c;
|
||
}
|
||
if (feld !== "" || zeile.length) { zeile.push(feld); zeilen.push(zeile); }
|
||
return zeilen.filter((z) => z.length > 1 || (z[0] || "").trim() !== "");
|
||
}
|
||
function csvZuJson(text) {
|
||
const z = csvLesen(text);
|
||
if (!z.length) return [];
|
||
const kopf = z[0].map((h) => h.trim());
|
||
return z.slice(1).map((r) => {
|
||
const o = {};
|
||
kopf.forEach((h, i) => {
|
||
const v = (r[i] === undefined ? "" : r[i]).trim();
|
||
o[h] = v === "" ? null
|
||
: /^-?\d+$/.test(v) ? parseInt(v, 10)
|
||
: /^-?\d*[.,]\d+$/.test(v) ? parseFloat(v.replace(",", "."))
|
||
: v;
|
||
});
|
||
return o;
|
||
});
|
||
}
|
||
function jsonZuCsv(daten, trenner) {
|
||
const t = trenner || ";";
|
||
const arr = Array.isArray(daten) ? daten : [daten];
|
||
if (!arr.length) return "";
|
||
const kopf = [];
|
||
arr.forEach((o) => Object.keys(o || {}).forEach((k) => {
|
||
if (kopf.indexOf(k) < 0) kopf.push(k);
|
||
}));
|
||
const feld = (v) => {
|
||
if (v === null || v === undefined) return "";
|
||
const s = typeof v === "object" ? JSON.stringify(v) : String(v);
|
||
return /["\n;,]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
|
||
};
|
||
return [kopf.join(t)].concat(
|
||
arr.map((o) => kopf.map((k) => feld((o || {})[k])).join(t))
|
||
).join("\n");
|
||
}
|
||
|
||
function b64Kodieren(s) {
|
||
const bytes = new TextEncoder().encode(String(s));
|
||
let bin = "";
|
||
bytes.forEach((b) => { bin += String.fromCharCode(b); });
|
||
return btoa(bin);
|
||
}
|
||
function b64Dekodieren(s) {
|
||
const bin = atob(String(s).trim());
|
||
const bytes = new Uint8Array(bin.length);
|
||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||
return new TextDecoder().decode(bytes);
|
||
}
|
||
|
||
/* ----------------------------------------------------- Die Smart-Zeile
|
||
Erkennt selbst, was gemeint ist — deterministisch, in fester
|
||
Reihenfolge, damit dieselbe Eingabe immer dasselbe liefert. */
|
||
const TRENNER = /\s+(?:in|nach|to|als|zu)\s+|\s*(?:→|->|=>|>)\s*/i;
|
||
|
||
function zerlegeMenge(text) {
|
||
const s = String(text).trim();
|
||
// Zahl (auch Bruch) am Anfang, danach optional Einheit und Zutat
|
||
const m = s.match(/^(-?[\d.,\s/]*[\d½⅓⅔¼¾⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞](?:\s*\/\s*\d+)?)\s*(.*)$/);
|
||
if (!m) return null;
|
||
const zahl = parseNumber(m[1]);
|
||
if (isNaN(zahl)) return null;
|
||
const rest = (m[2] || "").trim();
|
||
if (!rest) return { zahl: zahl, einheit: null, zutat: "" };
|
||
// längste passende Einheit vom Anfang des Rests
|
||
const teile = rest.split(/\s+/);
|
||
for (let n = Math.min(teile.length, 3); n >= 1; n--) {
|
||
const kand = teile.slice(0, n).join(" ");
|
||
const e = findeEinheit(kand);
|
||
if (e) return { zahl: zahl, einheit: e, zutat: teile.slice(n).join(" ") };
|
||
}
|
||
const e1 = findeEinheit(rest);
|
||
if (e1) return { zahl: zahl, einheit: e1, zutat: "" };
|
||
return { zahl: zahl, einheit: null, zutat: rest };
|
||
}
|
||
|
||
function smart(eingabe) {
|
||
const roh = String(eingabe || "").trim();
|
||
if (!roh) return null;
|
||
|
||
/* 1 — Farbe */
|
||
if (/^#[0-9a-f]{3,8}$/i.test(roh) || /^(rgb|hsl)a?\(/i.test(roh)) {
|
||
const c = farbeLesen(roh);
|
||
if (c) {
|
||
const hsl = rgbZuHsl(c.r, c.g, c.b);
|
||
return { art: "Farbe", haupt: rgbZuHex(c.r, c.g, c.b), einheit: "",
|
||
farbe: rgbZuHex(c.r, c.g, c.b), alt: [
|
||
{ k: "RGB", v: `rgb(${Math.round(c.r)}, ${Math.round(c.g)}, ${Math.round(c.b)})` },
|
||
{ k: "HSL", v: `hsl(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%)` },
|
||
{ k: "RGB-Anteile", v: `${Math.round(c.r)} / ${Math.round(c.g)} / ${Math.round(c.b)}` },
|
||
] };
|
||
}
|
||
}
|
||
|
||
/* 2 — Cron (fünf Felder) */
|
||
if (/^[\d*/,\-]+(\s+[\d*/,\-]+){4}$/.test(roh)) {
|
||
const txt = cronText(roh);
|
||
if (txt) return { art: "Cron", haupt: txt, einheit: "", alt: [
|
||
{ k: "Ausdruck", v: roh },
|
||
] };
|
||
}
|
||
|
||
/* 3 — Zeitstempel / Datum */
|
||
if (/^\d{10}$/.test(roh) || /^\d{13}$/.test(roh) ||
|
||
/^\d{4}-\d{2}-\d{2}/.test(roh) || /^\d{1,2}\.\d{1,2}\.\d{4}/.test(roh)) {
|
||
const d = zeitstempelLesen(roh);
|
||
if (d && !isNaN(d.getTime())) {
|
||
return { art: "Zeitpunkt",
|
||
haupt: d.toLocaleString("de-DE", { dateStyle: "full", timeStyle: "short" }),
|
||
einheit: "", alt: [
|
||
{ k: "Unix (s)", v: String(Math.floor(d.getTime() / 1000)) },
|
||
{ k: "Unix (ms)", v: String(d.getTime()) },
|
||
{ k: "ISO 8601", v: d.toISOString() },
|
||
{ k: "Kalenderwoche", v: "KW " + kalenderwoche(d) },
|
||
] };
|
||
}
|
||
}
|
||
|
||
/* 4 — Zahlensysteme */
|
||
let m = roh.match(/^0x([0-9a-f]+)$/i) || roh.match(/^0b([01]+)$/i);
|
||
if (m) {
|
||
const basis = roh[1].toLowerCase() === "x" ? 16 : 2;
|
||
const n = parseInt(m[1], basis);
|
||
return { art: "Zahl", haupt: fmt(n), einheit: "", alt: [
|
||
{ k: "Hex", v: "0x" + n.toString(16).toUpperCase() },
|
||
{ k: "Binär", v: "0b" + n.toString(2) },
|
||
{ k: "Oktal", v: "0o" + n.toString(8) },
|
||
] };
|
||
}
|
||
m = roh.match(/^(-?[\d.,]+)\s*(?:in|nach|to|als)\s*(hex|hexadezimal|binär|binaer|bin|oktal|dezimal)$/i);
|
||
if (m) {
|
||
const n = parseNumber(m[1]);
|
||
if (!isNaN(n)) {
|
||
const z = Math.round(n);
|
||
const ziel = m[2].toLowerCase();
|
||
const wert = ziel.startsWith("hex") ? "0x" + z.toString(16).toUpperCase()
|
||
: ziel.startsWith("bin") ? "0b" + z.toString(2)
|
||
: ziel.startsWith("okt") ? "0o" + z.toString(8)
|
||
: fmt(z);
|
||
return { art: "Zahl", haupt: wert, einheit: "", alt: [
|
||
{ k: "Dezimal", v: fmt(z) },
|
||
{ k: "Hex", v: "0x" + z.toString(16).toUpperCase() },
|
||
{ k: "Binär", v: "0b" + z.toString(2) },
|
||
] };
|
||
}
|
||
}
|
||
|
||
/* 5 — Einheiten mit Ziel: „2 cups Mehl in g“ */
|
||
const stueck = roh.split(TRENNER);
|
||
if (stueck.length >= 2 && stueck[0].trim() && stueck[stueck.length - 1].trim()) {
|
||
const links = zerlegeMenge(stueck[0]);
|
||
const rechtsText = stueck[stueck.length - 1].trim();
|
||
const rechts = findeEinheit(rechtsText);
|
||
if (links && links.einheit && rechts) {
|
||
const zutat = links.zutat;
|
||
if (links.einheit.dim === rechts.dim) {
|
||
const w = umrechnen(links.zahl, links.einheit, rechts);
|
||
return { art: DIMS[rechts.dim] ? DIMS[rechts.dim].name : "Temperatur",
|
||
haupt: fmt(w), einheit: rechts.key,
|
||
alt: nachbarn(links.zahl, links.einheit, rechts) };
|
||
}
|
||
const k = kuecheRechnen(links.zahl, links.einheit, rechts, zutat);
|
||
if (k) {
|
||
const hinweis = k.geraten
|
||
? `Dichte unbekannt — mit ${fmt(k.dichte)} g/ml gerechnet (wie Wasser)`
|
||
: `Dichte ${fmt(k.dichte)} g/ml${zutat ? " für „" + zutat + "“" : ""}`;
|
||
return { art: "Küche", haupt: fmtKueche(k.wert), einheit: rechts.key,
|
||
warn: k.geraten ? hinweis : null,
|
||
alt: [{ k: "Grundlage", v: hinweis }] };
|
||
}
|
||
return { fehler: `„${links.einheit.key}“ und „${rechts.key}“ passen nicht zusammen.` };
|
||
}
|
||
if (links && !links.einheit) return { fehler: `Einheit „${links.zutat || stueck[0]}“ kenne ich nicht.` };
|
||
if (links && !rechts) return { fehler: `Zieleinheit „${rechtsText}“ kenne ich nicht.` };
|
||
}
|
||
|
||
/* 6 — nur Menge: Übersicht in derselben Größe */
|
||
const nur = zerlegeMenge(roh);
|
||
if (nur && nur.einheit) {
|
||
const dim = nur.einheit.dim;
|
||
const alt = [];
|
||
if (dim === "temperatur") {
|
||
for (const key in TEMP) {
|
||
if (key === nur.einheit.key) continue;
|
||
alt.push({ k: "°" + key, v: fmt(umrechnen(nur.zahl, nur.einheit, { dim: dim, key: key })) });
|
||
}
|
||
return { art: "Temperatur", haupt: fmt(nur.zahl), einheit: "°" + nur.einheit.key, alt: alt };
|
||
}
|
||
const u = DIMS[dim].units;
|
||
const sortiert = Object.keys(u).sort((a, b) => u[a].f - u[b].f);
|
||
sortiert.forEach((key) => {
|
||
if (key === nur.einheit.key) return;
|
||
alt.push({ k: key, v: fmt(umrechnen(nur.zahl, nur.einheit, { dim: dim, key: key })) });
|
||
});
|
||
return { art: DIMS[dim].name, haupt: fmt(nur.zahl), einheit: nur.einheit.key, alt: alt };
|
||
}
|
||
|
||
return { fehler: "Das habe ich nicht verstanden. Beispiel: „2 cups Mehl in g“." };
|
||
}
|
||
|
||
/* Sinnvolle Nachbareinheiten zum Ergebnis (max. 5). */
|
||
function nachbarn(wert, von, ziel) {
|
||
const dim = ziel.dim;
|
||
const alt = [];
|
||
if (dim === "temperatur") {
|
||
for (const key in TEMP) {
|
||
if (key === ziel.key) continue;
|
||
alt.push({ k: "°" + key, v: fmt(umrechnen(wert, von, { dim: dim, key: key })) });
|
||
}
|
||
return alt;
|
||
}
|
||
const u = DIMS[dim].units;
|
||
const keys = Object.keys(u).sort((a, b) => u[a].f - u[b].f);
|
||
const pos = keys.indexOf(ziel.key);
|
||
keys.slice(Math.max(0, pos - 2), pos + 4).forEach((key) => {
|
||
if (key === ziel.key) return;
|
||
alt.push({ k: key, v: fmt(umrechnen(wert, von, { dim: dim, key: key })) });
|
||
});
|
||
return alt.slice(0, 6);
|
||
}
|
||
|
||
return {
|
||
parseNumber, fmt, fmtKueche, DIMS, TEMP, LOOKUP, findeEinheit, umrechnen,
|
||
DICHTE, dichteVon, kuecheRechnen, ofen,
|
||
hexZuRgb, rgbZuHex, rgbZuHsl, hslZuRgb, farbeLesen,
|
||
zeitstempelLesen, kalenderwoche, cronText,
|
||
slug, camel, snake, kebab, titel, textStat,
|
||
jsonZuYaml, yamlZuJson, csvZuJson, jsonZuCsv, csvLesen,
|
||
b64Kodieren, b64Dekodieren,
|
||
zerlegeMenge, smart, nachbarn,
|
||
};
|
||
})();
|
||
/* ══════════════════════════════════════════════ ENGINE ENDE ═══════════ */
|
||
|
||
/* ------------------------------------------------------------ Oberfläche */
|
||
(function () {
|
||
if (typeof document === "undefined") return;
|
||
const $ = (id) => document.getElementById(id);
|
||
const esc = (s) => (window.BT && BT.escape) ? BT.escape(s)
|
||
: String(s == null ? "" : s).replace(/[&<>"]/g, (c) =>
|
||
({ "&":"&", "<":"<", ">":">", '"':""" }[c]));
|
||
|
||
/* Reiter */
|
||
$("tabs").addEventListener("click", (e) => {
|
||
const b = e.target.closest("button[data-tab]");
|
||
if (!b) return;
|
||
document.querySelectorAll("#tabs button").forEach((x) => x.classList.remove("active"));
|
||
document.querySelectorAll("main > .tab").forEach((x) => x.classList.add("bt-hidden"));
|
||
b.classList.add("active");
|
||
$("tab-" + b.dataset.tab).classList.remove("bt-hidden");
|
||
});
|
||
|
||
/* Ablage im Browser — bewusst lokal, es verlässt nichts das Gerät. */
|
||
const Speicher = {
|
||
lesen(key, standard) {
|
||
try { const r = localStorage.getItem("btc." + key); return r ? JSON.parse(r) : standard; }
|
||
catch (_) { return standard; }
|
||
},
|
||
schreiben(key, wert) {
|
||
try { localStorage.setItem("btc." + key, JSON.stringify(wert)); } catch (_) {}
|
||
},
|
||
};
|
||
let verlauf = Speicher.lesen("verlauf", []);
|
||
let merkliste = Speicher.lesen("pins", []);
|
||
|
||
function kopieren(text) {
|
||
const fertig = () => { if (window.BT && BT.toast) BT.toast("Kopiert.", "ok"); };
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(fertig, () => {});
|
||
} else {
|
||
const ta = document.createElement("textarea");
|
||
ta.value = text; document.body.appendChild(ta); ta.select();
|
||
try { document.execCommand("copy"); fertig(); } catch (_) {}
|
||
document.body.removeChild(ta);
|
||
}
|
||
}
|
||
|
||
/* Klick auf eine Zeile kopiert deren Wert. */
|
||
document.addEventListener("click", (e) => {
|
||
const kv = e.target.closest(".kv");
|
||
if (kv && kv.dataset.copy) kopieren(kv.dataset.copy);
|
||
});
|
||
|
||
function zeile(k, v) {
|
||
return `<div class="kv" data-copy="${esc(v)}" title="Klicken zum Kopieren">
|
||
<span class="k">${esc(k)}</span><span class="v">${esc(v)}</span></div>`;
|
||
}
|
||
function ergebnisHtml(r) {
|
||
if (!r) return "";
|
||
if (r.fehler) return `<div class="err">${esc(r.fehler)}</div>`;
|
||
const kopf = `<div class="kind">${esc(r.art)}</div>
|
||
<div class="main">${r.farbe ? `<span class="swatch" style="background:${esc(r.farbe)}"></span>` : ""}
|
||
<span>${esc(r.haupt)}</span>${r.einheit ? `<span class="unit">${esc(r.einheit)}</span>` : ""}</div>`;
|
||
const warn = r.warn ? `<p class="note" style="color:var(--bt-warn)">⚠ ${esc(r.warn)}</p>` : "";
|
||
const alt = (r.alt && r.alt.length)
|
||
? `<div class="alts">${r.alt.map((a) => zeile(a.k, a.v)).join("")}</div>` : "";
|
||
return kopf + warn + alt;
|
||
}
|
||
|
||
/* ---------------------------------------------------------- Smart-Zeile */
|
||
const BEISPIELE = ["2 cups Mehl in g", "180 C in F", "1699999999", "#3b82f6",
|
||
"2,5 TB in GiB", "1 1/2 EL in ml", "5 km in mi", "0 3 * * 1"];
|
||
$("examples").innerHTML = BEISPIELE.map((b) =>
|
||
`<button type="button" data-bsp="${esc(b)}">${esc(b)}</button>`).join("");
|
||
$("examples").addEventListener("click", (e) => {
|
||
const b = e.target.closest("button[data-bsp]");
|
||
if (!b) return;
|
||
$("smart").value = b.dataset.bsp;
|
||
smartRechnen(true);
|
||
});
|
||
|
||
let letzteEingabe = "";
|
||
function smartRechnen(merken) {
|
||
const roh = $("smart").value;
|
||
const r = BTC.smart(roh);
|
||
$("res").innerHTML = ergebnisHtml(r);
|
||
if (merken && r && !r.fehler && roh.trim() && roh.trim() !== letzteEingabe) {
|
||
letzteEingabe = roh.trim();
|
||
verlauf = [{ q: roh.trim(), a: r.haupt + (r.einheit ? " " + r.einheit : "") }]
|
||
.concat(verlauf.filter((v) => v.q !== roh.trim())).slice(0, 12);
|
||
Speicher.schreiben("verlauf", verlauf);
|
||
zeichneVerlauf();
|
||
}
|
||
}
|
||
$("smart").addEventListener("input", () => smartRechnen(false));
|
||
$("smart").addEventListener("change", () => smartRechnen(true));
|
||
$("smart").addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter") { smartRechnen(true); anheften(); }
|
||
});
|
||
|
||
function anheften() {
|
||
const q = $("smart").value.trim();
|
||
if (!q) return;
|
||
const r = BTC.smart(q);
|
||
if (!r || r.fehler) return;
|
||
if (!merkliste.some((p) => p.q === q)) {
|
||
merkliste = [{ q: q }].concat(merkliste).slice(0, 10);
|
||
Speicher.schreiben("pins", merkliste);
|
||
zeichnePins();
|
||
}
|
||
}
|
||
function zeichnePins() {
|
||
if (!merkliste.length) { $("pins").innerHTML = ""; return; }
|
||
$("pins").innerHTML = merkliste.map((p, i) => {
|
||
const r = BTC.smart(p.q);
|
||
const wert = r && !r.fehler ? r.haupt + (r.einheit ? " " + r.einheit : "") : "?";
|
||
return `<span class="pin" data-pin="${i}"><span>${esc(p.q)}</span>
|
||
<b>${esc(wert)}</b><span class="x" data-del="${i}">✕</span></span>`;
|
||
}).join("");
|
||
}
|
||
$("pins").addEventListener("click", (e) => {
|
||
const del = e.target.closest("[data-del]");
|
||
if (del) {
|
||
merkliste.splice(Number(del.dataset.del), 1);
|
||
Speicher.schreiben("pins", merkliste); zeichnePins(); return;
|
||
}
|
||
const pin = e.target.closest("[data-pin]");
|
||
if (pin) { $("smart").value = merkliste[Number(pin.dataset.pin)].q; smartRechnen(false); }
|
||
});
|
||
function zeichneVerlauf() {
|
||
if (!verlauf.length) { $("hist").innerHTML = `<span class="bt-muted">Noch nichts umgerechnet.</span>`; return; }
|
||
$("hist").innerHTML = verlauf.map((v, i) =>
|
||
`<div data-h="${i}">${esc(v.q)} → <b>${esc(v.a)}</b></div>`).join("");
|
||
}
|
||
$("hist").addEventListener("click", (e) => {
|
||
const d = e.target.closest("[data-h]");
|
||
if (!d) return;
|
||
$("smart").value = verlauf[Number(d.dataset.h)].q;
|
||
$("smart").scrollIntoView({ behavior: "smooth", block: "start" });
|
||
smartRechnen(false);
|
||
});
|
||
|
||
/* ------------------------------------------------------------- Küche */
|
||
const KUECHE_EINHEITEN = ["ml","l","tl","el","cup","floz","g","kg","oz","lb"];
|
||
function fuelleSelect(el, keys, standard) {
|
||
el.innerHTML = keys.map((k) => `<option value="${k}"${k === standard ? " selected" : ""}>${k}</option>`).join("");
|
||
}
|
||
fuelleSelect($("kVon"), KUECHE_EINHEITEN, "cup");
|
||
fuelleSelect($("kNach"), KUECHE_EINHEITEN, "g");
|
||
$("kZutat").innerHTML = `<option value="">— ohne (wie Wasser) —</option>` +
|
||
Object.keys(BTC.DICHTE).sort().map((z) => `<option value="${esc(z)}">${esc(z)}</option>`).join("");
|
||
$("kZutat").value = "mehl";
|
||
|
||
function kuecheRechnenUI() {
|
||
const w = BTC.parseNumber($("kMenge").value);
|
||
if (isNaN(w)) { $("kOut").innerHTML = `<div class="err">Menge nicht lesbar.</div>`; return; }
|
||
const von = BTC.findeEinheit($("kVon").value), nach = BTC.findeEinheit($("kNach").value);
|
||
const k = BTC.kuecheRechnen(w, von, nach, $("kZutat").value);
|
||
if (!k) { $("kOut").innerHTML = `<div class="err">Passt nicht zusammen.</div>`; return; }
|
||
const alt = [];
|
||
if (k.dichte !== null) alt.push({ k: "Dichte", v: BTC.fmt(k.dichte) + " g/ml" });
|
||
$("kOut").innerHTML = ergebnisHtml({
|
||
art: "Küche", haupt: BTC.fmtKueche(k.wert), einheit: nach.key,
|
||
warn: k.geraten ? "Dichte unbekannt — wie Wasser gerechnet." : null, alt: alt,
|
||
});
|
||
}
|
||
["kMenge","kVon","kNach","kZutat"].forEach((id) =>
|
||
$(id).addEventListener("input", kuecheRechnenUI));
|
||
kuecheRechnenUI();
|
||
|
||
function ofenUI() {
|
||
const w = BTC.parseNumber($("oTemp").value);
|
||
if (isNaN(w)) { $("oOut").innerHTML = `<div class="err">Zahl nicht lesbar.</div>`; return; }
|
||
const o = BTC.ofen(w, $("oEinheit").value);
|
||
if (!o) { $("oOut").innerHTML = ""; return; }
|
||
$("oOut").innerHTML = ergebnisHtml({
|
||
art: "Ofen", haupt: BTC.fmt(o.c, 0), einheit: "°C Ober-/Unterhitze",
|
||
alt: [
|
||
{ k: "Umluft", v: BTC.fmt(o.umluft, 0) + " °C" },
|
||
{ k: "Fahrenheit", v: BTC.fmt(o.f, 0) + " °F" },
|
||
{ k: "Gasstufe", v: (o.gasGenau ? "" : "≈ ") + o.gas },
|
||
],
|
||
});
|
||
}
|
||
["oTemp","oEinheit"].forEach((id) => $(id).addEventListener("input", ofenUI));
|
||
ofenUI();
|
||
|
||
$("loeffel").innerHTML = [
|
||
["1 TL", "5 ml"], ["1 EL", "15 ml"], ["1 EL", "3 TL"],
|
||
["1 US-Cup", "236,6 ml"], ["1 US-Cup", "16 EL"],
|
||
["1 Msp", "≈ 0,5 ml"], ["1 Prise", "≈ 0,3 ml"],
|
||
].map((r) => zeile(r[0], r[1])).join("");
|
||
|
||
function portionUI() {
|
||
const a = BTC.parseNumber($("pVon").value), b = BTC.parseNumber($("pNach").value);
|
||
const teil = BTC.zerlegeMenge($("pMenge").value);
|
||
if (isNaN(a) || isNaN(b) || !a) { $("pOut").innerHTML = ""; return; }
|
||
if (!teil) { $("pOut").innerHTML = `<div class="err">Menge nicht lesbar.</div>`; return; }
|
||
const faktor = b / a;
|
||
const neu = teil.zahl * faktor;
|
||
$("pOut").innerHTML = ergebnisHtml({
|
||
art: "Portionen", haupt: BTC.fmtKueche(neu),
|
||
einheit: (teil.einheit ? teil.einheit.key : "") + (teil.zutat ? " " + teil.zutat : ""),
|
||
alt: [{ k: "Faktor", v: "× " + BTC.fmt(faktor) }],
|
||
});
|
||
}
|
||
["pVon","pNach","pMenge"].forEach((id) => $(id).addEventListener("input", portionUI));
|
||
|
||
/* -------------------------------------------------------------- Maße */
|
||
const MASS_DIMS = ["laenge","masse","volumen","flaeche","zeit","tempo","energie","druck"];
|
||
$("mDim").innerHTML = MASS_DIMS.map((d) =>
|
||
`<option value="${d}">${esc(BTC.DIMS[d].name)}</option>`).join("") +
|
||
`<option value="temperatur">Temperatur</option>`;
|
||
function massEinheiten() {
|
||
const dim = $("mDim").value;
|
||
const keys = dim === "temperatur" ? Object.keys(BTC.TEMP) : Object.keys(BTC.DIMS[dim].units);
|
||
fuelleSelect($("mVon"), keys, keys[0]);
|
||
massUI();
|
||
}
|
||
function massUI() {
|
||
const dim = $("mDim").value;
|
||
const w = BTC.parseNumber($("mWert").value);
|
||
if (isNaN(w)) { $("mOut").innerHTML = `<div class="err">Zahl nicht lesbar.</div>`; return; }
|
||
const von = { dim: dim, key: $("mVon").value };
|
||
const keys = dim === "temperatur" ? Object.keys(BTC.TEMP)
|
||
: Object.keys(BTC.DIMS[dim].units).sort((a, b) =>
|
||
BTC.DIMS[dim].units[a].f - BTC.DIMS[dim].units[b].f);
|
||
const alt = keys.filter((k) => k !== von.key).map((k) => ({
|
||
k: dim === "temperatur" ? "°" + k : k,
|
||
v: BTC.fmt(BTC.umrechnen(w, von, { dim: dim, key: k })),
|
||
}));
|
||
$("mOut").innerHTML = ergebnisHtml({
|
||
art: dim === "temperatur" ? "Temperatur" : BTC.DIMS[dim].name,
|
||
haupt: BTC.fmt(w), einheit: dim === "temperatur" ? "°" + von.key : von.key, alt: alt,
|
||
});
|
||
}
|
||
$("mDim").addEventListener("change", massEinheiten);
|
||
["mWert","mVon"].forEach((id) => $(id).addEventListener("input", massUI));
|
||
massEinheiten();
|
||
|
||
/* ----------------------------------------------------------- Technik */
|
||
fuelleSelect($("dVon"), Object.keys(BTC.DIMS.daten.units), "GB");
|
||
function datenUI() {
|
||
const w = BTC.parseNumber($("dWert").value);
|
||
if (isNaN(w)) { $("dOut").innerHTML = `<div class="err">Zahl nicht lesbar.</div>`; return; }
|
||
const von = { dim: "daten", key: $("dVon").value };
|
||
const keys = Object.keys(BTC.DIMS.daten.units)
|
||
.sort((a, b) => BTC.DIMS.daten.units[a].f - BTC.DIMS.daten.units[b].f);
|
||
$("dOut").innerHTML = ergebnisHtml({
|
||
art: "Datenmenge", haupt: BTC.fmt(w), einheit: von.key,
|
||
alt: keys.filter((k) => k !== von.key).map((k) => ({
|
||
k: k, v: BTC.fmt(BTC.umrechnen(w, von, { dim: "daten", key: k })) })),
|
||
});
|
||
}
|
||
["dWert","dVon"].forEach((id) => $(id).addEventListener("input", datenUI));
|
||
datenUI();
|
||
|
||
function zahlUI() {
|
||
const basis = Number($("zVon").value);
|
||
const roh = $("zWert").value.trim().replace(/^0[xbo]/i, "");
|
||
if (!roh) { $("zOut").innerHTML = ""; return; }
|
||
const n = parseInt(roh, basis);
|
||
if (isNaN(n)) { $("zOut").innerHTML = `<div class="err">Keine gültige Zahl zur Basis ${basis}.</div>`; return; }
|
||
$("zOut").innerHTML = ergebnisHtml({
|
||
art: "Zahl", haupt: BTC.fmt(n), einheit: "dezimal", alt: [
|
||
{ k: "Hex", v: n.toString(16).toUpperCase() },
|
||
{ k: "Binär", v: n.toString(2) },
|
||
{ k: "Oktal", v: n.toString(8) },
|
||
{ k: "Als Bytes", v: BTC.fmt(n / 1024) + " KiB" },
|
||
],
|
||
});
|
||
}
|
||
["zWert","zVon"].forEach((id) => $(id).addEventListener("input", zahlUI));
|
||
zahlUI();
|
||
|
||
function farbeUI(quelle) {
|
||
const c = BTC.farbeLesen($("fWert").value);
|
||
if (!c) { $("fOut").innerHTML = `<div class="err">Farbe nicht erkannt.</div>`; return; }
|
||
const hex = BTC.rgbZuHex(c.r, c.g, c.b);
|
||
const hsl = BTC.rgbZuHsl(c.r, c.g, c.b);
|
||
$("fSwatch").style.background = hex;
|
||
if (quelle !== "pick") $("fPick").value = hex;
|
||
$("fOut").innerHTML = ergebnisHtml({
|
||
art: "Farbe", haupt: hex, einheit: "", alt: [
|
||
{ k: "RGB", v: `rgb(${Math.round(c.r)}, ${Math.round(c.g)}, ${Math.round(c.b)})` },
|
||
{ k: "HSL", v: `hsl(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%)` },
|
||
{ k: "Helligkeit", v: BTC.fmt(hsl.l, 0) + " %" },
|
||
],
|
||
});
|
||
}
|
||
$("fWert").addEventListener("input", () => farbeUI("text"));
|
||
$("fPick").addEventListener("input", () => { $("fWert").value = $("fPick").value; farbeUI("pick"); });
|
||
farbeUI("text");
|
||
|
||
/* ----------------------------------------------------------- Formate */
|
||
function formatRechnen() {
|
||
const op = $("tOp").value;
|
||
const q = $("tIn").value;
|
||
let out = "", note = "";
|
||
try {
|
||
if (op === "b64e") out = BTC.b64Kodieren(q);
|
||
else if (op === "b64d") out = BTC.b64Dekodieren(q);
|
||
else if (op === "urle") out = encodeURIComponent(q);
|
||
else if (op === "urld") out = decodeURIComponent(q);
|
||
else if (op === "slug") out = BTC.slug(q);
|
||
else if (op === "upper") out = q.toUpperCase();
|
||
else if (op === "lower") out = q.toLowerCase();
|
||
else if (op === "title") out = BTC.titel(q);
|
||
else if (op === "camel") out = BTC.camel(q);
|
||
else if (op === "snake") out = BTC.snake(q);
|
||
else if (op === "kebab") out = BTC.kebab(q);
|
||
else if (op === "trimlines") {
|
||
out = q.split(/\r?\n/).map((z) => z.replace(/\s+$/g, ""))
|
||
.filter((z, i, a) => z.trim() || (a[i-1] || "").trim()).join("\n").trim();
|
||
}
|
||
else if (op === "stats") {
|
||
const s = BTC.textStat(q);
|
||
out = [`Zeichen: ${s.zeichen}`, `ohne Leerzeichen: ${s.ohneLeer}`,
|
||
`Wörter: ${s.woerter}`, `Zeilen: ${s.zeilen}`,
|
||
`Absätze: ${s.absaetze}`, `Bytes (UTF-8): ${s.bytes}`].join("\n");
|
||
}
|
||
else if (op === "jsonp") out = JSON.stringify(JSON.parse(q), null, 2);
|
||
else if (op === "jsonm") out = JSON.stringify(JSON.parse(q));
|
||
else if (op === "json2yaml") out = BTC.jsonZuYaml(JSON.parse(q));
|
||
else if (op === "yaml2json") {
|
||
out = JSON.stringify(BTC.yamlZuJson(q), null, 2);
|
||
note = "YAML wird nur im einfachen Teil der Sprache gelesen: verschachtelte "
|
||
+ "Zuordnungen, Listen und Skalare. Keine Anker, keine mehrzeiligen Blöcke.";
|
||
}
|
||
else if (op === "json2csv") out = BTC.jsonZuCsv(JSON.parse(q));
|
||
else if (op === "csv2json") out = JSON.stringify(BTC.csvZuJson(q), null, 2);
|
||
} catch (err) {
|
||
out = "";
|
||
note = "Fehler: " + err.message;
|
||
}
|
||
$("tOut").value = out;
|
||
$("tNote").textContent = note;
|
||
$("tNote").style.color = note.startsWith("Fehler") ? "var(--bt-err)" : "";
|
||
}
|
||
["tOp","tIn"].forEach((id) => $(id).addEventListener("input", formatRechnen));
|
||
$("tSwap").addEventListener("click", () => {
|
||
if (!$("tOut").value) return;
|
||
$("tIn").value = $("tOut").value; formatRechnen();
|
||
});
|
||
$("tCopy").addEventListener("click", () => kopieren($("tOut").value));
|
||
|
||
/* -------------------------------------------------------------- Zeit */
|
||
function tsUI() {
|
||
const roh = $("tsWert").value.trim();
|
||
if (!roh) { $("tsOut").innerHTML = ""; return; }
|
||
const d = BTC.zeitstempelLesen(roh);
|
||
if (!d || isNaN(d.getTime())) { $("tsOut").innerHTML = `<div class="err">Nicht erkannt.</div>`; return; }
|
||
const jetzt = Date.now();
|
||
const diff = Math.round((d.getTime() - jetzt) / 1000);
|
||
const rel = Math.abs(diff) < 60 ? `${Math.abs(diff)} s`
|
||
: Math.abs(diff) < 3600 ? `${Math.round(Math.abs(diff)/60)} min`
|
||
: Math.abs(diff) < 86400 ? `${Math.round(Math.abs(diff)/3600)} h`
|
||
: `${Math.round(Math.abs(diff)/86400)} Tage`;
|
||
$("tsOut").innerHTML = ergebnisHtml({
|
||
art: "Zeitpunkt",
|
||
haupt: d.toLocaleString("de-DE", { dateStyle: "full", timeStyle: "medium" }),
|
||
einheit: "", alt: [
|
||
{ k: "Unix (s)", v: String(Math.floor(d.getTime() / 1000)) },
|
||
{ k: "Unix (ms)", v: String(d.getTime()) },
|
||
{ k: "ISO 8601", v: d.toISOString() },
|
||
{ k: "Kalenderwoche", v: "KW " + BTC.kalenderwoche(d) },
|
||
{ k: diff < 0 ? "her" : "hin", v: rel },
|
||
],
|
||
});
|
||
}
|
||
$("tsWert").addEventListener("input", tsUI);
|
||
$("tsNow").addEventListener("click", () => {
|
||
$("tsWert").value = String(Math.floor(Date.now() / 1000)); tsUI();
|
||
});
|
||
|
||
const ZONEN = ["Europe/Berlin","Europe/London","Europe/Lisbon","Europe/Moscow",
|
||
"America/New_York","America/Chicago","America/Denver","America/Los_Angeles",
|
||
"America/Sao_Paulo","Asia/Dubai","Asia/Kolkata","Asia/Bangkok",
|
||
"Asia/Shanghai","Asia/Tokyo","Australia/Sydney","Pacific/Auckland","UTC"];
|
||
$("tzZone").innerHTML = ZONEN.map((z) =>
|
||
`<option value="${z}"${z === "America/New_York" ? " selected" : ""}>${z}</option>`).join("");
|
||
function tzUI() {
|
||
const roh = $("tzWert").value.trim();
|
||
const d = roh ? BTC.zeitstempelLesen(roh) : new Date();
|
||
if (!d || isNaN(d.getTime())) { $("tzOut").innerHTML = `<div class="err">Nicht erkannt.</div>`; return; }
|
||
const ziel = $("tzZone").value;
|
||
const zeig = (tz) => {
|
||
try {
|
||
return d.toLocaleString("de-DE", { timeZone: tz, dateStyle: "medium", timeStyle: "short" });
|
||
} catch (_) { return "—"; }
|
||
};
|
||
$("tzOut").innerHTML = ergebnisHtml({
|
||
art: "Zeitzone", haupt: zeig(ziel), einheit: ziel, alt: [
|
||
{ k: "Hier", v: d.toLocaleString("de-DE", { dateStyle: "medium", timeStyle: "short" }) },
|
||
{ k: "UTC", v: zeig("UTC") },
|
||
],
|
||
});
|
||
}
|
||
["tzWert","tzZone"].forEach((id) => $(id).addEventListener("input", tzUI));
|
||
tzUI();
|
||
|
||
function dvUI() {
|
||
const a = $("dvA").value, b = $("dvB").value;
|
||
if (!a || !b) { $("dvOut").innerHTML = ""; return; }
|
||
const da = new Date(a + "T00:00:00"), db = new Date(b + "T00:00:00");
|
||
const tage = Math.round((db - da) / 86400000);
|
||
const wochen = tage / 7;
|
||
let jahre = db.getFullYear() - da.getFullYear();
|
||
let monate = jahre * 12 + (db.getMonth() - da.getMonth());
|
||
if (db.getDate() < da.getDate()) monate--;
|
||
$("dvOut").innerHTML = ergebnisHtml({
|
||
art: "Differenz", haupt: BTC.fmt(tage, 0), einheit: "Tage", alt: [
|
||
{ k: "Wochen", v: BTC.fmt(wochen) },
|
||
{ k: "Monate (ganz)", v: String(monate) },
|
||
{ k: "Stunden", v: BTC.fmt(tage * 24, 0) },
|
||
{ k: "KW von / bis", v: "KW " + BTC.kalenderwoche(da) + " → KW " + BTC.kalenderwoche(db) },
|
||
],
|
||
});
|
||
}
|
||
["dvA","dvB"].forEach((id) => $(id).addEventListener("input", dvUI));
|
||
(function () {
|
||
const heute = new Date();
|
||
$("dvA").value = heute.toISOString().slice(0, 10);
|
||
const spaeter = new Date(heute.getTime() + 30 * 86400000);
|
||
$("dvB").value = spaeter.toISOString().slice(0, 10);
|
||
dvUI();
|
||
})();
|
||
|
||
function crUI() {
|
||
const t = BTC.cronText($("crWert").value);
|
||
$("crOut").innerHTML = t
|
||
? ergebnisHtml({ art: "Cron", haupt: t, einheit: "", alt: [] })
|
||
: `<div class="err">Fünf Felder erwartet, z. B. <code>0 3 * * 1</code>.</div>`;
|
||
}
|
||
$("crWert").addEventListener("input", crUI);
|
||
crUI();
|
||
|
||
/* Start */
|
||
zeichnePins();
|
||
zeichneVerlauf();
|
||
$("smart").focus();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|