Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 簡易レジのロジックを実装 #47

Merged
merged 20 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a11f8bb
refactor: Productの作成・更新に使うフォームのコンポーネントを作成
r4ai Nov 18, 2024
675f921
fix: デザインを既存のものに合わせる
r4ai Nov 18, 2024
1583f60
feat: 商品登録フォームを作成
r4ai Nov 18, 2024
f3faae3
fix: remove default values
r4ai Nov 18, 2024
f54456e
fix: @tanstack/formが上手く動かなかったためconformに移行
r4ai Nov 18, 2024
8a2232f
fix: フォーム提出成功時にフォームをリセットする
r4ai Nov 18, 2024
b6a3720
feat: 商品一覧を追加
r4ai Nov 18, 2024
0c27096
fix: 削除・更新モーダルを修正
r4ai Nov 19, 2024
2be5d0d
refactor
r4ai Nov 19, 2024
ce86702
feat: `/register`で作成した商品登録フォームを表示するよう変更
r4ai Nov 19, 2024
085941b
refactor
r4ai Nov 19, 2024
5f17a81
feat: 計算のロジックを追加gs
r4ai Nov 19, 2024
255812c
fix: フォントサイズにより少しレイアウトが崩れていたのを修正
r4ai Nov 19, 2024
bd5a720
feat: 電卓を実装
r4ai Nov 19, 2024
718755c
fix: 2桁以上の数値を入力した際に壊れる問題を修正
r4ai Nov 19, 2024
f8c7b1c
chore: Lintエラーを解決するために、TypeScriptではreact/prop-typesルールを無効化
r4ai Nov 19, 2024
67ff14a
refactor: remove dead code
r4ai Nov 19, 2024
2d0a1f3
Merge branch 'main' into feature/issue-31-calculator-logic
r4ai Nov 19, 2024
7688ed6
refactor: ファイル名をより一般的な名称に変更
r4ai Nov 19, 2024
861be59
Merge branch 'main' into feature/issue-31-calculator-logic
r4ai Nov 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ module.exports = {
"plugin:import/recommended",
"plugin:import/typescript",
],
rules: {
"react/prop-types": "off",
},
},

// Node
Expand Down
88 changes: 55 additions & 33 deletions app/components/organisms/reception/Calculator.tsx
Original file line number Diff line number Diff line change
@@ -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<CalculatorProps> = memo(({ total }) => {
const { onInput, clear, tokens, input, calculate } = useCalculator()

export const Calculator: FC = memo(() => {
return (
<Box
w="300px"
h="250px"
<VStack
w="fit-content"
bg="blackAlpha.500"
borderRadius="10px"
shadow="lg"
p={4}
gap={4}
>
<VStack>
<Text></Text>
<HStack>
<Button>7</Button>
<Button>8</Button>
<Button>9</Button>
<Button>*</Button>
</HStack>
<HStack>
<Button>4</Button>
<Button>5</Button>
<Button>6</Button>
<Button>-</Button>
</HStack>
<HStack>
<Button>1</Button>
<Button>2</Button>
<Button>3</Button>
<Button>+</Button>
</HStack>
<HStack>
<Button>0</Button>
<Button>T</Button>
<Button>C</Button>
<Button>=</Button>
</HStack>
<VStack
gap={0}
py={2}
px={4}
minH={20}
justifyContent="center"
alignItems="end"
bg="InfoBackground"
rounded="md"
width="full"
>
<Text color="GrayText">{tokens.map(renderToken).join(" ")}</Text>
<Text color="InfoText" fontSize="xl">
{input}
</Text>
</VStack>
</Box>
<Grid
w="fit-content"
gap={4}
templateColumns="repeat(4, 1fr)"
placeItems="center"
placeContent="center"
>
<Button onClick={() => onInput(7)}>7</Button>
<Button onClick={() => onInput(8)}>8</Button>
<Button onClick={() => onInput(9)}>9</Button>
<Button onClick={() => onInput("*")}>×</Button>
<Button onClick={() => onInput(4)}>4</Button>
<Button onClick={() => onInput(5)}>5</Button>
<Button onClick={() => onInput(6)}>6</Button>
<Button onClick={() => onInput("-")}>−</Button>
<Button onClick={() => onInput(1)}>1</Button>
<Button onClick={() => onInput(2)}>2</Button>
<Button onClick={() => onInput(3)}>3</Button>
<Button onClick={() => onInput("+")}>+</Button>
<Button onClick={() => onInput(0)}>0</Button>
<Tooltip hasArrow label="合計金額">
<Button onClick={() => onInput(total)}>T</Button>
</Tooltip>
<Button onClick={() => clear()}>C</Button>
<Button onClick={() => calculate()}>=</Button>
</Grid>
</VStack>
)
})

Calculator.displayName = "Calculator"
46 changes: 46 additions & 0 deletions app/hooks/useCalculator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useState } from "react"
import { calculate as calculateTokens, Token } from "~/lib/calculator"

export const useCalculator = () => {
const [tokens, setTokens] = useState<Token[]>([])
const [input, setInput] = useState<number>(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
}
124 changes: 124 additions & 0 deletions app/lib/calculator.ts
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion app/routes/reception.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export default function Reception() {
/>
</FormControl>
</Stack>
<Calculator />
<Calculator total={total} />
</VStack>
</ModalBody>
<ModalFooter gap={4}>
Expand Down