From c9812fc52f743cb8bbb4498f3fdbda3097ef3868 Mon Sep 17 00:00:00 2001 From: Natalie Fearnley <nfearnley@gmail.com> Date: Mon, 20 May 2024 16:40:29 -0400 Subject: [PATCH] changes --- discordplus/bot.py | 13 +- setup.cfg | 2 +- sizebot/cogs/admin.py | 2 +- sizebot/cogs/change.py | 126 ++++++---- sizebot/cogs/multiplayer.py | 8 +- sizebot/lib/changes.py | 2 +- sizebot/lib/digidecimal.py | 482 +++++++++++++++++++----------------- sizebot/lib/utils.py | 59 ++--- tests/test_infinite.py | 10 - 9 files changed, 361 insertions(+), 343 deletions(-) diff --git a/discordplus/bot.py b/discordplus/bot.py index 75699b45..ad39baec 100644 --- a/discordplus/bot.py +++ b/discordplus/bot.py @@ -1,5 +1,5 @@ -from typing import Any -from collections.abc import Iterable +from typing import TypeVar +from collections.abc import Iterator from copy import copy @@ -16,7 +16,10 @@ class BadMultilineCommand(commands.errors.CommandError): pass -def find_one(iterable: Iterable) -> Any: +T = TypeVar("T") + + +def find_one(iterable: Iterator[T]) -> T | None: try: return next(iterable) except StopIteration: @@ -33,12 +36,12 @@ async def process_commands(self: BotBase, message: discord.Message): # No command found, invoke will handle it if not ctx.command: - await self.invoke(ctx) + await self.invoke(ctx) # type: ignore return # One multiline command (command string starts with a multiline command) if ctx.command.multiline: - await self.invoke(ctx) + await self.invoke(ctx) # type: ignore return # Multiple commands (first command is not multiline) diff --git a/setup.cfg b/setup.cfg index ab25fa1d..9fbe2b29 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,7 +65,7 @@ console_scripts = sizebot-upgrade = sizebot.scripts.upgradeusers:main [flake8] -ignore = E501,W503,E241,E251,E266,ANN101,ANN102 +ignore = E501,W503,E241,E251,E266,ANN101,ANN102,ANN401,ANN002,ANN003 per-file-ignores = */__init__.py:F401,F403 sizebot/lib/proportions.py:E241,E272 diff --git a/sizebot/cogs/admin.py b/sizebot/cogs/admin.py index 14543170..46144116 100644 --- a/sizebot/cogs/admin.py +++ b/sizebot/cogs/admin.py @@ -29,7 +29,7 @@ async def halt(self, ctx: BotContext): hidden = True ) @commands.is_owner() - async def dump(self, ctx: BotContext, *, user: discord.Member = None): + async def dump(self, ctx: BotContext, *, user: discord.Member | None = None): """Dump a user's data.""" if user is None: user = ctx.author diff --git a/sizebot/cogs/change.py b/sizebot/cogs/change.py index 7fb671c8..08d5ef4b 100644 --- a/sizebot/cogs/change.py +++ b/sizebot/cogs/change.py @@ -1,7 +1,7 @@ import importlib.resources as pkg_resources import logging import random -from typing import cast +from typing import Literal, cast from sizebot.lib import errors, utils from sizebot.lib.digidecimal import Decimal @@ -12,6 +12,7 @@ from sizebot.lib import changes, userdb, nickmanager from sizebot.lib.diff import Diff, LimitedRate, Rate from sizebot.lib.errors import ChangeMethodInvalidException +from sizebot.lib.guilddb import Guild from sizebot.lib.objs import DigiObject, objects from sizebot.lib.types import BotContext, StrToSend from sizebot.lib.units import SV @@ -52,7 +53,7 @@ async def change(self, ctx: BotContext, *, arg: LimitedRate | Rate | Diff | str) `&change -1in/min until 1ft` `&change -1mm/sec for 1hr` """ - guildid = ctx.guild.id + guildid = cast(Guild, ctx.guild).id userid = ctx.author.id userdata = userdb.load(guildid, userid) # Load this data but don't use it as an ad-hoc user test. @@ -61,13 +62,13 @@ async def change(self, ctx: BotContext, *, arg: LimitedRate | Rate | Diff | str) amount = arg.amount if style == "add": - userdata.height = userdata.height + cast(SV, amount) + userdata.height = SV(userdata.height + cast(SV, amount)) elif style == "multiply": - userdata.height = userdata.height * cast(Decimal, amount) + userdata.height = SV(userdata.height * cast(Decimal, amount)) elif style == "power": userdata.scale = userdata.scale ** cast(Decimal, amount) else: - raise ChangeMethodInvalidException + raise ChangeMethodInvalidException(style) await nickmanager.nick_update(ctx.author) userdb.save(userdata) await ctx.send(f"{userdata.nickname} is now {userdata.height:m} ({userdata.height:u}) tall.") @@ -113,10 +114,10 @@ async def eatme(self, ctx: BotContext): guildid = ctx.guild.id userid = ctx.author.id - userdata = userdb.load(guildid, userid) - randmult = round(random.randint(2, 20), 1) - change_user(guildid, userid, "multiply", randmult) + randmult = Decimal(round(random.randint(2, 20), 1)) + change_user_mult(guildid, userid, randmult) await nickmanager.nick_update(ctx.author) + userdata = userdb.load(guildid, userid) lines = pkg_resources.read_text(sizebot.data, "eatme.txt").splitlines() @@ -138,10 +139,10 @@ async def drinkme(self, ctx: BotContext): guildid = ctx.guild.id userid = ctx.author.id - userdata = userdb.load(guildid, userid) - randmult = round(random.randint(2, 20), 1) - change_user(guildid, ctx.author.id, "divide", randmult) + randmult = Decimal(round(random.randint(2, 20), 1)) + change_user_div(guildid, userid, randmult) await nickmanager.nick_update(ctx.author) + userdata = userdb.load(guildid, userid) lines = pkg_resources.read_text(sizebot.data, "drinkme.txt").splitlines() @@ -231,59 +232,76 @@ async def changeTask(self): logger.error(utils.format_traceback(e)) -def change_user(guildid: int, userid: int, changestyle: str, amount: SV): - changestyle = changestyle.lower() - if changestyle in ["add", "+", "a", "plus"]: - changestyle = "add" - if changestyle in ["subtract", "sub", "-", "minus"]: - changestyle = "subtract" - if changestyle in ["power", "exp", "pow", "exponent", "^", "**"]: - changestyle = "power" - if changestyle in ["multiply", "mult", "m", "x", "times", "*"]: - changestyle = "multiply" - if changestyle in ["divide", "d", "/", "div"]: - changestyle = "divide" - if changestyle in ["percent", "per", "perc", "%"]: - changestyle = "percent" - - if changestyle not in ["add", "subtract", "multiply", "divide", "power", "percent"]: - raise errors.ChangeMethodInvalidException(changestyle) +ChangeStyles = Literal["add", "subtract", "multiply", "divide", "power", "percent"] - amountSV = None - amountVal = None - newamount = None - - if changestyle in ["add", "subtract"]: - amountSV = SV.parse(amount) - elif changestyle in ["multiply", "divide", "power"]: - amountVal = Decimal(amount) - if amountVal == 1: - raise errors.ValueIsOneException - if amountVal == 0: - raise errors.ValueIsZeroException - elif changestyle in ["percent"]: - amountVal = Decimal(amount) - if amountVal == 0: - raise errors.ValueIsZeroException - userdata = userdb.load(guildid, userid) +def change_user(guildid: int, userid: int, changestyle: str, amount: SV | Decimal): + mapper = utils.AliasMapper[ChangeStyles]({ + "add": ["add", "+", "a", "plus"], + "subtract": ["subtract", "sub", "-", "minus"], + "power": ["power", "exp", "pow", "exponent", "^", "**"], + "multiply": ["multiply", "mult", "m", "x", "times", "*"], + "divide": ["divide", "d", "/", "div"], + "percent": ["percent", "per", "perc", "%"], + }) + if changestyle not in mapper: + raise errors.ChangeMethodInvalidException(changestyle) + changestyle = mapper[changestyle] + + userdata = userdb.load(guildid, userid) if changestyle == "add": - newamount = userdata.height + amountSV + change_user_add(guildid, userid, cast(SV, amount)) elif changestyle == "subtract": - newamount = userdata.height - amountSV + change_user_sub(guildid, userid, cast(SV, amount)) elif changestyle == "multiply": - newamount = userdata.height * amountVal + change_user_mult(guildid, userid, cast(Decimal, amount)) elif changestyle == "divide": - newamount = userdata.height / amountVal + change_user_div(guildid, userid, cast(Decimal, amount)) elif changestyle == "power": - userdata = userdata ** amountVal - elif changestyle == "percent": - newamount = userdata.height * (amountVal / 100) + change_user_power(guildid, userid, cast(Decimal, amount)) + elif changestyle == "perc": + change_user_perc(guildid, userid, cast(Decimal, amount)) + + userdb.save(userdata) - if changestyle != "power": - userdata.height = newamount +def change_user_add(guildid: int, userid: int, amount: SV): + userdata = userdb.load(guildid, userid) + userdata.height = SV(userdata.height + amount) + userdb.save(userdata) + + +def change_user_sub(guildid: int, userid: int, amount: SV): + change_user_add(guildid, userid, SV(-amount)) + + +def change_user_mult(guildid: int, userid: int, amount: Decimal): + userdata = userdb.load(guildid, userid) + if amount == 1: + raise errors.ValueIsOneException + if amount == 0: + raise errors.ValueIsZeroException + userdata.height = SV(userdata.height * amount) + userdb.save(userdata) + + +def change_user_div(guildid: int, userid: int, amount: Decimal): + change_user_mult(guildid, userid, Decimal(1) / amount) + + +def change_user_power(guildid: int, userid: int, amount: Decimal): + userdata = userdb.load(guildid, userid) + userdata.scale = userdata.scale ** amount + userdb.save(userdata) + + +def change_user_perc(guildid: int, userid: int, amount: Decimal): + userdata = userdb.load(guildid, userid) + if amount == 0: + raise errors.ValueIsZeroException + amount_perc = amount / 100 + userdata.height = SV(userdata.height * amount_perc) userdb.save(userdata) diff --git a/sizebot/cogs/multiplayer.py b/sizebot/cogs/multiplayer.py index a4c754d3..7971f58d 100644 --- a/sizebot/cogs/multiplayer.py +++ b/sizebot/cogs/multiplayer.py @@ -36,11 +36,11 @@ async def pushbutton(self, ctx: BotContext, user: discord.Member): return diff = userdata.button if diff.changetype == "multiply": - userdata.height *= diff.amount + userdata.height = SV(userdata.height * diff.amount) elif diff.changetype == "add": - userdata.height += diff.amount + userdata.height = SV(userdata.height + diff.amount) elif diff.changetype == "power": - userdata = userdata ** diff.amount + userdata.scale = userdata.scale ** diff.amount userdb.save(userdata) await nickmanager.nick_update(user) await ctx.send(f"You pushed {userdata.nickname}'s button! They are now **{userdata.height:,.3mu}** tall.") @@ -164,7 +164,7 @@ async def changeother(self, ctx: BotContext, other: discord.Member, *, string: D elif style == "power": userdata = userdata ** amount else: - raise ChangeMethodInvalidException + raise ChangeMethodInvalidException(style) await nickmanager.nick_update(other) userdb.save(userdata) diff --git a/sizebot/lib/changes.py b/sizebot/lib/changes.py index 3b73db27..13f5abdf 100644 --- a/sizebot/lib/changes.py +++ b/sizebot/lib/changes.py @@ -116,7 +116,7 @@ def toJSON(self) -> Any: } -def start(userid: int, guildid: int, *, addPerSec: SV = 0, mulPerSec: Decimal = 1, stopSV: SV = None, stopTV: TV = None): +def start(userid: int, guildid: int, *, addPerSec: SV = 0, mulPerSec: Decimal = 1, stopSV: SV | None = None, stopTV: TV | None = None): """Start a new change task""" startTime = lastRan = time.time() change = Change(userid, guildid, addPerSec=addPerSec, mulPerSec=mulPerSec, stopSV=stopSV, stopTV=stopTV, startTime=startTime, lastRan=lastRan) diff --git a/sizebot/lib/digidecimal.py b/sizebot/lib/digidecimal.py index ee10b74d..6004d7a9 100644 --- a/sizebot/lib/digidecimal.py +++ b/sizebot/lib/digidecimal.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Any +from typing import Any, TypeVar, overload from collections.abc import Callable import decimal @@ -36,44 +36,57 @@ def minmax(first: Decimal, second: Decimal) -> tuple[Decimal, Decimal]: # get the values for magic methods, instead of the objects def values(fn: Callable) -> Callable: def wrapped(*args) -> Any: - valargs = [unwrapDecimal(a) for a in args] + valargs = [unwrap_decimal(a) for a in args] return fn(*valargs) return wrapped -# TODO: CamelCase -def clampInf(value: RawDecimal, limit: RawDecimal) -> RawDecimal: - if limit and abs(value) >= limit: +def _clamp_inf(value: RawDecimal) -> RawDecimal: + if abs(value) >= _max_num: value = RawDecimal("infinity") * int(math.copysign(1, value)) return value -# TODO: CamelCase -def unwrapDecimal(value: Decimal) -> RawDecimal: +T = TypeVar("T", bound=RawDecimal | int | float | str) + + +@overload +def unwrap_decimal(value: Decimal) -> RawDecimal: + ... + + +@overload +def unwrap_decimal(value: T) -> T: + ... + + +def unwrap_decimal(value: Decimal | T) -> RawDecimal | T: if isinstance(value, Decimal): - value = value._rawvalue + return value.to_pydecimal() return value +_max_num = RawDecimal("1e1000") + + @total_ordering class Decimal(): infinity = RawDecimal("infinity") - _infinity = RawDecimal("1e1000") - def __init__(self, value: Decimal): + def __init__(self, value: Decimal | RawDecimal | int | float | str): # initialize from Decimal - rawvalue = unwrapDecimal(value) - if isinstance(rawvalue, str): - if rawvalue == "∞": - rawvalue = "infinity" - elif rawvalue == "-∞": - rawvalue = "-infinity" - # initialize from fraction string - values = rawvalue.split("/") - if len(values) == 2: - numberator, denominator = values - rawvalue = unwrapDecimal(Decimal(numberator) / Decimal(denominator)) - self._rawvalue = clampInf(RawDecimal(rawvalue), unwrapDecimal(self._infinity)) + if isinstance(value, Decimal): + rawvalue = value.to_pydecimal() + elif isinstance(value, str): + rawvalue = _parse_rawdecimal(value) + elif isinstance(value, RawDecimal): + rawvalue = value + else: + rawvalue = RawDecimal(value) + self._rawvalue = _clamp_inf(rawvalue) + + def to_pydecimal(self) -> RawDecimal: + return self._rawvalue def __format__(self, spec: str) -> str: if self.is_infinite(): @@ -112,7 +125,7 @@ def __format__(self, spec: str) -> str: numspec = str(dSpec) if fractional: whole = rounded.to_integral_value(ROUND_DOWN) - rawwhole = fix_zeroes(whole._rawvalue) + rawwhole = _fix_zeroes(whole.to_pydecimal()) formatted = format(rawwhole, numspec) part = abs(whole - rounded) fraction = format_fraction(part) @@ -121,7 +134,7 @@ def __format__(self, spec: str) -> str: formatted = "" formatted += fraction else: - rawvalue = fix_zeroes(rounded._rawvalue) + rawvalue = _fix_zeroes(rounded.to_pydecimal()) formatted = format(rawvalue, numspec) if dSpec.type == "f": @@ -137,231 +150,216 @@ def __format__(self, spec: str) -> str: return formatted def __str__(self) -> str: - return str(self._rawvalue) + return str(self.to_pydecimal()) def __repr__(self) -> str: return f"Decimal('{self}')" - @values - def __bool__(value) -> bool: - return bool(value) + def __bool__(self) -> bool: + rawself = unwrap_decimal(self) + return bool(rawself) - @values - def __hash__(value) -> int: - return hash(value) + def __hash__(self) -> int: + rawself = unwrap_decimal(self) + return hash(rawself) # Math Methods - @values - def __eq__(value, other: Any) -> bool: - return value == other - - @values - def __lt__(value, other: Any) -> bool: - return value < other - - @values - def __add__(value, other: Any) -> Decimal: - return Decimal(value + other) - - def __radd__(self, other: Any) -> Decimal: - return Decimal.__add__(other, self) - - @values - def __sub__(value, other: Any) -> Decimal: - return Decimal(value - other) - - def __rsub__(self, other: Any) -> Decimal: - return Decimal.__sub__(other, self) - - @values - def __mul__(value, other: Any) -> Decimal: - return Decimal(value * other) - - def __rmul__(self, other: Any) -> Decimal: - return Decimal.__mul__(other, self) - - @values - def __matmul__(value, other: Any) -> Decimal: - return Decimal(value @ other) - - def __rmatmul__(self, other: Any) -> Decimal: - return Decimal.__matmul__(other, self) - - @values - def __truediv__(value, other: Any) -> Decimal: - if isinstance(value, RawDecimal) and value.is_infinite() and isinstance(other, RawDecimal) and other.is_infinite(): + def __eq__(self, other: Decimal) -> bool: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + return rawself == rawother + + def __lt__(self, other: Decimal) -> bool: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + return rawself < rawother + + def __add__(self, other: Decimal) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + return Decimal(rawself + rawother) + + def __radd__(self, other: Decimal) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + return Decimal(rawother + rawself) + + def __sub__(self, other: Decimal) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + return Decimal(rawself - rawother) + + def __rsub__(self, other: Decimal) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + return Decimal(rawother - rawself) + + def __mul__(self, other: Decimal | int) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + return Decimal(rawself * rawother) + + def __rmul__(self, other: Decimal | int) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + return Decimal(rawother * rawself) + + def __truediv__(self, other: Decimal | int) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + if rawself.is_infinite() and _is_infinite(rawother): raise decimal.InvalidOperation - elif isinstance(value, RawDecimal) and value.is_infinite(): - return Decimal(value) - elif isinstance(other, RawDecimal) and other.is_infinite(): + elif rawself.is_infinite(): + return Decimal(rawself) + elif _is_infinite(rawother): return Decimal(0) - return Decimal(value / other) - - def __rtruediv__(self, other: Any) -> Decimal: - return Decimal.__truediv__(other, self) + return Decimal(rawself / rawother) - @values - def __floordiv__(value, other: Any) -> Decimal: - if isinstance(value, RawDecimal) and value.is_infinite() and isinstance(other, RawDecimal) and other.is_infinite(): + def __rtruediv__(self, other: Decimal) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + if rawself.is_infinite() and rawother.is_infinite(): raise decimal.InvalidOperation - elif isinstance(value, RawDecimal) and value.is_infinite(): - return Decimal(value) - elif isinstance(other, RawDecimal) and other.is_infinite(): + elif rawself.is_infinite(): return Decimal(0) - return Decimal(value // other) - - def __rfloordiv__(self, other: Any) -> Decimal: - return Decimal.__floordiv__(other, self) - - @values - def __mod__(value, other: Any) -> Decimal: - if isinstance(value, RawDecimal) and value.is_infinite(): + elif rawother.is_infinite(): + return Decimal(rawself) + return Decimal(rawother / rawself) + + def __floordiv__(self, other: Decimal | int) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + if rawself.is_infinite() and _is_infinite(rawother): + raise decimal.InvalidOperation + elif rawself.is_infinite(): + return Decimal(rawself) + elif _is_infinite(rawother): return Decimal(0) - elif isinstance(other, RawDecimal) and other.is_infinite(): - return Decimal(value) - return Decimal(value % other) + return Decimal(rawself // rawother) - def __rmod__(self, other: Any) -> Decimal: - return Decimal.__mod__(other, self) + def __rfloordiv__(self, other: Decimal | int) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + if rawself.is_infinite() and _is_infinite(rawother): + raise decimal.InvalidOperation + elif rawself.is_infinite(): + return Decimal(0) + elif _is_infinite(rawother): + return Decimal(rawself) + return Decimal(rawother // rawself) + + def __mod__(self, other: Decimal | int) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + if rawself.is_infinite(): + return Decimal(0) + elif _is_infinite(rawother): + return Decimal(rawself) + return Decimal(rawself % rawother) + + def __rmod__(self, other: Decimal | int) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + if rawself.is_infinite(): + return Decimal(rawself) + elif _is_infinite(rawother): + return Decimal(0) + return Decimal(rawother % rawself) - def __divmod__(self, other: Any) -> tuple[Decimal, Decimal]: + def __divmod__(self, other: Decimal | int) -> tuple[Decimal, Decimal]: quotient = Decimal.__floordiv__(self, other) remainder = Decimal.__mod__(self, other) - return Decimal(quotient), Decimal(remainder) - - def __rdivmod__(self, other: Any) -> tuple[Decimal, Decimal]: - return Decimal.__divmod__(other, self) - - @values - def __pow__(value, other: Any) -> Decimal: - return Decimal(value ** other) - - @values - def __rpow__(value, other: Any) -> Decimal: - return Decimal(other ** value) - - @values - def __lshift__(value, other: Any) -> Decimal: - return Decimal.__mul__(value, Decimal.__pow__(Decimal(2), other)) - - def __rlshift__(self, other: Any) -> Decimal: - return Decimal.__lshift__(other, self) - - @values - def __rshift__(value, other: Any) -> Decimal: - return Decimal.__floordiv__(value, Decimal.__pow__(Decimal(2), other)) - - def __rrshift__(self, other: Any) -> Decimal: - return Decimal.__rshift__(other, self) - - @values - def __and__(value, other: Any) -> Decimal: - if (isinstance(value, RawDecimal) and value.is_infinite()): - return other - elif (isinstance(other, RawDecimal) and other.is_infinite()): - return value - return Decimal(value & other) - - def __rand__(self, other: Any) -> Decimal: - return Decimal.__and__(other, self) - - @values - def __xor__(value, other: Any) -> Decimal: - if isinstance(value, RawDecimal) and value.is_infinite(): - return Decimal.__invert__(other) - elif isinstance(other, RawDecimal) and other.is_infinite(): - return Decimal.__invert__(value) - return Decimal(value ^ other) - - def __rxor__(self, other: Any) -> Decimal: - return Decimal.__xor__(other, self) - - @values - def __or__(value, other: Any) -> Decimal: - if (isinstance(value, RawDecimal) and value.is_infinite()) or (isinstance(other, RawDecimal) and other.is_infinite()): - return Decimal("infinity") - return Decimal(value | other) - - def __ror__(self, other: Any) -> Decimal: - return Decimal.__or__(other, self) - - @values - def __neg__(value) -> Decimal: - return Decimal(-value) - - @values - def __pos__(value) -> Decimal: - return Decimal(+value) - - @values - def __abs__(value) -> Decimal: - return Decimal(abs(value)) - - @values - def __invert__(value) -> Decimal: - if isinstance(value, RawDecimal) and value.is_infinite(): - return -value - return Decimal(~value) - - @values - def __complex__(value) -> complex: - return complex(value) - - @values - def __int__(value) -> int: - return int(value) - - @values - def __float__(value) -> float: - return float(value) - - def __round__(value, ndigits: int = 0) -> Decimal: - if value.is_infinite(): - return value - exp = Decimal(10) ** -ndigits - return value.quantize(exp) - - @values - def __trunc__(value) -> Decimal: - if isinstance(value, RawDecimal) and value.is_infinite(): - return value - return Decimal(math.trunc(value)) - - @values - def __floor__(value) -> Decimal: - if isinstance(value, RawDecimal) and value.is_infinite(): - return value - return Decimal(math.floor(value)) - - @values - def __ceil__(value) -> Decimal: - if isinstance(value, RawDecimal) and value.is_infinite(): - return value - return Decimal(math.ceil(value)) - - @values - def quantize(value, exp: Decimal) -> Decimal: - return Decimal(value.quantize(exp)) - - @values - def is_infinite(value) -> bool: - return value.is_infinite() - - @values - def is_signed(value) -> bool: - return value.is_signed() + return quotient, remainder + + def __rdivmod__(self, other: Decimal | int) -> tuple[Decimal, Decimal]: + quotient = Decimal.__rfloordiv__(self, other) + remainder = Decimal.__rmod__(self, other) + return quotient, remainder + + def __pow__(self, other: Decimal | int) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + return Decimal(rawself ** rawother) + + def __rpow__(self, other: Decimal | int) -> Decimal: + rawself = unwrap_decimal(self) + rawother = unwrap_decimal(other) + return Decimal(rawother ** rawself) + + def __neg__(self) -> Decimal: + rawself = unwrap_decimal(self) + return Decimal(-rawself) + + def __pos__(self) -> Decimal: + rawself = unwrap_decimal(self) + return Decimal(+rawself) + + def __abs__(self) -> Decimal: + rawself = unwrap_decimal(self) + return Decimal(abs(rawself)) + + def __complex__(self) -> complex: + rawself = unwrap_decimal(self) + return complex(rawself) + + def __int__(self) -> int: + rawself = unwrap_decimal(self) + return int(rawself) + + def __float__(self) -> float: + rawself = unwrap_decimal(self) + return float(rawself) + + def __round__(self, ndigits: int = 0) -> Decimal: + rawself = unwrap_decimal(self) + if rawself.is_infinite(): + return Decimal(rawself) + exp = RawDecimal(10) ** -ndigits + return Decimal(rawself.quantize(exp)) + + def __trunc__(self) -> Decimal: + rawself = unwrap_decimal(self) + if rawself.is_infinite(): + return Decimal(rawself) + return Decimal(math.trunc(rawself)) + + def __floor__(self) -> Decimal: + rawself = unwrap_decimal(self) + if rawself.is_infinite(): + return Decimal(rawself) + return Decimal(math.floor(rawself)) + + def __ceil__(self) -> Decimal: + rawself = unwrap_decimal(self) + if rawself.is_infinite(): + return Decimal(rawself) + return Decimal(math.ceil(rawself)) + + def quantize(self, exp: Decimal | RawDecimal | int) -> Decimal: + rawself = unwrap_decimal(self) + rawexp = unwrap_decimal(exp) + return Decimal(rawself.quantize(rawexp)) + + def is_infinite(self) -> bool: + rawself = unwrap_decimal(self) + return rawself.is_infinite() + + def is_signed(self) -> bool: + rawself = unwrap_decimal(self) + return rawself.is_signed() @property - @values - def sign(value) -> str: - return "-" if value.is_signed() else "" + def sign(self) -> str: + rawself = unwrap_decimal(self) + return "-" if rawself.is_signed() else "" - def to_integral_value(self, *args, **kwargs) -> Decimal: - return Decimal(self._rawvalue.to_integral_value(*args, **kwargs)) + def to_integral_value(self, rounding: str | None = None) -> Decimal: + rawself = unwrap_decimal(self) + return Decimal(rawself.to_integral_value(rounding)) - @values - def log10(value) -> Decimal: - return Decimal(value.log10()) + def log10(self) -> Decimal: + rawself = unwrap_decimal(self) + return Decimal(rawself.log10()) class DecimalSpec: @@ -433,12 +431,12 @@ def round_decimal(d: Decimal, accuracy: int = 0) -> Decimal: return d.quantize(places) -def round_fraction(number: Decimal, denominator: Decimal) -> Decimal: +def round_fraction(number: Decimal, denominator: int) -> Decimal: rounded = round(number * denominator) / denominator return rounded -def format_fraction(value: Decimal) -> str: +def format_fraction(value: Decimal) -> str | None: if value is None: return None fractionStrings = ["", "⅛", "¼", "⅜", "½", "⅝", "¾", "⅞"] @@ -450,14 +448,14 @@ def format_fraction(value: Decimal) -> str: logger.error("Weird fraction IndexError:\n" f"fractionStrings = {fractionStrings!r}\n" f"len(fractionStrings) = {len(fractionStrings)!r}\n" - f"value = {value._rawvalue}\n" + f"value = {value.to_pydecimal()}\n" f"part = {part!r}\n" f"int(part * len(fractionStrings)) = {int(part * len(fractionStrings))}") raise e return fraction -def fix_zeroes(d: Decimal) -> Decimal: +def _fix_zeroes(d: RawDecimal) -> RawDecimal: """Reset the precision of a Decimal to avoid values that use exponents like '1e3' and values with trailing zeroes like '100.000' fixZeroes(Decimal('1e3')) -> Decimal('100') @@ -470,3 +468,23 @@ def fix_zeroes(d: Decimal) -> Decimal: Decimal('1e3') -> Decimal('100') """ return d.normalize() + 0 + + +def _parse_rawdecimal(s: str) -> RawDecimal: + if s == "∞": + s = "infinity" + elif s == "-∞": + s = "-infinity" + # initialize from fraction string + parts = s.split("/") + if len(parts) == 2: + numberator, denominator = parts + value = Decimal(numberator) / Decimal(denominator) + value.to_pydecimal() + return RawDecimal(s) + + +def _is_infinite(d: RawDecimal | int) -> bool: + if isinstance(d, RawDecimal): + return d.is_infinite() + return False diff --git a/sizebot/lib/utils.py b/sizebot/lib/utils.py index 5d2878d6..b0c5a0e4 100644 --- a/sizebot/lib/utils.py +++ b/sizebot/lib/utils.py @@ -1,4 +1,4 @@ -from typing import Any, Generator, Hashable, Sequence, TypeVar +from typing import Any, Generator, Generic, Hashable, Literal, Sequence, TypeVar from collections.abc import Callable, Iterable, Iterator import inspect @@ -421,40 +421,29 @@ def truncate(s: str, amount: int) -> str: return s -class AliasMap(dict): - def __init__(self, data: dict[Hashable, Sequence]): - super().__init__() - - for k, v in data.items(): - self[k] = v - - def __setitem__(self, k: Hashable, v: Sequence): - if not isinstance(k, Hashable): - raise ValueError("{k!r} is not hashable and can't be used as a key.") - if not isinstance(v, Sequence): - raise ValueError("{v!r} is not a sequence and can't be used as a value.") - if isinstance(v, str): - v = [v] - for i in v: - super().__setitem__(i, k) - super().__setitem__(k, k) - - def __str__(self) -> str: - swapped = {} - for v in self.values(): - swapped[v] = [] - for k, v in self.items(): - swapped[v].append(k) - - aliasstrings = [] - for k, v in swapped.items(): - s = k - for vv in v: - if vv != k: - s += f"/{vv}" - aliasstrings.append(s) - - return sentence_join(aliasstrings, oxford = True) +T = TypeVar("T", bound=str) +AliasList = dict[T, list[str]] +AliasMap = dict[str, T] + + +class AliasMapper(Generic[T]): + def __init__(self, aliases: AliasList[T]): + self._map = map_aliases(aliases) + + def __getitem__(self, key: str) -> T: + return self._map[key.lower()] + + def __contains__(self, key: str) -> bool: + return key.lower() in self._map + + +def map_aliases[T: str](alias_dict: AliasList[T]) -> AliasMap[T]: + aliasmap: AliasMap[T] = {} + for key, aliases in alias_dict: + aliasmap[key.lower()] = key.lower() + for alias in aliases: + aliasmap[alias.lower()] = key.lower() + return aliasmap RE_SCI_EXP = re.compile(r"(\d+\.?\d*)(\*\*|\^|[Ee][\+\-]?)(\d+\.?\d*)") diff --git a/tests/test_infinite.py b/tests/test_infinite.py index b69798ef..a1e5074f 100644 --- a/tests/test_infinite.py +++ b/tests/test_infinite.py @@ -295,16 +295,6 @@ def test_neg__abs__(): assert result == posinf -def test_pos__invert__(): - result = ~posinf - assert result == neginf - - -def test_neg__invert__(): - result = ~neginf - assert result == posinf - - def test_pos__round__(): result = round(posinf) assert result == posinf