diff --git a/src/components/desktop/AmountInput.vue b/src/components/desktop/AmountInput.vue index d7ea3705..daee27b1 100644 --- a/src/components/desktop/AmountInput.vue +++ b/src/components/desktop/AmountInput.vue @@ -79,7 +79,7 @@ import type { CurrencyPrependAndAppendText } from '@/core/currency.ts'; import { DEFAULT_DECIMAL_NUMBER_COUNT } from '@/consts/numeral.ts'; import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '@/consts/transaction.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 logger from '@/lib/logger.ts'; @@ -116,7 +116,6 @@ const { getCurrentDecimalSeparator, parseAmountFromLocalizedNumerals, formatAmountToLocalizedNumeralsWithoutDigitGrouping, - formatNumberToLocalizedNumerals, getAmountPrependAndAppendText } = useI18n(); @@ -218,15 +217,24 @@ function calculateFormula(): void { } finalFormula = numeralSystem.value.replaceLocalizedDigitsToWesternArabicDigits(finalFormula); - const calculatedValue = evaluateExpression(finalFormula); - if (isNumber(calculatedValue)) { - const textualValue = formatNumberToLocalizedNumerals(calculatedValue, 2); - const hasDecimalSeparator = textualValue.indexOf(decimalSeparator) >= 0; - currentValue.value = getValidFormattedValue(calculatedValue * 100, textualValue, hasDecimalSeparator); - formulaMode.value = false; - } else { - snackbar.value?.showMessage('Formula is invalid'); + try { + const calculatedAmount = evaluateExpressionToAmount(finalFormula); + + if (isNumber(calculatedAmount)) { + const textualValue = getFormattedValue(calculatedAmount); + const hasDecimalSeparator = textualValue.indexOf(decimalSeparator) >= 0; + 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); + } } } diff --git a/src/lib/evaluator.ts b/src/lib/evaluator.ts index 52c010ee..21502a45 100644 --- a/src/lib/evaluator.ts +++ b/src/lib/evaluator.ts @@ -1,7 +1,14 @@ +import { TRANSACTION_MIN_AMOUNT, TRANSACTION_MAX_AMOUNT } from '../consts/transaction.ts'; + import { replaceAll } from './common.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 = { '+': 1, '-': 1, @@ -9,6 +16,36 @@ const operatorPriority: Record = { '/': 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 { const finalTokens: string[] = []; const operatorStack: string[] = []; @@ -143,31 +180,28 @@ function evaluatePostfixExpr(tokens: string[]): number | null { result = a - b; break; case '*': - result = a * b; + result = Math.floor(a * b / normalizeFactor); break; case '/': if (b === 0) { logger.warn(`cannot evaluate expression "${tokens.join(' ')}", because division by zero`); return null; } - result = a / b; + result = Math.floor(a * normalizeFactor / b); break; default: return null; } + checkNumberRange(result); + // push the result back to the stack stack.push(result); break; default: // operands - const num = parseFloat(token); - - if (isNaN(num)) { - logger.warn(`cannot evaluate expression "${tokens.join(' ')}", because containing invalid number`); - return null; - } - - stack.push(num); + const normalizedNum = normalizeNumber(token); + checkNumberRange(normalizedNum); + stack.push(normalizedNum); break; } } @@ -179,7 +213,7 @@ function evaluatePostfixExpr(tokens: string[]): number | null { return stack[0]; } -export function evaluateExpression(expr: string): number | undefined { +export function evaluateExpressionToAmount(expr: string): number | undefined { if (!expr) { return undefined; } @@ -196,5 +230,5 @@ export function evaluateExpression(expr: string): number | undefined { return undefined; } - return result; + return denormalizeNumberToAmount(result); }