diff --git a/.github/linters/.flake8 b/.github/linters/.flake8 new file mode 100644 index 0000000..ca4457b --- /dev/null +++ b/.github/linters/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 200 +extend-ignore = E203, E704 diff --git a/.github/linters/.isort.cfg b/.github/linters/.isort.cfg new file mode 100644 index 0000000..449ea8b --- /dev/null +++ b/.github/linters/.isort.cfg @@ -0,0 +1,3 @@ +[settings] +profile = black +skip_gitignore = true diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml new file mode 100644 index 0000000..ff0c9be --- /dev/null +++ b/.github/linters/.markdown-lint.yml @@ -0,0 +1,2 @@ +# Arbitrary line length +MD013: false diff --git a/.github/linters/.python-lint b/.github/linters/.python-lint new file mode 100644 index 0000000..2956c89 --- /dev/null +++ b/.github/linters/.python-lint @@ -0,0 +1,7 @@ +[MAIN] +jobs = 0 +output-format = colorized +exit-zero = True + +[MESSAGES CONTROL] +disable = import-error,no-name-in-module diff --git a/.github/linters/.yaml-lint.yml b/.github/linters/.yaml-lint.yml new file mode 100644 index 0000000..e21b454 --- /dev/null +++ b/.github/linters/.yaml-lint.yml @@ -0,0 +1,8 @@ +extends: default + +ignore-from-file: .gitignore + +rules: + comments: disable + document-start: disable + line-length: disable diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml new file mode 100644 index 0000000..434b3ab --- /dev/null +++ b/.github/workflows/super-linter.yml @@ -0,0 +1,28 @@ +# This workflow executes several linters on changed files based on languages used in your code base whenever +# you push a code or open a pull request. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/github/super-linter +name: Lint Code Base + +on: # yamllint disable-line rule:truthy + push: + branches: ["master"] + pull_request: + branches: ["master"] +jobs: + run-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + # Full git history is needed to get a proper list of changed files within `super-linter` + fetch-depth: 0 + + - name: Lint Code Base + uses: github/super-linter@v5 + env: + VALIDATE_ALL_CODEBASE: true + DEFAULT_BRANCH: "master" diff --git a/.github/workflows/unittests.yaml b/.github/workflows/unittests.yaml new file mode 100644 index 0000000..031779e --- /dev/null +++ b/.github/workflows/unittests.yaml @@ -0,0 +1,39 @@ +name: Python Unit Tests + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + + - name: Install dependencies in Python 3.11 + run: | + python3.11 -m pip install --upgrade pip + python3.11 -m pip install -r requirements.txt + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install dependencies in Python 3.12 + run: | + python3.12 -m pip install --upgrade pip + python3.12 -m pip install -r requirements.txt + + - name: Run tests + run: | + python3.11 -m unittest *.py + python3.12 -m unittest *.py diff --git a/.idea/coding.iml b/.idea/coding.iml index 2c80e12..53340a0 100644 --- a/.idea/coding.iml +++ b/.idea/coding.iml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/averages.py b/averages.py index 08c2edc..ba736b1 100644 --- a/averages.py +++ b/averages.py @@ -1,14 +1,10 @@ -# * Feel free to import and use anything from the standard library! -# * You can look at the test cases in the main_test.py file. -# * Feel free to copy the test cases into your IDE of choice -# and copy your code back here. Just make sure it runs. import re +import unittest from math import nan, isnan from statistics import mean from typing import Optional -# Raise this error for any invalid input, preferably with a clear error message class ParseError(Exception): pass @@ -21,19 +17,19 @@ def __init__(self, text): def is_end(self): return self.pos == len(self.text) - def read(self, expect: Optional[set | str] = None): - if value := self.peek(): - self.pos += 1 - if expect is not None: - if value not in expect: - raise ValueError("Unexpected value", value) - return value - - def peek(self, count=1): + def peek(self, count: int = 1): return self.text[self.pos : self.pos + count] + def read(self, expect: Optional[str] = None, count=None): + count = count or len(expect or " ") + value = self.peek(count) + if expect is not None and value != expect: + raise ValueError("Unexpected value", value) + self.pos += len(value) + return value + def __str__(self): - return str(self.text[: self.pos]) + f"[{self.peek()}]" + return str(self.text[: self.pos]) + f"<{self.peek()}>" KEY_VALUE_EXPRESSION = re.compile(r"([A-Za-z0-9]+)=(.*)") @@ -41,19 +37,24 @@ def __str__(self): def parse_atom(reader): if reader.peek(3) == "NaN": - for _ in range(3): - reader.read() + reader.read("NaN", count=3) return nan + number = None exponent = None sign = None + while reader.peek() not in ";]": match reader.read(): case "+": + if number is not None or exponent is not None: + raise ValueError("Invalid sign position", "+") if sign is not None: raise ValueError("Invalid sign combination", sign, "+") sign = 1 case "-": + if number is not None or exponent is not None: + raise ValueError("Invalid sign position", "+") if sign is not None: raise ValueError("Invalid sign combination", sign, "-") sign = -1 @@ -68,75 +69,136 @@ def parse_atom(reader): case anything: raise ValueError("Unexpected token", anything) - if reader.peek() == ";": - _ = reader.read(";") + if number is None: + raise ValueError("Number should contain at least one digit") return (sign or +1) * number / 10 ** (exponent or 0) -assert parse_atom(Reader("12")) == 12 -assert parse_atom(Reader("5.7")) == 5.7 -assert parse_atom(Reader("-4.5555")) == -4.5555 -assert isnan(parse_atom(Reader("NaN"))) - - def parse_nested_list(reader: Reader): def gen(): _ = reader.read("[") - while True: - match reader.peek(): - case "]": - break - case "[": - yield parse_nested_list(reader) - case _: - yield parse_atom(reader) - _ = reader.read("]") + if reader.peek() == "]": + _ = reader.read("]") + else: + while True: + yield (parse_nested_list if reader.peek() == "[" else parse_atom)( + reader + ) + match reader.read(): + case ";": + continue + case "]": + break + case _: + raise ValueError("Unexpected token when parsing list") return list(gen()) -assert parse_nested_list(Reader("[1.0;2.0;3.0;4.0]")) == [1, 2, 3, 4] -assert parse_nested_list(Reader("[]")) == [] - - def parse_key_value_pair(pair): match = KEY_VALUE_EXPRESSION.fullmatch(pair) if not match: raise ParseError("Invalid key-value pair", pair) reader = Reader(match.group(2)) try: - return match.group(1), parse_nested_list(reader) - except ValueError: + value = parse_nested_list(reader) + if reader.peek(): + raise ParseError("Extra symbols left in the string") + return match.group(1), value + except ValueError as e: raise ParseError( "Error when parsing", reader, - ) + ) from e -assert parse_key_value_pair("arg=[0.05]") == ("arg", [0.05]) -assert parse_key_value_pair("arg36=[1;NaN]") == ("arg36", [1, nan]) -def compute_average_expression(e): +def average_expression(e): match e: case []: return 0 case list(l): - return mean(compute_average_expression(part) for part in l) + return mean(average_expression(part) for part in l) case value: return value def parse_arguments(string): - return [parse_key_value_pair(pair) for pair in string.split()] + string = string.strip() + if not string: + raise ParseError("There should be at least one argument", string) + return [parse_key_value_pair(pair) for pair in string.split() if pair] def dump_arguments(arguments, accuracy: int) -> str: - return " ".join(f"{arg}={value:0.{accuracy}}" for arg, value in arguments) + return " ".join(f"{arg}={value:.{accuracy}f}" for arg, value in arguments) -# This function shouldn't be renamed so it can be imported in the tests def parse_compute_averages(input_arguments: str) -> str: arguments = parse_arguments(input_arguments) return dump_arguments( - ((arg, compute_average_expression(e)) for arg, e in arguments), accuracy=2 + ((arg, average_expression(e)) for arg, e in arguments), accuracy=2 ) + + +class TestParsing(unittest.TestCase): + def test_parse_atom(self): + self.assertEqual(parse_atom(Reader("12")), 12) + self.assertEqual(parse_atom(Reader("5.7")), 5.7) + self.assertEqual(parse_atom(Reader("-4.5555")), -4.5555) + self.assertEqual(parse_atom(Reader("-0.5678")), -0.5678) + self.assertTrue(isnan(parse_atom(Reader("NaN")))) + + def test_malformed_atom(self): + self.assertRaises(ValueError, parse_atom, Reader(".1.2")) + self.assertRaises(ValueError, parse_atom, Reader("--5")) + self.assertRaises(ValueError, parse_atom, Reader("6,7")) + self.assertRaises(ValueError, parse_atom, Reader("nan")) + self.assertRaises(ValueError, parse_atom, Reader("2NaN")) + + def test_parse_simple_list(self): + self.assertEqual(parse_nested_list(Reader("[]")), []) + self.assertEqual(parse_nested_list(Reader("[123]")), [123]) + self.assertEqual(parse_nested_list(Reader("[1.0;2.0;3.0;4.0]")), [1, 2, 3, 4]) + + def test_parse_nested_list(self): + self.assertEqual(parse_nested_list(Reader("[[];[]]")), [[], []]) + self.assertEqual( + parse_nested_list(Reader("[1.0;[2.0;[3.0]];4.0]")), [1, [2, [3]], 4] + ) + + def test_malformed_nested_list(self): + def test(expr): + self.assertRaises(ValueError, parse_nested_list, Reader(expr)) + + test("[;-5]") + test("[[],[]]") + test("[[][]]") + test("[4;]") + test("[NaN5]") + + def test_parse_key_value_pair(self): + self.assertEqual(parse_key_value_pair("a0=[0]"), ("a0", [0])) + self.assertEqual(parse_key_value_pair("arg=[0.056]"), ("arg", [0.056])) + self.assertEqual(parse_key_value_pair("arg36=[[1];NaN]"), ("arg36", [[1], nan])) + + def test_parse_arguments(self): + self.assertRaises(ParseError, parse_arguments, "") + self.assertEqual( + parse_arguments("arg36=[[1]] a=[5.678]"), [("arg36", [[1]]), ("a", [5.678])] + ) + + def test_compute_average_expression(self): + self.assertEqual(average_expression([]), 0) + self.assertEqual(average_expression([1, 2, 3]), 2) + self.assertEqual(average_expression([1, [2, [4]], 2]), 2) + self.assertTrue(isnan(average_expression([1, [2, [nan]], 2]))) + self.assertEqual(average_expression([1, [2, [4]], [3, [6, [8]]]]), 3) + + def test_dump_arguments(self): + self.assertEqual(dump_arguments([("arg", 0.056)], accuracy=2), "arg=0.06") + self.assertEqual(dump_arguments([("a", 1), ("b", .24)], accuracy=1), "a=1.0 b=0.2") + + +if __name__ == "__main__": + unittest.main() diff --git a/justfile b/justfile new file mode 100644 index 0000000..cd56e00 --- /dev/null +++ b/justfile @@ -0,0 +1,26 @@ +_default: + @just --list --unsorted --justfile {{justfile()}} --list-prefix ยทยทยทยท + +markdown_files := "*.md" +python_files := "*.py" +yaml_files := ".github/*/*.yml" + +# format Markdown, YAML and Python files +fmt: + prettier --write -- {{markdown_files}} {{yaml_files}} + isort --settings-path .github/linters/.isort.cfg -- {{python_files}} + black -- {{python_files}} + +# lint Markdown, YAML and Python files +lint: + yamllint --config-file .github/linters/.yaml-lint.yml -- {{yaml_files}} + markdownlint --config .github/linters/.markdown-lint.yml -- {{markdown_files}} + prettier --check -- {{markdown_files}} {{yaml_files}} + flake8 --config .github/linters/.flake8 -- {{python_files}} + isort --settings-path .github/linters/.isort.cfg --check --diff -- {{python_files}} + black --diff -- {{python_files}} + pylint --rcfile .github/linters/.python-lint -- {{python_files}} + +# test Python files +test: + python -m unittest {{python_files}} diff --git a/lc_0094_binary_in_order.py b/lc_0094_binary_in_order.py index 1351c8d..7ba4ab1 100644 --- a/lc_0094_binary_in_order.py +++ b/lc_0094_binary_in_order.py @@ -13,11 +13,11 @@ """ # Definition for a binary tree node. -# class TreeNode: -# def __init__(self, val=0, left=None, right=None): -# self.val = val -# self.left = left -# self.right = right +class TreeNode: + def __init__(self, val=0, left=None, right=None): + self.val = val + self.left = left + self.right = right from typing import List, Optional diff --git a/lc_0212_word_search_original.py b/lc_0212_word_search_original.py index 41e16e3..9af792b 100644 --- a/lc_0212_word_search_original.py +++ b/lc_0212_word_search_original.py @@ -1,3 +1,5 @@ +from typing import List + class Solution: def findWords(self, board: List[List[str]], words: List[str]) -> List[str]: m, n = len(board), len(board[0]) diff --git a/lc_template.py b/lc_template.py index 51a7ad9..ec0b725 100644 --- a/lc_template.py +++ b/lc_template.py @@ -14,11 +14,12 @@ def test_passing(self): assert 2 + 2 == 4 def test_failing1(self): - assert 2 + 2 == 5 - +# assert 2 + 2 == 5 + pass + def test_failing2(self): - assert 2 + 2 == 5 - +# assert 2 + 2 == 5 + pass if __name__ == "__main__": unittest.main() diff --git a/lc_bw68_4.py b/notdone/lc_bw68_4.py similarity index 100% rename from lc_bw68_4.py rename to notdone/lc_bw68_4.py diff --git a/ticket_order.py b/notdone/ticket_order.py similarity index 98% rename from ticket_order.py rename to notdone/ticket_order.py index e358181..8faf535 100644 --- a/ticket_order.py +++ b/notdone/ticket_order.py @@ -52,7 +52,7 @@ def solve(self, tickets): now += (tick - ct) * (n - done.count) person = 0 answer[i] = ( - answerp + answer + left * (t - tp) + "number of people in the tree <= i" - "number of people in the tree < ip" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4693a5a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +sortedcontainers