Skip to content

Commit 7eff232

Browse files
committed
feat: add Basic Calculator
1 parent 19374dc commit 7eff232

File tree

7 files changed

+282
-1
lines changed

7 files changed

+282
-1
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"problem_name": "basic_calculator",
3+
"solution_class_name": "Solution",
4+
"problem_number": "224",
5+
"problem_title": "Basic Calculator",
6+
"difficulty": "Hard",
7+
"topics": "Math, String, Stack, Recursion",
8+
"tags": ["grind-75"],
9+
"readme_description": "Given a string `s` representing a valid expression, implement a basic calculator to evaluate it, and return the result of the evaluation.\n\n**Note:** You are **not** allowed to use any built-in function which evaluates strings as mathematical expressions, such as `eval()`.",
10+
"readme_examples": [
11+
{ "content": "```\nInput: s = \"1 + 1\"\nOutput: 2\n```" },
12+
{ "content": "```\nInput: s = \" 2-1 + 2 \"\nOutput: 3\n```" },
13+
{ "content": "```\nInput: s = \"(1+(4+5+2)-3)+(6+8)\"\nOutput: 23\n```" }
14+
],
15+
"readme_constraints": "- `1 <= s.length <= 3 * 10^5`\n- `s` consists of digits, `'+'`, `'-'`, `'('`, `')'`, and `' '`.\n- `s` represents a valid expression.\n- `'+'` is **not** used as a unary operation (i.e., `\"+1\"` and `\"+(2 + 3)\"` is invalid).\n- `'-'` could be used as a unary operation (i.e., `\"-1\"` and `\"-(2 + 3)\"` is valid).\n- There will be no two consecutive operators in the input.\n- Every number and running calculation will fit in a signed 32-bit integer.",
16+
"readme_additional": "",
17+
"solution_imports": "",
18+
"solution_methods": [
19+
{ "name": "calculate", "parameters": "s: str", "return_type": "int", "dummy_return": "0" }
20+
],
21+
"test_imports": "import pytest\nfrom leetcode_py.test_utils import logged_test\nfrom .solution import Solution",
22+
"test_class_name": "BasicCalculator",
23+
"test_helper_methods": [
24+
{ "name": "setup_method", "parameters": "", "body": "self.solution = Solution()" }
25+
],
26+
"test_methods": [
27+
{
28+
"name": "test_calculate",
29+
"parametrize": "s, expected",
30+
"parametrize_typed": "s: str, expected: int",
31+
"test_cases": "[(\"1 + 1\", 2), (\" 2-1 + 2 \", 3), (\"(1+(4+5+2)-3)+(6+8)\", 23), (\"1\", 1), (\"-1\", -1), (\"-(1+2)\", -3), (\"2147483647\", 2147483647), (\"1-1+1\", 1)]",
32+
"body": "result = self.solution.calculate(s)\nassert result == expected"
33+
}
34+
],
35+
"playground_imports": "from solution import Solution",
36+
"playground_test_case": "# Example test case\ns = '(1+(4+5+2)-3)+(6+8)'\nexpected = 23",
37+
"playground_execution": "result = Solution().calculate(s)\nresult",
38+
"playground_assertion": "assert result == expected"
39+
}

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
PYTHON_VERSION = 3.13
2-
PROBLEM ?= find_median_from_data_stream
2+
PROBLEM ?= basic_calculator
33
FORCE ?= 0
44
COMMA := ,
55

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Basic Calculator
2+
3+
**Difficulty:** Hard
4+
**Topics:** Math, String, Stack, Recursion
5+
**Tags:** grind-75
6+
7+
**LeetCode:** [Problem 224](https://leetcode.com/problems/basic-calculator/description/)
8+
9+
## Problem Description
10+
11+
Given a string `s` representing a valid expression, implement a basic calculator to evaluate it, and return the result of the evaluation.
12+
13+
**Note:** You are **not** allowed to use any built-in function which evaluates strings as mathematical expressions, such as `eval()`.
14+
15+
## Examples
16+
17+
### Example 1:
18+
19+
```
20+
Input: s = "1 + 1"
21+
Output: 2
22+
```
23+
24+
### Example 2:
25+
26+
```
27+
Input: s = " 2-1 + 2 "
28+
Output: 3
29+
```
30+
31+
### Example 3:
32+
33+
```
34+
Input: s = "(1+(4+5+2)-3)+(6+8)"
35+
Output: 23
36+
```
37+
38+
## Constraints
39+
40+
- `1 <= s.length <= 3 * 10^5`
41+
- `s` consists of digits, `'+'`, `'-'`, `'('`, `')'`, and `' '`.
42+
- `s` represents a valid expression.
43+
- `'+'` is **not** used as a unary operation (i.e., `"+1"` and `"+(2 + 3)"` is invalid).
44+
- `'-'` could be used as a unary operation (i.e., `"-1"` and `"-(2 + 3)"` is valid).
45+
- There will be no two consecutive operators in the input.
46+
- Every number and running calculation will fit in a signed 32-bit integer.

leetcode/basic_calculator/__init__.py

Whitespace-only changes.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "code",
5+
"execution_count": null,
6+
"id": "imports",
7+
"metadata": {},
8+
"outputs": [],
9+
"source": [
10+
"from solution import Solution"
11+
]
12+
},
13+
{
14+
"cell_type": "code",
15+
"execution_count": null,
16+
"id": "setup",
17+
"metadata": {},
18+
"outputs": [],
19+
"source": [
20+
"# Example test case\n",
21+
"s = \"(1+(4+5+2)-3)+(6+8)\"\n",
22+
"expected = 23"
23+
]
24+
},
25+
{
26+
"cell_type": "code",
27+
"execution_count": null,
28+
"id": "execute",
29+
"metadata": {},
30+
"outputs": [],
31+
"source": [
32+
"result = Solution().calculate(s)\nresult"
33+
]
34+
},
35+
{
36+
"cell_type": "code",
37+
"execution_count": null,
38+
"id": "test",
39+
"metadata": {},
40+
"outputs": [],
41+
"source": [
42+
"assert result == expected"
43+
]
44+
}
45+
],
46+
"metadata": {
47+
"kernelspec": {
48+
"display_name": "leetcode-py-py3.13",
49+
"language": "python",
50+
"name": "python3"
51+
},
52+
"language_info": {
53+
"codemirror_mode": {
54+
"name": "ipython",
55+
"version": 3
56+
},
57+
"file_extension": ".py",
58+
"mimetype": "text/x-python",
59+
"name": "python",
60+
"nbconvert_exporter": "python3",
61+
"pygments_lexer": "ipython3",
62+
"version": "3.13.7"
63+
}
64+
},
65+
"nbformat": 4,
66+
"nbformat_minor": 5
67+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
class Solution:
2+
# Time: O(n)
3+
# Space: O(n)
4+
def calculate(self, s: str) -> int:
5+
stack = []
6+
num = 0
7+
sign = 1
8+
result = 0
9+
10+
for char in s:
11+
if char.isdigit():
12+
num = num * 10 + int(char)
13+
elif char in "+-":
14+
result += sign * num
15+
num = 0
16+
sign = 1 if char == "+" else -1
17+
elif char == "(":
18+
stack.append(result)
19+
stack.append(sign)
20+
result = 0
21+
sign = 1
22+
elif char == ")":
23+
if len(stack) < 2:
24+
raise ValueError("Mismatched parentheses")
25+
result += sign * num
26+
num = 0
27+
result *= stack.pop()
28+
result += stack.pop()
29+
elif char != " ":
30+
raise ValueError(f"Invalid character: '{char}'")
31+
32+
if stack:
33+
raise ValueError("Mismatched parentheses")
34+
35+
return result + sign * num
36+
37+
38+
# Example walkthrough: "(1+(4+5+2)-3)+(6+8)" = 23
39+
#
40+
# char | num | sign | result | stack | action
41+
# -----|-----|------|--------|------------|------------------
42+
# '(' | 0 | 1 | 0 | [0, 1] | push result=0, sign=1
43+
# '1' | 1 | 1 | 0 | [0, 1] | build num=1
44+
# '+' | 0 | 1 | 1 | [0, 1] | result += 1*1 = 1
45+
# '(' | 0 | 1 | 0 | [0,1,1,1] | push result=1, sign=1
46+
# '4' | 4 | 1 | 0 | [0,1,1,1] | build num=4
47+
# '+' | 0 | 1 | 4 | [0,1,1,1] | result += 1*4 = 4
48+
# '5' | 5 | 1 | 4 | [0,1,1,1] | build num=5
49+
# '+' | 0 | 1 | 9 | [0,1,1,1] | result += 1*5 = 9
50+
# '2' | 2 | 1 | 9 | [0,1,1,1] | build num=2
51+
# ')' | 0 | 1 | 11 | [0, 1] | result=11*1+1 = 12
52+
# '-' | 0 | -1 | 12 | [0, 1] | sign = -1
53+
# '3' | 3 | -1 | 12 | [0, 1] | build num=3
54+
# ')' | 0 | 1 | 9 | [] | result=9*1+0 = 9
55+
# '+' | 0 | 1 | 9 | [] | sign = 1
56+
# '(' | 0 | 1 | 0 | [9, 1] | push result=9, sign=1
57+
# '6' | 6 | 1 | 0 | [9, 1] | build num=6
58+
# '+' | 0 | 1 | 6 | [9, 1] | result += 1*6 = 6
59+
# '8' | 8 | 1 | 6 | [9, 1] | build num=8
60+
# ')' | 0 | 1 | 14 | [] | result=14*1+9 = 23
61+
# end | 0 | 1 | 14 | [] | return 14+1*0 = 23

leetcode/basic_calculator/tests.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import pytest
2+
3+
from leetcode_py.test_utils import logged_test
4+
5+
from .solution import Solution
6+
7+
8+
class TestBasicCalculator:
9+
def setup_method(self):
10+
self.solution = Solution()
11+
12+
@pytest.mark.parametrize(
13+
"s, expected",
14+
[
15+
("1 + 1", 2),
16+
(" 2-1 + 2 ", 3),
17+
("(1+(4+5+2)-3)+(6+8)", 23),
18+
("1", 1),
19+
("-1", -1),
20+
("-(1+2)", -3),
21+
("2147483647", 2147483647),
22+
("1-1+1", 1),
23+
# Additional edge cases
24+
("0", 0),
25+
("-0", 0),
26+
("()", 0),
27+
("((()))", 0),
28+
("1+(2+3)", 6),
29+
("(1+2)+3", 6),
30+
("1-(2+3)", -4),
31+
("(1-2)+3", 2),
32+
("-(-1)", 1),
33+
("-(-(-1))", -1),
34+
("1000000-999999", 1),
35+
("10+20-30+40", 40),
36+
("((1+2)+(3+4))", 10),
37+
("1+(2-(3+4))", -4),
38+
("-(1+(2+3))", -6),
39+
(" 1 + 2 ", 3),
40+
("123+456", 579),
41+
("-2147483648", -2147483648),
42+
],
43+
)
44+
@logged_test
45+
def test_calculate(self, s: str, expected: int):
46+
result = self.solution.calculate(s)
47+
assert result == expected
48+
49+
@pytest.mark.parametrize(
50+
"s, error_msg",
51+
[
52+
("(1+2", "Mismatched parentheses"),
53+
("1+2)", "Mismatched parentheses"),
54+
("((1+2)", "Mismatched parentheses"),
55+
("1+2))", "Mismatched parentheses"),
56+
("1*2", r"Invalid character: '\*'"),
57+
("1/2", "Invalid character: '/'"),
58+
("1%2", "Invalid character: '%'"),
59+
("1^2", r"Invalid character: '\^'"),
60+
("1&2", "Invalid character: '&'"),
61+
("a+b", "Invalid character: 'a'"),
62+
("1+2.5", r"Invalid character: '\.'"),
63+
],
64+
)
65+
@logged_test
66+
def test_calculate_invalid_input(self, s: str, error_msg: str):
67+
with pytest.raises(ValueError, match=error_msg):
68+
self.solution.calculate(s)

0 commit comments

Comments
 (0)