from __future__ import annotations
from typing import TYPE_CHECKING
import contextlib
from dataclasses import dataclass
try:
# python 3.12+
from typing import override # noqa # type: ignore
except ImportError:
from typing_extensions import override # noqa # type: ignore
from ooodev.utils.data_type.base_int_value import BaseIntValue
if TYPE_CHECKING:
from ooodev.units.angle_t import AngleT
def _to_positive_angle(angle: int) -> int:
if not isinstance(angle, int):
raise TypeError(f"Expected type int for angle. Got {type(angle).__name__}")
# coverts all ints, negative or positive into a positive angle.
# eg: -10 becomes 350, 380 becomes 20
return (3600000 + angle) % 360
[docs]@dataclass(unsafe_hash=True)
class Angle(BaseIntValue):
"""
Represents an angle value from ``0`` to ``359``.
All input integers are converted into a positive angle.
Example:
.. code-block::
>>> print(Angle(360))
Angle(Value=0)
>>> print(Angle(413))
Angle(Value=53)
>>> print(Angle(-235))
Angle(Value=125)
>>> print(Angle(1794))
Angle(Value=354)
.. versionchanged:: 0.8.1
Now will accept any integer value, negative or positive.
"""
def __post_init__(self) -> None:
self.value = _to_positive_angle(self.value)
def _from_int(self, value: int) -> Angle:
return Angle(_to_positive_angle(value))
# region math and comparison
@override
def __eq__(self, other: object) -> bool:
# for some reason BaseIntValue __eq__ is not picked up.
# I suspect this is due to this class being a dataclass.
if isinstance(other, Angle):
return self.value == other.value
with contextlib.suppress(AttributeError):
return self.get_angle100() == other.get_angle100() # type: ignore
if isinstance(other, int):
return self.value == other
return False
@override
def __add__(self, other: object) -> Angle:
if hasattr(other, "get_angle"):
oth_val = other.get_angle() # type: ignore
return self.from_angle(self.value + oth_val) # type: ignore
if isinstance(other, (int, float)):
return self.from_angle(self.value + int(other)) # type: ignore
return NotImplemented
@override
def __radd__(self, other: object) -> Angle:
return self if other == 0 else self.__add__(other)
@override
def __sub__(self, other: object) -> Angle:
if hasattr(other, "get_angle"):
oth_val = other.get_angle() # type: ignore
return self.from_angle(self.value - oth_val) # type: ignore
if isinstance(other, (int, float)):
return self.from_angle(self.value - int(other)) # type: ignore
return NotImplemented
@override
def __rsub__(self, other: object) -> Angle:
if isinstance(other, (int, float)):
self_val = self.get_angle()
return self.from_angle(int(other) - self_val) # type: ignore
return NotImplemented
@override
def __mul__(self, other: object) -> Angle:
if hasattr(other, "get_angle"):
oth_val = other.get_angle() # type: ignore
return self.from_angle(self.value * oth_val) # type: ignore
if isinstance(other, (int, float)):
return self.from_angle(self.value * int(other)) # type: ignore
return NotImplemented
@override
def __rmul__(self, other: int) -> Angle:
return self if other == 0 else self.__mul__(other)
@override
def __truediv__(self, other: object) -> Angle:
if hasattr(other, "get_angle"):
oth_val = other.get_angle() # type: ignore
if oth_val == 0:
raise ZeroDivisionError
return self.from_angle(self.value // oth_val) # type: ignore
if isinstance(other, (int, float)):
if other == 0:
raise ZeroDivisionError
return self.from_angle(self.value // other) # type: ignore
return NotImplemented
@override
def __rtruediv__(self, other: object) -> Angle:
if isinstance(other, (int, float)):
if self.value == 0:
raise ZeroDivisionError
return self.from_angle(other // self.value) # type: ignore
return NotImplemented
@override
def __abs__(self) -> int:
return abs(self.value)
@override
def __lt__(self, other: object) -> bool:
if hasattr(other, "get_angle"):
return self.value < other.get_angle() # type: ignore
with contextlib.suppress(Exception):
return self.value < int(other) # type: ignore
return False
@override
def __le__(self, other: object) -> bool:
if hasattr(other, "get_angle"):
return self.value <= other.get_angle() # type: ignore
with contextlib.suppress(Exception):
return self.value <= int(other) # type: ignore
return False
@override
def __gt__(self, other: object) -> bool:
if hasattr(other, "get_angle"):
return self.value > other.get_angle() # type: ignore
with contextlib.suppress(Exception):
return self.value > int(other) # type: ignore
return False
@override
def __ge__(self, other: object) -> bool:
if hasattr(other, "get_angle"):
return self.value >= other.get_angle() # type: ignore
with contextlib.suppress(Exception):
return self.value >= int(other) # type: ignore
return False
# endregion math and comparison
[docs] def get_angle(self) -> int:
"""
Gets Angle Value as ``degrees``
.. versionadded:: 0.17.4
"""
return self.value
[docs] def get_angle10(self) -> int:
"""Gets Angle Value as ``1/10 degree``"""
return self.value * 10
[docs] def get_angle100(self) -> int:
"""Gets Angle Value as ``1/100 degree``"""
return self.value * 100
[docs] @staticmethod
def from_angle(value: int) -> Angle:
"""
Get an angle from ``degree`` units.
Args:
value (int): Angle in ``degree`` units.
Returns:
Angle:
"""
return Angle(0) if value == 0 else Angle(value)
[docs] @staticmethod
def from_angle10(value: int) -> Angle:
"""
Get an angle from ``1/10 degree`` units.
Args:
value (int): Angle in ``1/10 degree`` units.
Returns:
Angle:
"""
return Angle(0) if value == 0 else Angle(round(value / 10))
[docs] @staticmethod
def from_angle100(value: int) -> Angle:
"""
Get an angle from ``1/100 degree`` units.
Args:
value (int): Angle in ``1/10 degree`` units.
Returns:
Angle:
"""
return Angle(0) if value == 0 else Angle(round(value / 100))
[docs] @classmethod
def from_unit_val(cls, value: AngleT | int) -> Angle:
"""
Get instance from ``Angle`` or int value.
Args:
value (Angle, int): ``Angle`` or int value. If int then it is assumed to be in degrees.
Returns:
Angle:
.. versionadded:: 0.32.0
"""
try:
unit = value.get_angle() # type: ignore
return cls.from_angle(unit)
except AttributeError:
return cls.from_angle(int(value)) # type: ignore