Skip to content

Commit

Permalink
Updates
Browse files Browse the repository at this point in the history
  • Loading branch information
joel-jeremy committed Nov 4, 2024
1 parent e74786a commit ce26247
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ import React, {

import { css } from '@emotion/css';

import {
evalArithmetic,
hasArithmeticOperator,
lastIndexOfArithmeticOperator,
} from 'loot-core/shared/arithmetic';
import {
amountToCurrency,
amountToInteger,
appendDecimals,
currencyToAmount,
} from 'loot-core/src/shared/util';
Expand Down Expand Up @@ -75,6 +81,10 @@ const AmountInput = memo(function AmountInput({
setValue(initialValue);
}, [initialValue]);

useEffect(() => {
keyboardRef.current?.setInput(text);
}, [text]);

const onKeyUp: HTMLProps<HTMLInputElement>['onKeyUp'] = e => {
if (e.key === 'Backspace' && text === '') {
setEditing(true);
Expand All @@ -87,7 +97,10 @@ const AmountInput = memo(function AmountInput({
};

const applyText = () => {
const parsed = currencyToAmount(text) || 0;
const parsed = hasArithmeticOperator(text)
? evalArithmetic(text)
: currencyToAmount(text) || 0;

const newValue = editing ? parsed : value;

setValue(Math.abs(newValue));
Expand Down Expand Up @@ -115,11 +128,44 @@ const AmountInput = memo(function AmountInput({
};

const onChangeText = (text: string) => {
text = appendDecimals(text, String(hideFraction) === 'true');
console.log('text', text);

const lastOperatorIndex = lastIndexOfArithmeticOperator(text);
if (lastOperatorIndex > 0) {
// This will evaluate the expression whenever an operator is added
// so only one operation will be displayed at a given time
const isOperatorAtEnd = lastOperatorIndex === text.length - 1;
if (isOperatorAtEnd) {
const lastOperator = text[lastOperatorIndex];
const charIndexPriorToLastOperator = lastOperatorIndex - 1;
const charPriorToLastOperator =
text.length > 0 ? text[charIndexPriorToLastOperator] : '';

if (
charPriorToLastOperator &&
hasArithmeticOperator(charPriorToLastOperator)
) {
// Clicked on another operator while there is still an operator
// Replace previous operator with the new one
// TODO: Fix why clicking the same operator duplicates it
text = `${text.slice(0, charIndexPriorToLastOperator)}${lastOperator}`;
} else {
// Evaluate the left side of the expression whenever an operator is added
const left = text.slice(0, lastOperatorIndex);
const leftEvaluated = evalArithmetic(left);
const leftEvaluatedWithDecimal = appendDecimals(
String(amountToInteger(leftEvaluated)),
String(hideFraction) === 'true',
);
text = `${leftEvaluatedWithDecimal}${lastOperator}`;
}
}
} else {
text = appendDecimals(text, String(hideFraction) === 'true');
}
setEditing(true);
setText(text);
props.onChangeValue?.(text);
keyboardRef.current?.setInput(text);
};

const input = (
Expand Down Expand Up @@ -172,6 +218,7 @@ const AmountInput = memo(function AmountInput({
keyboardRef={(r: AmountKeyboardRef) => (keyboardRef.current = r)}
onChange={onChangeText}
onBlur={onBlur}
onEnter={onUpdate}
/>
)}
</View>
Expand Down
30 changes: 26 additions & 4 deletions packages/desktop-client/src/components/util/AmountKeyboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type AmountKeyboardRef = SimpleKeyboard;

type AmountKeyboardProps = ComponentPropsWithoutRef<typeof Keyboard> & {
onBlur?: FocusEventHandler<HTMLDivElement>;
onEnter?: (text: string) => void;
};

export function AmountKeyboard(props: AmountKeyboardProps) {
Expand Down Expand Up @@ -50,10 +51,18 @@ export function AmountKeyboard(props: AmountKeyboardProps) {
},
},
// eslint-disable-next-line rulesdir/typography
'& [data-skbtn="+"], & [data-skbtn="-"], & [data-skbtn="×"], & [data-skbtn="÷"], & [data-skbtn="{bksp}"]':
'& [data-skbtn="+"], & [data-skbtn="-"], & [data-skbtn="×"], & [data-skbtn="÷"]':
{
backgroundColor: theme.keyboardButtonSecondaryBackground,
},
// eslint-disable-next-line rulesdir/typography
'& [data-skbtn="{bksp}"], & [data-skbtn="{clear}"]': {
backgroundColor: theme.keyboardButtonSecondaryBackground,
},
// eslint-disable-next-line rulesdir/typography
'& [data-skbtn="{enter}"]': {
backgroundColor: theme.buttonPrimaryBackground,
},
}),
props.theme,
]);
Expand Down Expand Up @@ -81,25 +90,38 @@ export function AmountKeyboard(props: AmountKeyboardProps) {
}}
>
<Keyboard
layoutName="default"
layout={{
// eslint-disable-next-line prettier/prettier
default: [
'+ 1 2 3',
'- 4 5 6',
'× 7 8 9',
'÷ . 0 {bksp}',
'÷ {clear} 0 {bksp}',
'{space} , . {enter}',
],
}}
display={{
'{bksp}': '⌫',
'{enter}': '↵',
'{space}': '␣',
'{clear}': 'C',
}}
useButtonTag
stopMouseUpPropagation
stopMouseDownPropagation
autoUseTouchEvents
{...props}
keyboardRef={r => {
keyboardRef.current = r;
props.keyboardRef?.(r);
}}
onKeyPress={key => {
if (key === '{clear}') {
props.onChange?.('');
} else if (key === '{enter}') {
props.onEnter?.(keyboardRef.current?.getInput() || '');
}
props.onKeyPress?.(key);
}}
theme={layoutClassName}
/>
</View>
Expand Down
53 changes: 39 additions & 14 deletions packages/loot-core/src/shared/arithmetic.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
// @ts-strict-ignore
import { currencyToAmount } from './util';

function fail(state, msg) {
// These operators go from high to low order of precedence
const operators = ['^', '/', '÷', '*', '×', '-', '+'] as const;
type ArithmeticOp = (typeof operators)[number];

type ArithmeticAst =
| number
| { op: ArithmeticOp; left: ArithmeticAst; right: ArithmeticAst };

type ArithmeticState = { str: string; index: number };

const parseOperator = makeOperatorParser(...operators);

function fail(state: ArithmeticState, msg: string) {
throw new Error(
msg + ': ' + JSON.stringify(state.str.slice(state.index, 10)),
);
}

function char(state) {
function char(state: ArithmeticState): string {
return state.str[state.index];
}

function next(state) {
function next(state: ArithmeticState): string {
if (state.index >= state.str.length) {
return null;
}
Expand All @@ -21,7 +33,7 @@ function next(state) {
return ch;
}

function nextOperator(state, op) {
function nextOperator(state: ArithmeticState, op: ArithmeticOp) {
if (char(state) === op) {
next(state);
return true;
Expand All @@ -30,7 +42,7 @@ function nextOperator(state, op) {
return false;
}

function parsePrimary(state) {
function parsePrimary(state: ArithmeticState): number {
// We only support numbers
const isNegative = char(state) === '-';
if (isNegative) {
Expand All @@ -50,7 +62,7 @@ function parsePrimary(state) {
return isNegative ? -number : number;
}

function parseParens(state) {
function parseParens(state: ArithmeticState): ArithmeticAst {
if (char(state) === '(') {
next(state);
const expr = parseOperator(state);
Expand All @@ -66,7 +78,7 @@ function parseParens(state) {
return parsePrimary(state);
}

function makeOperatorParser(...ops) {
function makeOperatorParser(...ops: ArithmeticOp[]) {
return ops.reduce((prevParser, op) => {
return state => {
let node = prevParser(state);
Expand All @@ -78,15 +90,15 @@ function makeOperatorParser(...ops) {
}, parseParens);
}

// These operators go from high to low order of precedence
const parseOperator = makeOperatorParser('^', '/', '*', '-', '+');

function parse(expression: string) {
const state = { str: expression.replace(/\s/g, ''), index: 0 };
function parse(expression: string): ArithmeticAst {
const state: ArithmeticState = {
str: expression.replace(/\s/g, ''),
index: 0,
};
return parseOperator(state);
}

function evaluate(ast): number {
function evaluate(ast: ArithmeticAst): number {
if (typeof ast === 'number') {
return ast;
}
Expand All @@ -99,8 +111,10 @@ function evaluate(ast): number {
case '-':
return evaluate(left) - evaluate(right);
case '*':
case '×':
return evaluate(left) * evaluate(right);
case '/':
case '÷':
return evaluate(left) / evaluate(right);
case '^':
return Math.pow(evaluate(left), evaluate(right));
Expand All @@ -112,7 +126,7 @@ function evaluate(ast): number {
export function evalArithmetic(
expression: string,
defaultValue: number = null,
) {
): number {
// An empty expression always evals to the default
if (expression === '') {
return defaultValue;
Expand All @@ -129,3 +143,14 @@ export function evalArithmetic(
// Never return NaN
return isNaN(result) ? defaultValue : result;
}

export function hasArithmeticOperator(expression: string): boolean {
return operators.some(op => expression.includes(op));
}

export function lastIndexOfArithmeticOperator(expression: string): number {
return operators.reduce((max, op) => {
const index = expression.lastIndexOf(op);
return index > max ? index : max;
}, -1);
}

0 comments on commit ce26247

Please sign in to comment.