mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-19 01:04:25 +08:00
use integers to calculate formulas for evaluator (#214)
This commit is contained in:
@@ -79,7 +79,7 @@ import type { CurrencyPrependAndAppendText } from '@/core/currency.ts';
|
|||||||
import { DEFAULT_DECIMAL_NUMBER_COUNT } from '@/consts/numeral.ts';
|
import { DEFAULT_DECIMAL_NUMBER_COUNT } from '@/consts/numeral.ts';
|
||||||
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
|
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.ts';
|
||||||
import { isNumber, replaceAll } from '@/lib/common.ts';
|
import { isNumber, replaceAll } from '@/lib/common.ts';
|
||||||
import { evaluateExpression } from '@/lib/evaluator.ts';
|
import { evaluateExpressionToAmount } from '@/lib/evaluator.ts';
|
||||||
import type { ComponentDensity } from '@/lib/ui/desktop.ts';
|
import type { ComponentDensity } from '@/lib/ui/desktop.ts';
|
||||||
import logger from '@/lib/logger.ts';
|
import logger from '@/lib/logger.ts';
|
||||||
|
|
||||||
@@ -116,7 +116,6 @@ const {
|
|||||||
getCurrentDecimalSeparator,
|
getCurrentDecimalSeparator,
|
||||||
parseAmountFromLocalizedNumerals,
|
parseAmountFromLocalizedNumerals,
|
||||||
formatAmountToLocalizedNumeralsWithoutDigitGrouping,
|
formatAmountToLocalizedNumeralsWithoutDigitGrouping,
|
||||||
formatNumberToLocalizedNumerals,
|
|
||||||
getAmountPrependAndAppendText
|
getAmountPrependAndAppendText
|
||||||
} = useI18n();
|
} = useI18n();
|
||||||
|
|
||||||
@@ -218,15 +217,24 @@ function calculateFormula(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
finalFormula = numeralSystem.value.replaceLocalizedDigitsToWesternArabicDigits(finalFormula);
|
finalFormula = numeralSystem.value.replaceLocalizedDigitsToWesternArabicDigits(finalFormula);
|
||||||
const calculatedValue = evaluateExpression(finalFormula);
|
|
||||||
|
|
||||||
if (isNumber(calculatedValue)) {
|
try {
|
||||||
const textualValue = formatNumberToLocalizedNumerals(calculatedValue, 2);
|
const calculatedAmount = evaluateExpressionToAmount(finalFormula);
|
||||||
const hasDecimalSeparator = textualValue.indexOf(decimalSeparator) >= 0;
|
|
||||||
currentValue.value = getValidFormattedValue(calculatedValue * 100, textualValue, hasDecimalSeparator);
|
if (isNumber(calculatedAmount)) {
|
||||||
formulaMode.value = false;
|
const textualValue = getFormattedValue(calculatedAmount);
|
||||||
} else {
|
const hasDecimalSeparator = textualValue.indexOf(decimalSeparator) >= 0;
|
||||||
snackbar.value?.showMessage('Formula is invalid');
|
currentValue.value = getValidFormattedValue(calculatedAmount, textualValue, hasDecimalSeparator);
|
||||||
|
formulaMode.value = false;
|
||||||
|
} else {
|
||||||
|
snackbar.value?.showMessage('Formula is invalid');
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
logger.error('cannot evaluate formula in amount input, original formula is ' + finalFormula, ex);
|
||||||
|
|
||||||
|
if (ex instanceof Error) {
|
||||||
|
snackbar.value?.showMessage(ex.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+46
-12
@@ -1,7 +1,14 @@
|
|||||||
|
import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '../consts/transaction.ts';
|
||||||
|
|
||||||
import { replaceAll } from './common.ts';
|
import { replaceAll } from './common.ts';
|
||||||
|
|
||||||
import logger from './logger.ts';
|
import logger from './logger.ts';
|
||||||
|
|
||||||
|
const maxAllowedDecimalCount = 6;
|
||||||
|
const normalizeFactor: number = 1000000;
|
||||||
|
const normalizedDecimalsMaxZeroString: string = '000000';
|
||||||
|
const normalizedNumberToAmountFactor: number = 10000; // 1000000 / 100
|
||||||
|
|
||||||
const operatorPriority: Record<string, number> = {
|
const operatorPriority: Record<string, number> = {
|
||||||
'+': 1,
|
'+': 1,
|
||||||
'-': 1,
|
'-': 1,
|
||||||
@@ -9,6 +16,36 @@ const operatorPriority: Record<string, number> = {
|
|||||||
'/': 2,
|
'/': 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function normalizeNumber(textualNumber: string): number {
|
||||||
|
const decimalSeparatorPos = textualNumber.indexOf('.');
|
||||||
|
|
||||||
|
if (decimalSeparatorPos < 0) {
|
||||||
|
return parseInt(textualNumber + normalizedDecimalsMaxZeroString);
|
||||||
|
}
|
||||||
|
|
||||||
|
const integer = textualNumber.substring(0, decimalSeparatorPos);
|
||||||
|
const decimals = textualNumber.substring(decimalSeparatorPos + 1);
|
||||||
|
|
||||||
|
if (decimals.length > maxAllowedDecimalCount) {
|
||||||
|
throw new Error('Numeric Overflow');
|
||||||
|
}
|
||||||
|
|
||||||
|
const paddedDecimals = (decimals + normalizedDecimalsMaxZeroString).substring(0, maxAllowedDecimalCount);
|
||||||
|
return parseInt(integer + paddedDecimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
function denormalizeNumberToAmount(num: number): number {
|
||||||
|
return Math.floor(num / normalizedNumberToAmountFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkNumberRange(num: number): void {
|
||||||
|
const amount = denormalizeNumberToAmount(num);
|
||||||
|
|
||||||
|
if (amount > TRANSACTION_MAX_AMOUNT || amount < TRANSACTION_MIN_AMOUNT) {
|
||||||
|
throw new Error('Numeric Overflow');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toPostfixExprTokens(expr: string): string[] | null {
|
function toPostfixExprTokens(expr: string): string[] | null {
|
||||||
const finalTokens: string[] = [];
|
const finalTokens: string[] = [];
|
||||||
const operatorStack: string[] = [];
|
const operatorStack: string[] = [];
|
||||||
@@ -143,31 +180,28 @@ function evaluatePostfixExpr(tokens: string[]): number | null {
|
|||||||
result = a - b;
|
result = a - b;
|
||||||
break;
|
break;
|
||||||
case '*':
|
case '*':
|
||||||
result = a * b;
|
result = Math.floor(a * b / normalizeFactor);
|
||||||
break;
|
break;
|
||||||
case '/':
|
case '/':
|
||||||
if (b === 0) {
|
if (b === 0) {
|
||||||
logger.warn(`cannot evaluate expression "${tokens.join(' ')}", because division by zero`);
|
logger.warn(`cannot evaluate expression "${tokens.join(' ')}", because division by zero`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
result = a / b;
|
result = Math.floor(a * normalizeFactor / b);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkNumberRange(result);
|
||||||
|
|
||||||
// push the result back to the stack
|
// push the result back to the stack
|
||||||
stack.push(result);
|
stack.push(result);
|
||||||
break;
|
break;
|
||||||
default: // operands
|
default: // operands
|
||||||
const num = parseFloat(token);
|
const normalizedNum = normalizeNumber(token);
|
||||||
|
checkNumberRange(normalizedNum);
|
||||||
if (isNaN(num)) {
|
stack.push(normalizedNum);
|
||||||
logger.warn(`cannot evaluate expression "${tokens.join(' ')}", because containing invalid number`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
stack.push(num);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +213,7 @@ function evaluatePostfixExpr(tokens: string[]): number | null {
|
|||||||
|
|
||||||
return stack[0];
|
return stack[0];
|
||||||
}
|
}
|
||||||
export function evaluateExpression(expr: string): number | undefined {
|
export function evaluateExpressionToAmount(expr: string): number | undefined {
|
||||||
if (!expr) {
|
if (!expr) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -196,5 +230,5 @@ export function evaluateExpression(expr: string): number | undefined {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return denormalizeNumberToAmount(result);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user