Skip to content

Commit

Permalink
Updates
Browse files Browse the repository at this point in the history
  • Loading branch information
sciyoshi committed Mar 2, 2025
1 parent 53831de commit 815a20f
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 492 deletions.
51 changes: 21 additions & 30 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,27 @@ on:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v2

- uses: actions/setup-python@v2
with:
python-version: 3.11

- name: cache poetry install
uses: actions/cache@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
path: ~/.local
key: poetry-0
python-version: ${{ matrix.python-version }}
- run: pip install uv
- run: uv venv
- run: uv sync
- run: uv run pytest

- uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true

- name: cache deps
id: cache-deps
uses: actions/cache@v3
with:
path: .venv
key: pydeps-${{ hashFiles('**/poetry.lock') }}

- run: poetry install --no-interaction --no-root
if: steps.cache-deps.outputs.cache-hit != 'true'

- run: poetry install --no-interaction

- run: poetry run pytest tests.py
- run: poetry run black mudder.py tests.py
- run: poetry run flake8 mudder.py
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- run: pip install uv
- run: uv venv
- run: uv pip install --requirement pyproject.toml --all-extras
- run: .venv/bin/ruff format --check .
- run: .venv/bin/ruff check .
- run: .venv/bin/mypy .
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ From the original readme:
> Generate lexicographically-spaced strings between two strings from
> pre-defined alphabets.
[1]: https://github.com/fasiha/mudderjs
This technique is also known as _fractional indexing_.

[1]: https://github.com/fasiha/mudderjs

## Example

Expand Down
96 changes: 50 additions & 46 deletions mudder.py → mudder/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import math
from decimal import Decimal, ROUND_HALF_UP
from collections.abc import Iterable, Reversible
from decimal import ROUND_HALF_UP, Decimal
from functools import partial
from itertools import chain, cycle
from operator import add
from typing import Dict, Iterable, List, Optional, Reversible, Tuple, Union

__all__ = [
"SymbolTable",
"decimal",
"alphabet",
"base36",
"base62",
"alphabet",
"decimal",
]


Expand All @@ -29,27 +29,26 @@ def is_prefix_code(strings: Iterable[str]) -> bool:

class SymbolTable:
def __init__(
self, symbols: Iterable[str], symbol_map: Optional[Dict[str, int]] = None
):
self, symbols: Iterable[str], symbol_map: dict[str, int] | None = None
) -> None:
symbols = list(symbols)
if not symbol_map:
symbol_map = dict((c, i) for i, c in enumerate(symbols))

symbol_values = set(symbol_map.values())
for i in range(len(symbols)):
if i not in symbol_values:
raise ValueError(
f"{len(symbols)} symbols given but {i} not found in symbol table"
)
msg = f"{len(symbols)} symbols given but {i} not found in symbol table"
raise ValueError(msg)

self.num2sym = symbols
self.sym2num = symbol_map
self.max_base = len(symbols)
self.is_prefix_code = is_prefix_code(symbols)

def number_to_digits(self, num: int, base: Optional[int] = None) -> List[int]:
def number_to_digits(self, num: int, base: int | None = None) -> list[int]:
base = base or self.max_base
digits: List[int] = []
digits: list[int] = []
while num >= 1:
digits.append(num % base)
num = num // base
Expand All @@ -61,20 +60,19 @@ def number_to_digits(self, num: int, base: Optional[int] = None) -> List[int]:
def digits_to_string(self, digits: Iterable[int]) -> str:
return "".join([self.num2sym[n] for n in digits])

def string_to_digits(self, string: Iterable[str]) -> List[int]:
def string_to_digits(self, string: Iterable[str]) -> list[int]:
if isinstance(string, str):
if not self.is_prefix_code:
raise ValueError(
msg = (
"Parsing without prefix code is unsupported. "
"Pass in array of stringy symbols?"
)
raise ValueError(msg)
string = (c for c in string if c in self.sym2num)

return [self.sym2num[c] for c in string]

def digits_to_number(
self, digits: Reversible[int], base: Optional[int] = None
) -> int:
def digits_to_number(self, digits: Reversible[int], base: int | None = None) -> int:
base = base or self.max_base
current_base = 1
accum = 0
Expand All @@ -83,15 +81,15 @@ def digits_to_number(
current_base *= base
return accum

def number_to_string(self, num: int, base: Optional[int] = None) -> str:
def number_to_string(self, num: int, base: int | None = None) -> str:
return self.digits_to_string(self.number_to_digits(num, base=base))

def string_to_number(self, num: Iterable[str], base: Optional[int] = None) -> int:
def string_to_number(self, num: Iterable[str], base: int | None = None) -> int:
return self.digits_to_number(self.string_to_digits(num), base=base)

def round_fraction(
self, numerator: int, denominator: int, base: Optional[int] = None
) -> List[int]:
self, numerator: int, denominator: int, base: int | None = None
) -> list[int]:
base = base or self.max_base
places = math.ceil(math.log(denominator) / math.log(base))
scale = pow(base, places)
Expand All @@ -105,12 +103,12 @@ def round_fraction(

def mudder(
self,
a: Union[Iterable[str], int] = "",
a: Iterable[str] | int = "",
b: Iterable[str] = "",
num_strings: int = 1,
base: Optional[int] = None,
num_divisions: Optional[int] = None,
) -> List[str]:
base: int | None = None,
num_divisions: int | None = None,
) -> list[str]:
if isinstance(a, int):
num_strings = a
a = ""
Expand All @@ -135,8 +133,8 @@ def mudder(


def long_div(
numerator: List[int], denominator: int, base: int
) -> Tuple[List[int], int]:
numerator: list[int], denominator: int, base: int
) -> tuple[list[int], int]:
result = []
remainder = 0
for current in numerator:
Expand All @@ -146,15 +144,16 @@ def long_div(
return result, remainder


def long_sub_same_len( # noqa: C901
a: List[int],
b: List[int],
def long_sub_same_len(
a: list[int],
b: list[int],
base: int,
remainder: Optional[Tuple[int, int]] = None,
denominator=0,
) -> Tuple[List[int], int]:
remainder: tuple[int, int] | None = None,
denominator: int = 0,
) -> tuple[list[int], int]:
if len(a) != len(b):
raise ValueError("a and b should have same length")
msg = "a and b should have same length"
raise ValueError(msg)

a = a.copy() # pre-emptively copy
if remainder:
Expand All @@ -169,7 +168,8 @@ def long_sub_same_len( # noqa: C901
ret[i] = a[i] - b[i]
continue
if i == 0:
raise ValueError("Cannot go negative")
msg = "Cannot go negative"
raise ValueError(msg)
do_break = False
# look for a digit to the left to borrow from
for j in reversed(range(i)):
Expand All @@ -189,18 +189,20 @@ def long_sub_same_len( # noqa: C901
break
if do_break:
continue
raise ValueError("Failed to find digit to borrow from")
msg = "Failed to find digit to borrow from"
raise ValueError(msg)
if remainder:
# result, remainder
return ret[:-1], ret[-1]
return ret, 0


def long_add_same_len(
a: List[int], b: List[int], base: int, remainder: int, denominator: int
) -> Tuple[List[int], bool, int, int]:
a: list[int], b: list[int], base: int, remainder: int, denominator: int
) -> tuple[list[int], bool, int, int]:
if len(a) != len(b):
raise ValueError("a and b should have same length")
msg = "a and b should have same length"
raise ValueError(msg)

carry = remainder >= denominator
res = b.copy()
Expand All @@ -215,22 +217,23 @@ def long_add_same_len(
return res, carry, remainder, denominator


def right_pad(arr: List[int], to_length: int, val: int = 0) -> List[int]:
def right_pad(arr: list[int], to_length: int, val: int = 0) -> list[int]:
pad_len = to_length - len(arr)
if pad_len > 0:
return arr + [val] * pad_len
return arr


def long_linspace(
a: List[int], b: List[int], base: int, n: int, m: int
) -> List[Tuple[List[int], int, int]]:
a: list[int], b: list[int], base: int, n: int, m: int
) -> list[tuple[list[int], int, int]]:
if len(a) < len(b):
a = right_pad(a, len(b))
elif len(b) < len(a):
b = right_pad(b, len(a))
if a == b:
raise ValueError("Start and end strings are lexicographically inseperable")
msg = "Start and end strings are lexicographically inseparable"
raise ValueError(msg)
a_div, a_div_rem = long_div(a, m, base)
b_div, b_div_rem = long_div(b, m, base)

Expand All @@ -252,21 +255,21 @@ def long_linspace(
return ret


def left_pad(arr: List[int], to_length: int, val: int = 0) -> List[int]:
def left_pad(arr: list[int], to_length: int, val: int = 0) -> list[int]:
pad_len = to_length - len(arr)
if pad_len > 0:
return [val] * pad_len + arr
return arr


def chop_digits(rock: List[int], water: List[int]) -> List[int]:
def chop_digits(rock: list[int], water: list[int]) -> list[int]:
for i in range(len(water)):
if water[i] and (i >= len(rock) or rock[i] != water[i]):
return water[: i + 1]
return water


def lexicographic_less_than_array(a: List[int], b: List[int]) -> bool:
def lexicographic_less_than_array(a: list[int], b: list[int]) -> bool:
n = min(len(a), len(b))
for i in range(n):
if a[i] == b[i]:
Expand All @@ -275,7 +278,7 @@ def lexicographic_less_than_array(a: List[int], b: List[int]) -> bool:
return len(a) < len(b)


def chop_successive_digits(strings: List[List[int]]) -> List[List[int]]:
def chop_successive_digits(strings: list[list[int]]) -> list[list[int]]:
reversed_ = not lexicographic_less_than_array(strings[0], strings[1])
if reversed_:
strings.reverse()
Expand All @@ -300,6 +303,7 @@ def chop_successive_digits(strings: List[List[int]]) -> List[List[int]]:
digits + alpha_lower + alpha_upper,
# 0-9, then 10-35 repeating (for upper and lower case)
chain(range(10), cycle(map(partial(add, 10), range(26)))),
strict=False,
)
),
)
Expand Down
Empty file added mudder/py.typed
Empty file.
6 changes: 0 additions & 6 deletions mypy.ini

This file was deleted.

Loading

0 comments on commit 815a20f

Please sign in to comment.