feat: Initialize SvelteKit project, add tsconfig.json, and introduce a new Calculator.svelte component.

This commit is contained in:
Ben
2026-03-06 21:18:58 -08:00
parent e35fdcb118
commit 47042ee5a7
30 changed files with 3887 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
// ─── Pure conversion engine ─────────────────────────────────────────
// No DOM dependencies — just math. Used by the Calculator component.
import type { CalculatorDef } from './data/calculators';
export interface SolveResult {
val1: string;
val2: string;
val3: string;
}
function fmt(n: number): string {
return parseFloat(n.toFixed(6)).toString();
}
function gcd(a: number, b: number): number {
a = Math.abs(Math.round(a));
b = Math.abs(Math.round(b));
return b ? gcd(b, a % b) : a;
}
/**
* Run the conversion for a given calculator definition.
* `source` indicates which field the user is typing in (1, 2, or 3).
* Returns new string values for all fields.
*/
export function solve(
calc: CalculatorDef,
source: 1 | 2 | 3,
rawVal1: string,
rawVal2: string,
rawVal3: string
): SolveResult {
const v1 = parseFloat(rawVal1);
const v2 = parseFloat(rawVal2);
const v3 = parseFloat(rawVal3);
const factor = calc.factor ?? 1;
const offset = calc.offset ?? 0;
let out: SolveResult = { val1: rawVal1, val2: rawVal2, val3: rawVal3 };
switch (calc.type) {
case 'standard':
if (source === 1) {
out.val2 = !isNaN(v1) ? fmt(v1 * factor + offset) : '';
} else {
out.val1 = !isNaN(v2) ? fmt((v2 - offset) / factor) : '';
}
break;
case 'inverse':
if (source === 1) {
out.val2 = (!isNaN(v1) && v1 !== 0) ? fmt(factor / v1) : '';
} else {
out.val1 = (!isNaN(v2) && v2 !== 0) ? fmt(factor / v2) : '';
}
break;
case '3col':
if (source === 1 || source === 2) {
out.val3 = (!isNaN(v1) && !isNaN(v2) && v2 !== 0) ? fmt(v1 / v2) : '';
} else {
out.val1 = (!isNaN(v3) && !isNaN(v2)) ? fmt(v3 * v2) : '';
}
break;
case '3col-mul':
if (source === 1 || source === 2) {
out.val3 = (!isNaN(v1) && !isNaN(v2)) ? fmt(v1 * v2) : '';
} else {
out.val1 = (!isNaN(v3) && !isNaN(v2) && v2 !== 0) ? fmt(v3 / v2) : '';
}
break;
case 'base': {
const fromBase = calc.fromBase ?? 10;
const toBase = calc.toBase ?? 2;
if (source === 1) {
const val = rawVal1.trim();
if (!val) { out.val2 = ''; break; }
const dec = parseInt(val, fromBase);
out.val2 = !isNaN(dec) ? dec.toString(toBase).toUpperCase() : 'Invalid';
} else {
const val = rawVal2.trim();
if (!val) { out.val1 = ''; break; }
const dec = parseInt(val, toBase);
out.val1 = !isNaN(dec) ? dec.toString(fromBase).toUpperCase() : 'Invalid';
}
break;
}
case 'text-bin':
if (source === 1) {
out.val2 = rawVal1.split('').map(c => c.charCodeAt(0).toString(2).padStart(8, '0')).join(' ');
} else {
try {
out.val1 = rawVal2.split(' ').map(b => String.fromCharCode(parseInt(b, 2))).join('');
} catch { out.val1 = 'Error'; }
}
break;
case 'bin-text':
if (source === 1) {
try {
out.val2 = rawVal1.split(' ').map(b => String.fromCharCode(parseInt(b, 2))).join('');
} catch { out.val2 = 'Error'; }
} else {
out.val1 = rawVal2.split('').map(c => c.charCodeAt(0).toString(2).padStart(8, '0')).join(' ');
}
break;
case 'dms-dd':
if (source === 1) {
if (!isNaN(v1)) {
const d = Math.floor(v1);
const md = (v1 - d) * 60;
const m = Math.floor(md);
const sec = ((md - m) * 60).toFixed(2);
out.val2 = `${d}° ${m}' ${sec}"`;
} else { out.val2 = ''; }
} else {
const match = rawVal2.match(/(?:([0-9.-]+)\s*°)?\s*(?:([0-9.-]+)\s*')?\s*(?:([0-9.-]+)\s*")?/);
if (match && rawVal2.trim().length > 0) {
const d = parseFloat(match[1]) || 0;
const m = parseFloat(match[2]) || 0;
const sec = parseFloat(match[3]) || 0;
out.val1 = fmt(d + m / 60 + sec / 3600);
} else { out.val1 = ''; }
}
break;
case 'dec-frac':
if (source === 1) {
if (!isNaN(v1)) {
const parts = v1.toString().split('.');
const len = parts[1] ? parts[1].length : 0;
const den = Math.pow(10, len);
const num = v1 * den;
const div = gcd(num, den);
out.val2 = `${num / div}/${den / div}`;
} else { out.val2 = ''; }
} else {
const parts = rawVal2.split('/');
if (parts.length === 2 && !isNaN(Number(parts[0])) && !isNaN(Number(parts[1])) && Number(parts[1]) !== 0) {
out.val1 = fmt(Number(parts[0]) / Number(parts[1]));
} else {
const f = parseFloat(parts[0]);
out.val1 = !isNaN(f) ? f.toString() : '';
}
}
break;
case 'db-int':
if (source === 1) {
out.val2 = !isNaN(v1) ? (1e-12 * Math.pow(10, v1 / 10)).toExponential(6) : '';
} else {
out.val1 = (!isNaN(v2) && v2 > 0) ? fmt(10 * Math.log10(v2 / 1e-12)) : '';
}
break;
case 'db-spl':
if (source === 1) {
out.val2 = !isNaN(v1) ? fmt(20 * Math.pow(10, v1 / 20)) : '';
} else {
out.val1 = (!isNaN(v2) && v2 > 0) ? fmt(20 * Math.log10(v2 / 20)) : '';
}
break;
case 'db-v':
if (source === 1) {
out.val2 = !isNaN(v1) ? fmt(Math.pow(10, v1 / 20)) : '';
} else {
out.val1 = (!isNaN(v2) && v2 > 0) ? fmt(20 * Math.log10(v2)) : '';
}
break;
case 'db-w':
if (source === 1) {
out.val2 = !isNaN(v1) ? fmt(Math.pow(10, v1 / 10)) : '';
} else {
out.val1 = (!isNaN(v2) && v2 > 0) ? fmt(10 * Math.log10(v2)) : '';
}
break;
}
return out;
}