diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 62bbda1..5f5f4e9 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -71,6 +71,9 @@ module.exports = { "plugin:import/recommended", "plugin:import/typescript", ], + rules: { + "react/prop-types": "off", + }, }, // Node diff --git a/app/components/organisms/reception/Calculator.tsx b/app/components/organisms/reception/Calculator.tsx index b184cc4..499551e 100644 --- a/app/components/organisms/reception/Calculator.tsx +++ b/app/components/organisms/reception/Calculator.tsx @@ -1,45 +1,67 @@ -import { Box, Button, HStack, Text, VStack } from "@chakra-ui/react" +import { Button, Grid, Text, Tooltip, VStack } from "@chakra-ui/react" import { FC, memo } from "react" +import { useCalculator } from "~/hooks/useCalculator" +import { renderToken } from "~/lib/calculator" + +export type CalculatorProps = { + total: number +} + +export const Calculator: FC = memo(({ total }) => { + const { onInput, clear, tokens, input, calculate } = useCalculator() -export const Calculator: FC = memo(() => { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - + + {tokens.map(renderToken).join(" ")} + + {input} + - + + + + + + + + + + + + + + + + + + + + + ) }) - Calculator.displayName = "Calculator" diff --git a/app/hooks/useCalculator.ts b/app/hooks/useCalculator.ts new file mode 100644 index 0000000..31bdcb1 --- /dev/null +++ b/app/hooks/useCalculator.ts @@ -0,0 +1,46 @@ +import { useState } from "react" +import { calculate as calculateTokens, Token } from "~/lib/calculator" + +export const useCalculator = () => { + const [tokens, setTokens] = useState([]) + const [input, setInput] = useState(0) + + const pushToken = (token: Token) => setTokens((prev) => [...prev, token]) + const popToken = () => setTokens((prev) => prev.slice(0, -1)) + const clearTokens = () => setTokens([]) + + const onInput = (token: Token) => { + if (typeof token === "number") { + setInput((prev) => Number.parseInt(prev.toString() + token.toString())) + } else { + pushToken(input) + setInput(0) + pushToken(token) + } + } + + const clear = () => { + setInput(0) + clearTokens() + } + + const calculate = () => { + const output = calculateTokens([...tokens, input]) + clearTokens() + setInput(output) + return output + } + + return { + tokens, + setTokens, + pushToken, + popToken, + clearTokens, + input, + setInput, + onInput, + clear, + calculate, + } as const +} diff --git a/app/lib/calculator.ts b/app/lib/calculator.ts new file mode 100644 index 0000000..8a1e5e1 --- /dev/null +++ b/app/lib/calculator.ts @@ -0,0 +1,124 @@ +export type ParenthesisToken = "(" | ")" + +export type OperatorToken = "+" | "-" | "*" | "/" + +export type Token = number | OperatorToken | ParenthesisToken + +export type Expression = Num | Operator + +export type Num = { + type: "number" + value: number + lhs?: Expression + rhs?: Expression +} + +export type Operator = { + type: "operator" + value: OperatorToken + lhs: Expression + rhs: Expression +} + +export const renderToken = (token: Token): string => { + switch (token) { + case "*": + return "×" + case "-": + return "−" + default: + return token.toString() + } +} + +export const isOperator = (token?: Token): token is OperatorToken => + token === "+" || token === "-" || token === "*" || token === "/" + +export const isNumber = (token?: Token): token is number => + typeof token === "number" + +export const calculate = (tokens: Token[]): number => { + const expr = parse(tokens) + return calculateExpr(expr) +} + +const calculateExpr = (expr: Expression): number => { + if (expr.type === "number") { + return expr.value + } + + if (expr.type === "operator") { + const lhs = calculateExpr(expr.lhs) + const rhs = calculateExpr(expr.rhs) + switch (expr.value) { + case "+": + return lhs + rhs + case "-": + return lhs - rhs + case "*": + return lhs * rhs + case "/": + return lhs / rhs + } + } + throw new Error("Invalid expression") +} + +// primary = "(" expr ")" | number +const parsePrimary = (tokens: Token[]): [Expression, Token[]] => { + const token = tokens[0] + if (token === "(") { + const [expr, rest] = parseExpr(tokens.slice(1)) + if (rest[0] !== ")") { + throw new Error("Invalid expression, expected ')'") + } + return [expr, rest.slice(1)] + } + if (typeof token === "number") { + return [{ type: "number", value: token }, tokens.slice(1)] + } + throw new Error("Invalid expression, expected number or '('") +} + +// mul = primary ("*" primary | "/" primary)* +const parseMul = (tokens: Token[]): [Expression, Token[]] => { + let [lhs, rest] = parsePrimary(tokens) + while (rest.length > 0) { + const token = rest[0] + if (token === "*" || token === "/") { + const [rhs, rest2] = parsePrimary(rest.slice(1)) + lhs = { type: "operator", value: token, lhs, rhs } + rest = rest2 + } else { + break + } + } + return [lhs, rest] +} + +// expr = mul ("+" mul | "-" mul)* +const parseExpr = (tokens: Token[]): [Expression, Token[]] => { + let [lhs, rest] = parseMul(tokens) + while (rest.length > 0) { + const token = rest[0] + if (token === "+" || token === "-") { + const [rhs, rest2] = parseMul(rest.slice(1)) + lhs = { type: "operator", value: token, lhs, rhs } + rest = rest2 + } else { + break + } + } + return [lhs, rest] +} + +/** + * 数式のトークン列をパースして、計算順序を表す木構造を返す + */ +const parse = (tokens: Token[]): Expression => { + const [expr, rest] = parseExpr(tokens) + if (rest.length > 0) { + throw new Error("Invalid expression") + } + return expr +} diff --git a/app/routes/reception.tsx b/app/routes/reception.tsx index efd7dea..7bb9930 100644 --- a/app/routes/reception.tsx +++ b/app/routes/reception.tsx @@ -239,7 +239,7 @@ export default function Reception() { /> - +