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