mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
feat: improved calculator
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
export default {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: [
|
||||
'**/__tests__/**/*.ts',
|
||||
'**/?(*.)+(spec|test).ts'
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'json'],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.ts',
|
||||
'!src/**/*.d.ts',
|
||||
],
|
||||
};
|
||||
+7
-1
@@ -16,7 +16,10 @@
|
||||
"dependency-graph": "depcruise src --include-only \"^src\" --output-type dot | dot -T svg > dependency-graph.svg",
|
||||
"release": "gh release create $npm_package_name@$npm_package_version ./dist/*.zip --generate-notes",
|
||||
"publish": "bun lib/publish.js --b",
|
||||
"zip": "bedframe zip"
|
||||
"zip": "bedframe zip",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"targets": {
|
||||
"prod": {
|
||||
@@ -37,6 +40,7 @@
|
||||
"@babel/runtime": "^7.26.9",
|
||||
"@bedframe/cli": "^0.0.91",
|
||||
"@crxjs/vite-plugin": "2.0.0-beta.32",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
@@ -44,6 +48,7 @@
|
||||
"dependency-cruiser": "^16.10.0",
|
||||
"eslint": "9.22.0",
|
||||
"glob": "^11.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"prettier": "^3.5.3",
|
||||
"process": "^0.11.10",
|
||||
@@ -52,6 +57,7 @@
|
||||
"sass-loader": "^16.0.5",
|
||||
"semver": "^7.7.1",
|
||||
"tailwindcss": "3",
|
||||
"ts-jest": "^29.3.4",
|
||||
"url": "^0.11.4"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from 'svelte';
|
||||
import { unitFullNames } from './unitMap';
|
||||
import * as math from 'mathjs';
|
||||
import { calculateExpression } from '../utils/calculator';
|
||||
|
||||
let { searchTerm = '', isSelected = false } = $props<{ searchTerm: string, isSelected: boolean }>();
|
||||
|
||||
@@ -13,73 +12,32 @@
|
||||
let isCalculating = $state(false);
|
||||
let inputUnit = $state<string>('');
|
||||
let outputUnit = $state<string>('');
|
||||
|
||||
function detectUnit(expression: string): string {
|
||||
try {
|
||||
const unit = math.unit(expression);
|
||||
if (unit) {
|
||||
// Get the base unit name
|
||||
const unitStr = unit.formatUnits();
|
||||
return unitFullNames[unitStr] || unitStr;
|
||||
}
|
||||
} catch (e) {
|
||||
// Not a unit or invalid expression
|
||||
}
|
||||
return '';
|
||||
}
|
||||
let isPartial = $state(false);
|
||||
|
||||
// Process the input with debounce to avoid unnecessary calculations
|
||||
const processInput = (input: string) => {
|
||||
isCalculating = true;
|
||||
|
||||
try {
|
||||
if (
|
||||
!input.trim() ||
|
||||
(input.trim().length <= 2 && !/\d/.test(input))
|
||||
) {
|
||||
result = null;
|
||||
inputUnit = '';
|
||||
outputUnit = '';
|
||||
dispatch('hasResult', null);
|
||||
return;
|
||||
}
|
||||
const calcResult = calculateExpression(input);
|
||||
|
||||
isCalculating = true;
|
||||
|
||||
// Let mathjs handle everything
|
||||
const evaluated = math.evaluate(input.replace('**', '^'));
|
||||
|
||||
// Format the result
|
||||
if (evaluated !== undefined) {
|
||||
if (math.typeOf(evaluated) === 'Unit') {
|
||||
// Handle unit conversion results
|
||||
result = math.format(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
|
||||
inputUnit = detectUnit(input);
|
||||
outputUnit = detectUnit(result);
|
||||
} else if (typeof evaluated === 'number') {
|
||||
// Handle regular numbers
|
||||
if (math.round(evaluated) === evaluated) {
|
||||
result = math.format(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
|
||||
} else {
|
||||
result = math.format(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
|
||||
}
|
||||
inputUnit = '';
|
||||
outputUnit = '';
|
||||
} else {
|
||||
result = math.format(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
|
||||
inputUnit = '';
|
||||
outputUnit = '';
|
||||
}
|
||||
dispatch('hasResult', result);
|
||||
if (calcResult.isValid) {
|
||||
result = calcResult.result;
|
||||
inputUnit = calcResult.inputUnit;
|
||||
outputUnit = calcResult.outputUnit;
|
||||
isPartial = calcResult.isPartial;
|
||||
dispatch('hasResult', calcResult.result);
|
||||
} else {
|
||||
result = null;
|
||||
inputUnit = '';
|
||||
outputUnit = '';
|
||||
isPartial = false;
|
||||
dispatch('hasResult', null);
|
||||
}
|
||||
} catch (e) {
|
||||
// If mathjs throws an error, this isn't a valid expression
|
||||
result = null;
|
||||
inputUnit = '';
|
||||
outputUnit = '';
|
||||
isPartial = false;
|
||||
dispatch('hasResult', null);
|
||||
} finally {
|
||||
isCalculating = false;
|
||||
@@ -96,7 +54,7 @@
|
||||
</script>
|
||||
|
||||
{#if result !== null}
|
||||
<div class="p-2">
|
||||
<div class="">
|
||||
<p class="text-[0.85rem] p-1 pb-0.5 pt-0 font-semibold text-zinc-500 dark:text-zinc-400">Calculator</p>
|
||||
<div class="flex items-center justify-between gap-8 rounded-lg border border-transparent {isSelected ? 'bg-zinc-900/5 dark:bg-white/10 border-zinc-900/5 dark:border-zinc-100/5' : ''}">
|
||||
<div class="flex flex-col flex-1 items-center py-4 pl-4 min-w-0">
|
||||
@@ -124,7 +82,7 @@
|
||||
{result}
|
||||
</div>
|
||||
<div class="px-3 py-1 mt-1 text-sm rounded-md text-zinc-900 dark:text-zinc-300 bg-zinc-100 dark:bg-zinc-100/10">
|
||||
{outputUnit || 'Result'}
|
||||
{outputUnit || (isPartial ? 'Partial' : 'Result')}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import * as math from 'mathjs';
|
||||
import { unitFullNames } from './unitMap';
|
||||
|
||||
export interface CalculatorResult {
|
||||
result: string | null;
|
||||
isValid: boolean;
|
||||
isPartial: boolean;
|
||||
inputUnit: string;
|
||||
outputUnit: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const expandedMath = math.create(math.all);
|
||||
|
||||
expandedMath.import({
|
||||
five: 5,
|
||||
ten: 10,
|
||||
three: 3,
|
||||
four: 4,
|
||||
eight: 8,
|
||||
sixteen: 16,
|
||||
twenty: 20,
|
||||
twentyfive: 25,
|
||||
fifty: 50,
|
||||
hundred: 100,
|
||||
plus: (a: number, b: number) => a + b,
|
||||
minus: (a: number, b: number) => a - b,
|
||||
times: (a: number, b: number) => a * b,
|
||||
divided: (a: number, b: number) => a / b,
|
||||
power: (a: number, b: number) => Math.pow(a, b),
|
||||
half: (a: number) => a / 2,
|
||||
double: (a: number) => a * 2,
|
||||
quarter: (a: number) => a / 4,
|
||||
|
||||
// String functions
|
||||
length: (str: string) => str.length,
|
||||
concat: (...args: string[]) => args.join(''),
|
||||
uppercase: (str: string) => str.toUpperCase(),
|
||||
lowercase: (str: string) => str.toLowerCase(),
|
||||
substr: (str: string, start: number, length: number) => str.substr(start, length),
|
||||
|
||||
// Random functions
|
||||
randomInt: (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min,
|
||||
|
||||
// Comparison and Boolean operations
|
||||
and: (a: boolean, b: boolean) => a && b,
|
||||
or: (a: boolean, b: boolean) => a || b,
|
||||
not: (a: boolean) => !a,
|
||||
|
||||
// Combinatorics
|
||||
permutations: (n: number, r: number) => expandedMath.combinations(n, r) * expandedMath.factorial(r),
|
||||
nPr: (n: number, r: number) => expandedMath.combinations(n, r) * expandedMath.factorial(r),
|
||||
nCr: (n: number, r: number) => expandedMath.combinations(n, r),
|
||||
|
||||
// Number theory
|
||||
gcd: (a: number, b: number) => expandedMath.gcd(a, b),
|
||||
lcm: (a: number, b: number) => expandedMath.lcm(a, b),
|
||||
|
||||
// Precision functions
|
||||
precision: (num: number, digits: number) => parseFloat(num.toPrecision(digits)),
|
||||
fix: (num: number, digits: number) => parseFloat(num.toFixed(digits)),
|
||||
|
||||
// Percentage operations
|
||||
percent: (value: number) => value / 100,
|
||||
|
||||
// Financial operations
|
||||
compound: (principal: number, rate: number, time: number) => principal * Math.pow(1 + rate, time),
|
||||
}, { override: true });
|
||||
|
||||
function detectUnit(expression: string): string {
|
||||
try {
|
||||
const unit = expandedMath.unit(expression);
|
||||
if (unit) {
|
||||
const unitStr = unit.formatUnits();
|
||||
return unitFullNames[unitStr] || unitStr;
|
||||
}
|
||||
} catch (e) {
|
||||
// Not a unit or invalid expression
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function isLikelyMathExpression(input: string): boolean {
|
||||
const trimmed = input.trim();
|
||||
|
||||
// Must contain at least one digit or mathematical operator
|
||||
if (!/[\d+\-*/^()=.]/.test(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for common non-math words that shouldn't trigger calculator
|
||||
const nonMathWords = ['abs', 'function', 'class', 'const', 'let', 'var', 'if', 'else', 'while', 'for', 'return', 'import', 'export'];
|
||||
const words = trimmed.toLowerCase().split(/\s+/);
|
||||
|
||||
// If it's just a single non-math word, skip it
|
||||
if (words.length === 1 && nonMathWords.includes(words[0]) && !/\d/.test(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have some mathematical content
|
||||
const mathPattern = /(\d+\.?\d*|\+|\-|\*|\/|\^|\(|\)|sin|cos|tan|log|sqrt|pi|e|=)/i;
|
||||
return mathPattern.test(trimmed);
|
||||
}
|
||||
|
||||
function tryCompleteExpression(expression: string): string | null {
|
||||
const trimmed = expression.trim();
|
||||
|
||||
// Common patterns for incomplete expressions
|
||||
const incompletePatterns = [
|
||||
/[\+\-\*\/\^]\s*$/, // ends with operator
|
||||
/\(\s*$/, // ends with opening parenthesis
|
||||
/[\+\-\*\/\^]\s*\(/, // operator followed by opening parenthesis
|
||||
];
|
||||
|
||||
for (const pattern of incompletePatterns) {
|
||||
if (pattern.test(trimmed)) {
|
||||
// Try to evaluate what we have so far by removing the incomplete part
|
||||
let partial = trimmed.replace(/[\+\-\*\/\^]\s*$/, '').trim();
|
||||
|
||||
// Handle cases like "4 + 3 *" -> evaluate "4 + 3"
|
||||
if (partial && !partial.match(/[\+\-\*\/\^]\s*$/)) {
|
||||
try {
|
||||
const result = expandedMath.evaluate(partial);
|
||||
if (typeof result === 'number' && !isNaN(result)) {
|
||||
return expandedMath.format(result, { precision: 14, lowerExp: -15, upperExp: 15 });
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue to other attempts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function calculateExpression(input: string): CalculatorResult {
|
||||
const trimmed = input.trim();
|
||||
|
||||
// Early exit for empty or very short inputs
|
||||
if (!trimmed || (trimmed.length <= 2 && !/\d/.test(trimmed))) {
|
||||
return {
|
||||
result: null,
|
||||
isValid: false,
|
||||
isPartial: false,
|
||||
inputUnit: '',
|
||||
outputUnit: '',
|
||||
};
|
||||
}
|
||||
|
||||
// Check if this looks like a math expression at all
|
||||
if (!isLikelyMathExpression(trimmed)) {
|
||||
return {
|
||||
result: null,
|
||||
isValid: false,
|
||||
isPartial: false,
|
||||
inputUnit: '',
|
||||
outputUnit: '',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// First try to evaluate the expression as-is
|
||||
const evaluated = expandedMath.evaluate(trimmed.replace('**', '^'));
|
||||
|
||||
if (evaluated !== undefined) {
|
||||
let result: string;
|
||||
let inputUnit = '';
|
||||
let outputUnit = '';
|
||||
|
||||
if (math.typeOf(evaluated) === 'Unit') {
|
||||
// Handle unit conversion results
|
||||
result = expandedMath.format(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
|
||||
inputUnit = detectUnit(trimmed);
|
||||
outputUnit = detectUnit(result);
|
||||
} else if (typeof evaluated === 'number') {
|
||||
// Handle regular numbers
|
||||
result = math.format(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
|
||||
} else {
|
||||
result = math.format(evaluated, { precision: 14, lowerExp: -15, upperExp: 15 });
|
||||
}
|
||||
|
||||
return {
|
||||
result,
|
||||
isValid: true,
|
||||
isPartial: false,
|
||||
inputUnit,
|
||||
outputUnit,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Try to handle incomplete expressions
|
||||
const partialResult = tryCompleteExpression(trimmed);
|
||||
|
||||
if (partialResult) {
|
||||
return {
|
||||
result: partialResult,
|
||||
isValid: true,
|
||||
isPartial: true,
|
||||
inputUnit: '',
|
||||
outputUnit: '',
|
||||
};
|
||||
}
|
||||
|
||||
// If it still looks like math but failed, return the error
|
||||
return {
|
||||
result: null,
|
||||
isValid: false,
|
||||
isPartial: false,
|
||||
inputUnit: '',
|
||||
outputUnit: '',
|
||||
error: error instanceof Error ? error.message : 'Invalid expression',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
result: null,
|
||||
isValid: false,
|
||||
isPartial: false,
|
||||
inputUnit: '',
|
||||
outputUnit: '',
|
||||
};
|
||||
}
|
||||
+28
-1
@@ -177,7 +177,7 @@ export const unitFullNames: Record<string, string> = {
|
||||
|
||||
// --- Currency ---
|
||||
USD: "US Dollars",
|
||||
EUR: "Euros",
|
||||
EUR: "Euros",
|
||||
GBP: "British Pounds",
|
||||
AUD: "Australian Dollars",
|
||||
CAD: "Canadian Dollars",
|
||||
@@ -190,4 +190,31 @@ export const unitFullNames: Record<string, string> = {
|
||||
NOK: "Norwegian Krone",
|
||||
SGD: "Singapore Dollars",
|
||||
HKD: "Hong Kong Dollars",
|
||||
KRW: "South Korean Won",
|
||||
MXN: "Mexican Peso",
|
||||
BRL: "Brazilian Real",
|
||||
RUB: "Russian Ruble",
|
||||
TRY: "Turkish Lira",
|
||||
ZAR: "South African Rand",
|
||||
PLN: "Polish Zloty",
|
||||
THB: "Thai Baht",
|
||||
DKK: "Danish Krone",
|
||||
CZK: "Czech Koruna",
|
||||
HUF: "Hungarian Forint",
|
||||
ILS: "Israeli Shekel",
|
||||
PHP: "Philippine Peso",
|
||||
TWD: "Taiwan Dollar",
|
||||
MYR: "Malaysian Ringgit",
|
||||
|
||||
// --- Cryptocurrencies ---
|
||||
BTC: "Bitcoin",
|
||||
ETH: "Ethereum",
|
||||
XRP: "Ripple",
|
||||
LTC: "Litecoin",
|
||||
ADA: "Cardano",
|
||||
DOT: "Polkadot",
|
||||
SOL: "Solana",
|
||||
MATIC: "Polygon",
|
||||
AVAX: "Avalanche",
|
||||
UNI: "Uniswap",
|
||||
};
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"types": ["vite/client", "node"]
|
||||
"types": ["vite/client", "node", "jest"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
||||
Reference in New Issue
Block a user