Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .github/workflows/run-tests-impls.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ jobs:
if: ${{ runner.os == 'Windows' && matrix.target-impl == 'cpp' }}
uses: microsoft/[email protected]

- name: Setup mypy
if: ${{ matrix.target-impl == 'python' }}
run: pip install mypy

- name: Run tests with ${{ matrix.target-impl }} on ${{ matrix.os }}
shell: pwsh
run: |
Expand Down
1 change: 1 addition & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"recommendations": [
"editorconfig.editorconfig",
"ms-dotnettools.csharp",
"ms-python.mypy-type-checker",
"ms-vscode.cpptools"
]
}
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"streambuf": "cpp",
"typeinfo": "cpp"
},
"mypy-type-checker.args": ["--config-file=${workspaceFolder}/mypy.ini"],
"[jsonc]": {
"editor.formatOnSave": false,
"editor.formatOnType": false,
Expand Down
21 changes: 21 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SPDX-FileCopyrightText: 2025 smdn <[email protected]>
# SPDX-License-Identifier: MIT
# cSpell:ignore mypy
[mypy]
python_version = 3.12

strict = true

show_error_codes = true

disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_globals = true
disallow_untyped_defs = true
disallow_incomplete_defs = true

no_implicit_optional = true

warn_return_any = true
warn_unused_ignores = true
warn_unreachable = true
15 changes: 15 additions & 0 deletions src/impls/python/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
MYPY = mypy --config-file ../../../mypy.ini

all: type-check run

type-check:
$(MYPY) polish.py

run:
@if [ -z "${INPUT}" ]; then \
./polish.py; \
fi

@if [ -n "${INPUT}" ]; then \
echo ${INPUT} | ./polish.py; \
fi
3 changes: 3 additions & 0 deletions src/impls/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@ infix notation: ((2 + (5 * 3)) - 4)
polish notation: - + 2 * 5 3 4
calculated result: 13
```

# 型チェック
型ヒントの検証を行う場合は、`make type-check`を実行してください。 なお、`mypy`がインストールされている必要があります。
82 changes: 45 additions & 37 deletions src/impls/python/polish.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,38 @@
# SPDX-FileCopyrightText: 2022 smdn <[email protected]>
# SPDX-License-Identifier: MIT
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import Callable
from typing import IO
import sys

# 与えられた式が不正な形式であることを報告するための例外クラス
class MalformedExpressionException(Exception):
def __init__(self, message):
def __init__(self, message: str):
super().__init__(message)

# ノードを構成するデータ構造
class Node:
__expression: str # このノードが表す式(二分木への分割後は演算子または項となる)
__left: Node | None # 左の子ノード
__right: Node | None # 右の子ノード

# コンストラクタ(与えられた式expressionを持つノードを構成する)
def __init__(self, expression):
def __init__(self, expression: str):
# 式expressionにおける括弧の対応数をチェックする
Node.__validate_bracket_balance(expression)

# チェックした式expressionをこのノードが表す式として設定する
self.__expression = expression # このノードが表す式(二分木への分割後は演算子または項となる)
self.__expression = expression

self.__left = None # 左の子ノード
self.__right = None # 右の子ノード
self.__left = None
self.__right = None

# 式expression内の括弧の対応を検証するメソッド
# 開き括弧と閉じ括弧が同数でない場合はエラーとする
@staticmethod
def __validate_bracket_balance(expression):
nest_depth = 0 # 丸括弧の深度(くくられる括弧の数を計上するために用いる)
def __validate_bracket_balance(expression: str) -> None:
nest_depth: int = 0 # 丸括弧の深度(くくられる括弧の数を計上するために用いる)

# 1文字ずつ検証する
for ch in expression:
Expand All @@ -51,12 +58,12 @@ def __validate_bracket_balance(expression):
raise MalformedExpressionException("unbalanced bracket: {}".format(expression))

# 式expressionを二分木へと分割するメソッド
def parse_expression(self):
def parse_expression(self) -> None:
# 式expressionから最も外側にある丸括弧を取り除く
self.__expression = Node.__remove_outermost_bracket(self.__expression)

# 式expressionから演算子を探して位置を取得する
pos_operator = Node.__get_operator_position(self.__expression)
pos_operator: int = Node.__get_operator_position(self.__expression)

if pos_operator < 0:
# 式expressionに演算子が含まれない場合、expressionは項であるとみなす
Expand Down Expand Up @@ -86,9 +93,9 @@ def parse_expression(self):

# 式expressionから最も外側にある丸括弧を取り除いて返すメソッド
@staticmethod
def __remove_outermost_bracket(expression):
has_outermost_bracket = False # 最も外側に括弧を持つかどうか
nest_depth = 0 # 丸括弧の深度(式中で開かれた括弧が閉じられたかどうか調べるために用いる)
def __remove_outermost_bracket(expression: str) -> str:
has_outermost_bracket: bool = False # 最も外側に括弧を持つかどうか
nest_depth: int = 0 # 丸括弧の深度(式中で開かれた括弧が閉じられたかどうか調べるために用いる)

if expression[0] == "(":
# 0文字目が開き丸括弧の場合、最も外側に丸括弧があると仮定する
Expand Down Expand Up @@ -138,12 +145,14 @@ def __remove_outermost_bracket(expression):
# 式expressionから最も右側にあり、かつ優先順位が低い演算子を探して位置を返すメソッド
# (演算子がない場合は-1を返す)
@staticmethod
def __get_operator_position(expression):
pos_operator = -1 # 現在見つかっている演算子の位置(初期値として-1=演算子なしを設定)
current_priority = sys.maxsize # 現在見つかっている演算子の優先順位(初期値としてsys.maxsizeを設定)
nest_depth = 0 # 丸括弧の深度(括弧でくくられていない部分の演算子を「最も優先順位が低い」と判断するために用いる)
def __get_operator_position(expression: str) -> int:
pos_operator: int = -1 # 現在見つかっている演算子の位置(初期値として-1=演算子なしを設定)
current_priority: int = sys.maxsize # 現在見つかっている演算子の優先順位(初期値としてsys.maxsizeを設定)
nest_depth: int = 0 # 丸括弧の深度(括弧でくくられていない部分の演算子を「最も優先順位が低い」と判断するために用いる)

# 与えられた文字列を先頭から1文字ずつ検証する
i: int

for i in range(len(expression)):
priority = 0 # 演算子の優先順位(値が低いほど優先順位が低いものとする)

Expand Down Expand Up @@ -183,10 +192,10 @@ def __get_operator_position(expression):
# 二分木を巡回し、ノードの行きがけ・通りがけ・帰りがけに指定された関数オブジェクトをコールバックするメソッド
def traverse(
self,
on_visit, # ノードの行きがけにコールバックする関数オブジェクト
on_transit, # ノードの通りがけにコールバックする関数オブジェクト
on_leave # ノードの帰りがけにコールバックする関数オブジェクト
):
on_visit: Callable[[Node], int | None] | None, # ノードの行きがけにコールバックする関数オブジェクト
on_transit: Callable[[Node], int | None] | None, # ノードの通りがけにコールバックする関数オブジェクト
on_leave: Callable[[Node], int | None] | None # ノードの帰りがけにコールバックする関数オブジェクト
) -> None:
# このノードの行きがけに行う動作をコールバックする
if on_visit:
on_visit(self)
Expand All @@ -209,7 +218,7 @@ def traverse(

# 後行順序訪問(帰りがけ順)で二分木を巡回して
# すべてのノードの演算子または項をoutに出力するメソッド
def write_postorder(self, out):
def write_postorder(self, out: IO[str]) -> None:
# 巡回を開始する
self.traverse(
None, # ノードへの行きがけには何もしない
Expand All @@ -221,14 +230,14 @@ def write_postorder(self, out):

# 通りがけ順で巡回する際に、行きがけにコールバックさせる関数
@staticmethod
def __write_inorder_on_visit(out, node):
def __write_inorder_on_visit(out: IO[str], node: Node) -> None:
# 左右に項を持つ場合、読みやすさのために項の前(行きがけ)に開き括弧を補う
if node.__left and node.__right:
out.write('(')

# 通りがけ順で巡回する際に、通りがけにコールバックさせる関数
@staticmethod
def __write_inorder_on_transit(out, node):
def __write_inorder_on_transit(out: IO[str], node: Node) -> None:
# 左に子ノードを持つ場合は、読みやすさのために空白を補う
if node.__left:
out.write(' ')
Expand All @@ -242,13 +251,13 @@ def __write_inorder_on_transit(out, node):

# 通りがけ順で巡回する際に、帰りがけにコールバックさせる関数
@staticmethod
def __write_inorder_on_leave(out, node):
def __write_inorder_on_leave(out: IO[str], node: Node) -> None:
if node.__left and node.__right:
out.write(')')

# 中間順序訪問(通りがけ順)で二分木を巡回して
# すべてのノードの演算子または項をoutに出力するメソッド
def write_inorder(self, out):
def write_inorder(self, out: IO[str]) -> None:
# 巡回を開始する
self.traverse(
# ノードへの行きがけに、必要なら開き括弧を補う
Expand All @@ -261,7 +270,7 @@ def write_inorder(self, out):

# 先行順序訪問(行きがけ順)で二分木を巡回して
# すべてのノードの演算子または項をwriterに出力するメソッド
def write_preorder(self, out):
def write_preorder(self, out: IO[str]) -> None:
# 巡回を開始する
self.traverse(
# ノードへの行きがけに、ノードの演算子または項を出力する
Expand All @@ -273,7 +282,7 @@ def write_preorder(self, out):

# 後行順序訪問(帰りがけ順)で二分木を巡回して、二分木全体の値を計算するメソッド
# すべてのノードの値が計算できた場合はその値、そうでない場合(記号を含む場合など)はNoneを返す
def calculate_expression_tree(self):
def calculate_expression_tree(self) -> (float | None):
# 巡回を開始する
# ノードからの帰りがけに、ノードが表す部分式から、その値を計算する
# 帰りがけに計算することによって、末端の部分木から順次計算し、再帰的に木全体の値を計算する
Expand All @@ -286,11 +295,10 @@ def calculate_expression_tree(self):
# ノードの値を数値に変換し、計算結果を返す
return Node.__parse_number(self.__expression)


# 与えられたノードの演算子と左右の子ノードの値から、ノードの値を計算する関数
# 計算できた場合、計算結果の値はnode.__expressionに文字列として代入し、左右のノードは削除する
@staticmethod
def calculate_node(node):
def calculate_node(node: Node) -> None:
# 左右に子ノードを持たない場合、現在のノードは部分式ではなく項であり、
# それ以上計算できないので処理を終える
if not node.__left or not node.__right:
Expand All @@ -301,14 +309,14 @@ def calculate_node(node):
# ノードの値が計算できないものとして、処理を終える

# 左ノードの値を数値に変換して演算子の左項left_operandの値とする
left_operand = Node.__parse_number(node.__left.__expression)
left_operand: (float | None) = Node.__parse_number(node.__left.__expression)

if left_operand is None:
# floatで扱える範囲外の値か、途中に変換できない文字があるため、計算できないものとして扱い、処理を終える
return

# 右ノードの値を数値に変換して演算子の右項right_operandの値とする
right_operand = Node.__parse_number(node.__right.__expression)
right_operand: (float | None) = Node.__parse_number(node.__right.__expression)

if right_operand is None:
# floatで扱える範囲外の値か、途中に変換できない文字があるため、計算できないものとして扱い、処理を終える
Expand Down Expand Up @@ -337,7 +345,7 @@ def calculate_node(node):
# 正常に変換できた場合は変換した数値を返す
# 変換できなかった場合はNoneを返す
@staticmethod
def __parse_number(expression):
def __parse_number(expression: str) -> float | None:
try:
# 与えられた文字列を数値に変換する
return float(expression)
Expand All @@ -349,9 +357,9 @@ def __parse_number(expression):
# 0: 正常終了 (二分木への分割、および式全体の値の計算に成功した場合)
# 1: 入力のエラーによる終了 (二分木への分割に失敗した場合)
# 2: 計算のエラーによる終了 (式全体の値の計算に失敗した場合)
def main():
def main() -> int:
# 標準入力から二分木に分割したい式を入力する
expression = input("input expression: ")
expression: str = input("input expression: ")

if not expression or expression.isspace():
# 入力が得られなかった場合、または入力が空白のみの場合は、処理を終了する
Expand All @@ -360,7 +368,7 @@ def main():
# 入力された式から空白を除去する(空白を空の文字列に置き換える)
expression = expression.replace(" ", "")

root = None
root: Node

try:
# 二分木の根(root)ノードを作成し、式全体を格納する
Expand Down Expand Up @@ -391,9 +399,9 @@ def main():
print()

# 分割した二分木から式全体の値を計算する
result_value = root.calculate_expression_tree()
result_value: float | None = root.calculate_expression_tree()

if result_value is not None:
if type(result_value) is float:
# 計算できた場合はその値を表示する
print("calculated result: {:.17g}".format(result_value))
return 0
Expand Down
11 changes: 11 additions & 0 deletions tests/impls/implementations.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@
"Run": { "Command": "python3", "Arguments": [ "polish.py" ] },
}
},
{
"ImplementationId": "python",
"DisplayName": "Python 3 with type annotations",
"Directory": "src/impls/python/",
"Condition": "[bool](Get-Command make -errorAction SilentlyContinue)",
"Commands": {
"Build": [ { "Command": "make", "Arguments": [ "type-check" ] } ],
"Clean": [ ],
"Run": { "Command": "python3", "Arguments": [ "polish.py" ] },
}
},
{
"ImplementationId": "visualbasic",
"DisplayName": "Visual Basic",
Expand Down
7 changes: 6 additions & 1 deletion tests/impls/run-tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ function Invoke-Tests {
Invoke-Expression "$($cmd.Command) $($cmd.Arguments -join ' ')"
}
else {
[void]$(& $cmd.Command $cmd.Arguments)
& $cmd.Command $cmd.Arguments 2>&1 | ForEach-Object { Write-Host $_ }

if ($LASTEXITCODE -ne 0) {
$done_build = $false
break
}
}
}
catch {
Expand Down