mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 20:44:32 -05:00
Compare commits
9 Commits
Transactio
...
mobile-cal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1156efc6f | ||
|
|
dfd34d6c9a | ||
|
|
6d7cf52fee | ||
|
|
2d23a09f03 | ||
|
|
1fcba8571a | ||
|
|
db09dd7b6f | ||
|
|
dfdf17c4d2 | ||
|
|
ad7d90228e | ||
|
|
204d4e9394 |
@@ -201,4 +201,11 @@ export const theme = {
|
|||||||
tooltipBackground: 'var(--color-tooltipBackground)',
|
tooltipBackground: 'var(--color-tooltipBackground)',
|
||||||
tooltipBorder: 'var(--color-tooltipBorder)',
|
tooltipBorder: 'var(--color-tooltipBorder)',
|
||||||
calendarCellBackground: 'var(--color-calendarCellBackground)',
|
calendarCellBackground: 'var(--color-calendarCellBackground)',
|
||||||
|
keyboardBackground: 'var(--color-keyboardBackground)',
|
||||||
|
keyboardBorder: 'var(--color-keyboardBorder)',
|
||||||
|
keyboardText: 'var(--color-keyboardText)',
|
||||||
|
keyboardButtonBackground: 'var(--color-keyboardButtonBackground)',
|
||||||
|
keyboardButtonShadow: 'var(--color-keyboardButtonShadow)',
|
||||||
|
keyboardButtonSecondaryBackground:
|
||||||
|
'var(--color-keyboardButtonSecondaryBackground)',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
"react-modal": "3.16.3",
|
"react-modal": "3.16.3",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-router": "7.6.2",
|
"react-router": "7.6.2",
|
||||||
|
"react-simple-keyboard": "^3.8.106",
|
||||||
"react-simple-pull-to-refresh": "^1.3.3",
|
"react-simple-pull-to-refresh": "^1.3.3",
|
||||||
"react-spring": "^10.0.0",
|
"react-spring": "^10.0.0",
|
||||||
"react-stately": "^3.37.0",
|
"react-stately": "^3.37.0",
|
||||||
|
|||||||
@@ -15,14 +15,24 @@ import { theme } from '@actual-app/components/theme';
|
|||||||
import { View } from '@actual-app/components/view';
|
import { View } from '@actual-app/components/view';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
import {
|
||||||
|
evalArithmetic,
|
||||||
|
hasArithmeticOperator,
|
||||||
|
lastIndexOfArithmeticOperator,
|
||||||
|
} from 'loot-core/shared/arithmetic';
|
||||||
import {
|
import {
|
||||||
amountToCurrency,
|
amountToCurrency,
|
||||||
|
amountToInteger,
|
||||||
appendDecimals,
|
appendDecimals,
|
||||||
currencyToAmount,
|
currencyToAmount,
|
||||||
reapplyThousandSeparators,
|
reapplyThousandSeparators,
|
||||||
} from 'loot-core/shared/util';
|
} from 'loot-core/shared/util';
|
||||||
|
|
||||||
import { makeAmountFullStyle } from '@desktop-client/components/budget/util';
|
import { makeAmountFullStyle } from '@desktop-client/components/budget/util';
|
||||||
|
import {
|
||||||
|
AmountKeyboard,
|
||||||
|
type AmountKeyboardRef,
|
||||||
|
} from '@desktop-client/components/util/AmountKeyboard';
|
||||||
import { useMergedRefs } from '@desktop-client/hooks/useMergedRefs';
|
import { useMergedRefs } from '@desktop-client/hooks/useMergedRefs';
|
||||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||||
|
|
||||||
@@ -50,7 +60,7 @@ const AmountInput = memo(function AmountInput({
|
|||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
const [value, setValue] = useState(0);
|
const [value, setValue] = useState(0);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [hideFraction] = useSyncedPref('hideFraction');
|
const [hideFractionPref] = useSyncedPref('hideFraction');
|
||||||
|
|
||||||
const mergedInputRef = useMergedRefs<HTMLInputElement>(
|
const mergedInputRef = useMergedRefs<HTMLInputElement>(
|
||||||
props.inputRef,
|
props.inputRef,
|
||||||
@@ -58,6 +68,7 @@ const AmountInput = memo(function AmountInput({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const initialValue = Math.abs(props.value);
|
const initialValue = Math.abs(props.value);
|
||||||
|
const keyboardRef = useRef<AmountKeyboardRef | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focused) {
|
if (focused) {
|
||||||
@@ -83,7 +94,11 @@ const AmountInput = memo(function AmountInput({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const applyText = () => {
|
const applyText = () => {
|
||||||
const parsed = currencyToAmount(text) || 0;
|
const parsed =
|
||||||
|
(hasArithmeticOperator(text)
|
||||||
|
? evalArithmetic(text)
|
||||||
|
: currencyToAmount(text)) ?? 0;
|
||||||
|
|
||||||
const newValue = editing ? parsed : value;
|
const newValue = editing ? parsed : value;
|
||||||
|
|
||||||
setValue(Math.abs(newValue));
|
setValue(Math.abs(newValue));
|
||||||
@@ -114,10 +129,52 @@ const AmountInput = memo(function AmountInput({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onChangeText = (text: string) => {
|
const onChangeText = (text: string) => {
|
||||||
text = reapplyThousandSeparators(text);
|
const hideFraction = String(hideFractionPref) === 'true';
|
||||||
text = appendDecimals(text, String(hideFraction) === 'true');
|
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
|
||||||
|
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) ?? 0;
|
||||||
|
const leftEvaluatedWithDecimal = appendDecimals(
|
||||||
|
reapplyThousandSeparators(String(amountToInteger(leftEvaluated))),
|
||||||
|
hideFraction,
|
||||||
|
);
|
||||||
|
text = `${leftEvaluatedWithDecimal}${lastOperator}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Append decimals to the right side of the expression
|
||||||
|
const left = text.slice(0, lastOperatorIndex);
|
||||||
|
const right = text.slice(lastOperatorIndex + 1);
|
||||||
|
const lastOperator = text[lastOperatorIndex];
|
||||||
|
const rightWithDecimal = appendDecimals(
|
||||||
|
reapplyThousandSeparators(right),
|
||||||
|
hideFraction,
|
||||||
|
);
|
||||||
|
text = `${left}${lastOperator}${rightWithDecimal}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
text = appendDecimals(reapplyThousandSeparators(text), hideFraction);
|
||||||
|
}
|
||||||
setEditing(true);
|
setEditing(true);
|
||||||
setText(text);
|
setText(text);
|
||||||
|
keyboardRef.current?.setInput(text);
|
||||||
props.onChangeValue?.(text);
|
props.onChangeValue?.(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,11 +183,17 @@ const AmountInput = memo(function AmountInput({
|
|||||||
type="text"
|
type="text"
|
||||||
ref={mergedInputRef}
|
ref={mergedInputRef}
|
||||||
value={text}
|
value={text}
|
||||||
inputMode="decimal"
|
inputMode="none"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
onChange={e => onChangeText(e.target.value)}
|
onChange={e => onChangeText(e.target.value)}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
onBlur={onBlur}
|
onBlur={e => {
|
||||||
|
// Do not blur when clicking on the keyboard elements
|
||||||
|
if (keyboardRef.current?.keyboardDOM.contains(e.relatedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onBlur(e);
|
||||||
|
}}
|
||||||
onKeyUp={onKeyUp}
|
onKeyUp={onKeyUp}
|
||||||
data-testid="amount-input"
|
data-testid="amount-input"
|
||||||
style={{ flex: 1, textAlign: 'center', position: 'absolute' }}
|
style={{ flex: 1, textAlign: 'center', position: 'absolute' }}
|
||||||
@@ -160,6 +223,14 @@ const AmountInput = memo(function AmountInput({
|
|||||||
>
|
>
|
||||||
{editing ? text : amountToCurrency(value)}
|
{editing ? text : amountToCurrency(value)}
|
||||||
</Text>
|
</Text>
|
||||||
|
{focused && (
|
||||||
|
<AmountKeyboard
|
||||||
|
keyboardRef={(r: AmountKeyboardRef) => (keyboardRef.current = r)}
|
||||||
|
onChange={onChangeText}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onEnter={onUpdate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
135
packages/desktop-client/src/components/util/AmountKeyboard.tsx
Normal file
135
packages/desktop-client/src/components/util/AmountKeyboard.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import React, { type FocusEventHandler, useCallback, useRef } from 'react';
|
||||||
|
import {
|
||||||
|
default as Keyboard,
|
||||||
|
type KeyboardReactInterface,
|
||||||
|
type SimpleKeyboard,
|
||||||
|
} from 'react-simple-keyboard';
|
||||||
|
|
||||||
|
import { styles } from '@actual-app/components/styles';
|
||||||
|
import { theme } from '@actual-app/components/theme';
|
||||||
|
import { View } from '@actual-app/components/view';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
|
||||||
|
export type AmountKeyboardRef = SimpleKeyboard;
|
||||||
|
|
||||||
|
type AmountKeyboardProps = KeyboardReactInterface['options'] & {
|
||||||
|
onBlur?: FocusEventHandler<HTMLDivElement>;
|
||||||
|
onEnter?: (text: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AmountKeyboard(props: AmountKeyboardProps) {
|
||||||
|
const layoutClassName = cx([
|
||||||
|
css({
|
||||||
|
'& .hg-row': {
|
||||||
|
display: 'flex',
|
||||||
|
},
|
||||||
|
'& .hg-button': {
|
||||||
|
...styles.noTapHighlight,
|
||||||
|
...styles.largeText,
|
||||||
|
fontWeight: 500,
|
||||||
|
display: 'flex',
|
||||||
|
height: '60px',
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
borderRadius: 16,
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: theme.keyboardButtonBackground,
|
||||||
|
color: theme.keyboardText,
|
||||||
|
padding: '5px 10px',
|
||||||
|
margin: 2,
|
||||||
|
borderWidth: 0,
|
||||||
|
outline: 0,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
':active': {
|
||||||
|
transform: 'translateY(1px)',
|
||||||
|
boxShadow: `0 1px 4px 0 ${theme.keyboardButtonShadow}`,
|
||||||
|
transition: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line actual/typography
|
||||||
|
'& [data-skbtn="+"], & [data-skbtn="-"], & [data-skbtn="*"], & [data-skbtn="/"]':
|
||||||
|
{
|
||||||
|
backgroundColor: theme.keyboardButtonSecondaryBackground,
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line actual/typography
|
||||||
|
'& [data-skbtn="{bksp}"], & [data-skbtn="{clear}"]': {
|
||||||
|
backgroundColor: theme.keyboardButtonSecondaryBackground,
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line actual/typography
|
||||||
|
'& [data-skbtn="{enter}"]': {
|
||||||
|
backgroundColor: theme.buttonPrimaryBackground,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
props.theme,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const keyboardRef = useRef<SimpleKeyboard | null>(null);
|
||||||
|
const keyboardRefProp = props.keyboardRef;
|
||||||
|
|
||||||
|
const mergedRef = useCallback(
|
||||||
|
(r: SimpleKeyboard) => {
|
||||||
|
keyboardRef.current = r;
|
||||||
|
keyboardRefProp?.(r);
|
||||||
|
},
|
||||||
|
[keyboardRefProp],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
left: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 999,
|
||||||
|
backgroundColor: theme.keyboardBackground,
|
||||||
|
borderTop: `1px solid ${theme.keyboardBorder}`,
|
||||||
|
padding: 5,
|
||||||
|
}}
|
||||||
|
onBlur={e => {
|
||||||
|
if (keyboardRef.current?.keyboardDOM.contains(e.relatedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onBlur?.(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Keyboard
|
||||||
|
layout={{
|
||||||
|
default: [
|
||||||
|
'+ 1 2 3',
|
||||||
|
'- 4 5 6',
|
||||||
|
'* 7 8 9',
|
||||||
|
'/ {clear} 0 {bksp}',
|
||||||
|
'{space} , . {enter}',
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
display={{
|
||||||
|
'{bksp}': '⌫',
|
||||||
|
'{enter}': '↵',
|
||||||
|
'{space}': '␣',
|
||||||
|
'{clear}': 'C',
|
||||||
|
'*': '×',
|
||||||
|
'/': '÷',
|
||||||
|
}}
|
||||||
|
useButtonTag
|
||||||
|
autoUseTouchEvents
|
||||||
|
physicalKeyboardHighlight
|
||||||
|
disableButtonHold
|
||||||
|
debug
|
||||||
|
{...props}
|
||||||
|
keyboardRef={mergedRef}
|
||||||
|
onKeyPress={(key, e) => {
|
||||||
|
if (key === '{clear}') {
|
||||||
|
props.onChange?.('', e);
|
||||||
|
} else if (key === '{enter}') {
|
||||||
|
props.onEnter?.(keyboardRef.current?.getInput() || '');
|
||||||
|
}
|
||||||
|
props.onKeyPress?.(key, e);
|
||||||
|
}}
|
||||||
|
theme={layoutClassName}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -217,3 +217,9 @@ export const tooltipBackground = colorPalette.navy800;
|
|||||||
export const tooltipBorder = colorPalette.navy700;
|
export const tooltipBorder = colorPalette.navy700;
|
||||||
|
|
||||||
export const calendarCellBackground = colorPalette.navy900;
|
export const calendarCellBackground = colorPalette.navy900;
|
||||||
|
export const keyboardBackground = colorPalette.navy900;
|
||||||
|
export const keyboardBorder = colorPalette.navy600;
|
||||||
|
export const keyboardText = buttonNormalText;
|
||||||
|
export const keyboardButtonBackground = buttonNormalBackground;
|
||||||
|
export const keyboardButtonShadow = 'rgba(0, 0, 0, 0.4)';
|
||||||
|
export const keyboardButtonSecondaryBackground = colorPalette.navy500;
|
||||||
|
|||||||
@@ -217,3 +217,9 @@ export const tooltipBackground = colorPalette.navy50;
|
|||||||
export const tooltipBorder = colorPalette.navy150;
|
export const tooltipBorder = colorPalette.navy150;
|
||||||
|
|
||||||
export const calendarCellBackground = colorPalette.navy100;
|
export const calendarCellBackground = colorPalette.navy100;
|
||||||
|
export const keyboardBackground = colorPalette.navy100;
|
||||||
|
export const keyboardBorder = colorPalette.navy150;
|
||||||
|
export const keyboardText = buttonNormalText;
|
||||||
|
export const keyboardButtonBackground = buttonNormalBackground;
|
||||||
|
export const keyboardButtonShadow = 'rgba(0, 0, 0, 0.2)';
|
||||||
|
export const keyboardButtonSecondaryBackground = colorPalette.navy200;
|
||||||
|
|||||||
@@ -219,3 +219,9 @@ export const tooltipBackground = colorPalette.white;
|
|||||||
export const tooltipBorder = colorPalette.navy150;
|
export const tooltipBorder = colorPalette.navy150;
|
||||||
|
|
||||||
export const calendarCellBackground = colorPalette.navy100;
|
export const calendarCellBackground = colorPalette.navy100;
|
||||||
|
export const keyboardBackground = colorPalette.navy100;
|
||||||
|
export const keyboardBorder = colorPalette.navy150;
|
||||||
|
export const keyboardText = buttonNormalText;
|
||||||
|
export const keyboardButtonBackground = buttonNormalBackground;
|
||||||
|
export const keyboardButtonShadow = 'rgba(0, 0, 0, 0.2)';
|
||||||
|
export const keyboardButtonSecondaryBackground = colorPalette.navy200;
|
||||||
|
|||||||
@@ -219,3 +219,9 @@ export const tooltipBackground = colorPalette.gray800;
|
|||||||
export const tooltipBorder = colorPalette.gray600;
|
export const tooltipBorder = colorPalette.gray600;
|
||||||
|
|
||||||
export const calendarCellBackground = colorPalette.navy900;
|
export const calendarCellBackground = colorPalette.navy900;
|
||||||
|
export const keyboardBackground = colorPalette.gray900;
|
||||||
|
export const keyboardBorder = colorPalette.gray600;
|
||||||
|
export const keyboardText = buttonNormalText;
|
||||||
|
export const keyboardButtonBackground = buttonNormalBackground;
|
||||||
|
export const keyboardButtonShadow = 'rgba(0, 0, 0, 0.4)';
|
||||||
|
export const keyboardButtonSecondaryBackground = colorPalette.gray400;
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { currencyToAmount } from './util';
|
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(
|
throw new Error(
|
||||||
msg + ': ' + JSON.stringify(state.str.slice(state.index, 10)),
|
msg + ': ' + JSON.stringify(state.str.slice(state.index, 10)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function char(state) {
|
function char(state: ArithmeticState): string {
|
||||||
return state.str[state.index];
|
return state.str[state.index];
|
||||||
}
|
}
|
||||||
|
|
||||||
function next(state) {
|
function next(state: ArithmeticState): string {
|
||||||
if (state.index >= state.str.length) {
|
if (state.index >= state.str.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -21,7 +33,7 @@ function next(state) {
|
|||||||
return ch;
|
return ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextOperator(state, op) {
|
function nextOperator(state: ArithmeticState, op: ArithmeticOp) {
|
||||||
if (char(state) === op) {
|
if (char(state) === op) {
|
||||||
next(state);
|
next(state);
|
||||||
return true;
|
return true;
|
||||||
@@ -30,7 +42,7 @@ function nextOperator(state, op) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parsePrimary(state) {
|
function parsePrimary(state: ArithmeticState): number {
|
||||||
// We only support numbers
|
// We only support numbers
|
||||||
const isNegative = char(state) === '-';
|
const isNegative = char(state) === '-';
|
||||||
if (isNegative) {
|
if (isNegative) {
|
||||||
@@ -50,7 +62,7 @@ function parsePrimary(state) {
|
|||||||
return isNegative ? -number : number;
|
return isNegative ? -number : number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseParens(state) {
|
function parseParens(state: ArithmeticState): ArithmeticAst {
|
||||||
if (char(state) === '(') {
|
if (char(state) === '(') {
|
||||||
next(state);
|
next(state);
|
||||||
const expr = parseOperator(state);
|
const expr = parseOperator(state);
|
||||||
@@ -66,7 +78,7 @@ function parseParens(state) {
|
|||||||
return parsePrimary(state);
|
return parsePrimary(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeOperatorParser(...ops) {
|
function makeOperatorParser(...ops: ArithmeticOp[]) {
|
||||||
return ops.reduce((prevParser, op) => {
|
return ops.reduce((prevParser, op) => {
|
||||||
return state => {
|
return state => {
|
||||||
let node = prevParser(state);
|
let node = prevParser(state);
|
||||||
@@ -78,15 +90,15 @@ function makeOperatorParser(...ops) {
|
|||||||
}, parseParens);
|
}, parseParens);
|
||||||
}
|
}
|
||||||
|
|
||||||
// These operators go from high to low order of precedence
|
function parse(expression: string): ArithmeticAst {
|
||||||
const parseOperator = makeOperatorParser('^', '/', '*', '-', '+');
|
const state: ArithmeticState = {
|
||||||
|
str: expression.replace(/\s/g, ''),
|
||||||
function parse(expression: string) {
|
index: 0,
|
||||||
const state = { str: expression.replace(/\s/g, ''), index: 0 };
|
};
|
||||||
return parseOperator(state);
|
return parseOperator(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
function evaluate(ast): number {
|
function evaluate(ast: ArithmeticAst): number {
|
||||||
if (typeof ast === 'number') {
|
if (typeof ast === 'number') {
|
||||||
return ast;
|
return ast;
|
||||||
}
|
}
|
||||||
@@ -99,8 +111,10 @@ function evaluate(ast): number {
|
|||||||
case '-':
|
case '-':
|
||||||
return evaluate(left) - evaluate(right);
|
return evaluate(left) - evaluate(right);
|
||||||
case '*':
|
case '*':
|
||||||
|
case '×':
|
||||||
return evaluate(left) * evaluate(right);
|
return evaluate(left) * evaluate(right);
|
||||||
case '/':
|
case '/':
|
||||||
|
case '÷':
|
||||||
return evaluate(left) / evaluate(right);
|
return evaluate(left) / evaluate(right);
|
||||||
case '^':
|
case '^':
|
||||||
return Math.pow(evaluate(left), evaluate(right));
|
return Math.pow(evaluate(left), evaluate(right));
|
||||||
@@ -129,3 +143,14 @@ export function evalArithmetic(
|
|||||||
// Never return NaN
|
// Never return NaN
|
||||||
return isNaN(result) ? defaultValue : result;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
11
yarn.lock
11
yarn.lock
@@ -185,6 +185,7 @@ __metadata:
|
|||||||
react-modal: "npm:3.16.3"
|
react-modal: "npm:3.16.3"
|
||||||
react-redux: "npm:^9.2.0"
|
react-redux: "npm:^9.2.0"
|
||||||
react-router: "npm:7.6.2"
|
react-router: "npm:7.6.2"
|
||||||
|
react-simple-keyboard: "npm:^3.8.106"
|
||||||
react-simple-pull-to-refresh: "npm:^1.3.3"
|
react-simple-pull-to-refresh: "npm:^1.3.3"
|
||||||
react-spring: "npm:^10.0.0"
|
react-spring: "npm:^10.0.0"
|
||||||
react-stately: "npm:^3.37.0"
|
react-stately: "npm:^3.37.0"
|
||||||
@@ -17080,6 +17081,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"react-simple-keyboard@npm:^3.8.106":
|
||||||
|
version: 3.8.106
|
||||||
|
resolution: "react-simple-keyboard@npm:3.8.106"
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
checksum: 10/69b75d1de7fc604c2fe8b614765a0e2f7d8a61dfbb5ad7e969531ba0fcf3ab0f2e92abc9e08528fa3d190d381b531c8a1d42f929173e882d5ce3fd3441713885
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"react-simple-pull-to-refresh@npm:^1.3.3":
|
"react-simple-pull-to-refresh@npm:^1.3.3":
|
||||||
version: 1.3.3
|
version: 1.3.3
|
||||||
resolution: "react-simple-pull-to-refresh@npm:1.3.3"
|
resolution: "react-simple-pull-to-refresh@npm:1.3.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user