Python/geometry/geometry.py

260 lines
6.9 KiB
Python
Raw Permalink Normal View History

from __future__ import annotations
import math
from dataclasses import dataclass, field
from types import NoneType
from typing import Self
# Building block classes
@dataclass
class Angle:
"""
An Angle in degrees (unit of measurement)
>>> Angle()
Angle(degrees=90)
>>> Angle(45.5)
Angle(degrees=45.5)
>>> Angle(-1)
Traceback (most recent call last):
...
TypeError: degrees must be a numeric value between 0 and 360.
>>> Angle(361)
Traceback (most recent call last):
...
TypeError: degrees must be a numeric value between 0 and 360.
"""
degrees: float = 90
def __post_init__(self) -> None:
if not isinstance(self.degrees, (int, float)) or not 0 <= self.degrees <= 360:
raise TypeError("degrees must be a numeric value between 0 and 360.")
@dataclass
class Side:
"""
A side of a two dimensional Shape such as Polygon, etc.
adjacent_sides: a list of sides which are adjacent to the current side
angle: the angle in degrees between each adjacent side
length: the length of the current side in meters
>>> Side(5)
Side(length=5, angle=Angle(degrees=90), next_side=None)
>>> Side(5, Angle(45.6))
Side(length=5, angle=Angle(degrees=45.6), next_side=None)
>>> Side(5, Angle(45.6), Side(1, Angle(2))) # doctest: +ELLIPSIS
Side(length=5, angle=Angle(degrees=45.6), next_side=Side(length=1, angle=Angle(d...
"""
length: float
angle: Angle = field(default_factory=Angle)
next_side: Side | None = None
def __post_init__(self) -> None:
if not isinstance(self.length, (int, float)) or self.length <= 0:
raise TypeError("length must be a positive numeric value.")
if not isinstance(self.angle, Angle):
raise TypeError("angle must be an Angle object.")
if not isinstance(self.next_side, (Side, NoneType)):
raise TypeError("next_side must be a Side or None.")
@dataclass
class Ellipse:
"""
A geometric Ellipse on a 2D surface
>>> Ellipse(5, 10)
Ellipse(major_radius=5, minor_radius=10)
>>> Ellipse(5, 10) is Ellipse(5, 10)
False
>>> Ellipse(5, 10) == Ellipse(5, 10)
True
"""
major_radius: float
minor_radius: float
@property
def area(self) -> float:
"""
>>> Ellipse(5, 10).area
157.07963267948966
"""
return math.pi * self.major_radius * self.minor_radius
@property
def perimeter(self) -> float:
"""
>>> Ellipse(5, 10).perimeter
47.12388980384689
"""
return math.pi * (self.major_radius + self.minor_radius)
class Circle(Ellipse):
"""
A geometric Circle on a 2D surface
>>> Circle(5)
Circle(radius=5)
>>> Circle(5) is Circle(5)
False
>>> Circle(5) == Circle(5)
True
>>> Circle(5).area
78.53981633974483
>>> Circle(5).perimeter
31.41592653589793
"""
def __init__(self, radius: float) -> None:
super().__init__(radius, radius)
self.radius = radius
def __repr__(self) -> str:
return f"Circle(radius={self.radius})"
@property
def diameter(self) -> float:
"""
>>> Circle(5).diameter
10
"""
return self.radius * 2
def max_parts(self, num_cuts: float) -> float:
"""
Return the maximum number of parts that circle can be divided into if cut
'num_cuts' times.
>>> circle = Circle(5)
>>> circle.max_parts(0)
1.0
>>> circle.max_parts(7)
29.0
>>> circle.max_parts(54)
1486.0
>>> circle.max_parts(22.5)
265.375
>>> circle.max_parts(-222)
Traceback (most recent call last):
...
TypeError: num_cuts must be a positive numeric value.
>>> circle.max_parts("-222")
Traceback (most recent call last):
...
TypeError: num_cuts must be a positive numeric value.
"""
if not isinstance(num_cuts, (int, float)) or num_cuts < 0:
raise TypeError("num_cuts must be a positive numeric value.")
return (num_cuts + 2 + num_cuts**2) * 0.5
@dataclass
class Polygon:
"""
An abstract class which represents Polygon on a 2D surface.
>>> Polygon()
Polygon(sides=[])
"""
sides: list[Side] = field(default_factory=list)
def add_side(self, side: Side) -> Self:
"""
>>> Polygon().add_side(Side(5))
Polygon(sides=[Side(length=5, angle=Angle(degrees=90), next_side=None)])
"""
self.sides.append(side)
return self
def get_side(self, index: int) -> Side:
"""
>>> Polygon().get_side(0)
Traceback (most recent call last):
...
IndexError: list index out of range
>>> Polygon().add_side(Side(5)).get_side(-1)
Side(length=5, angle=Angle(degrees=90), next_side=None)
"""
return self.sides[index]
def set_side(self, index: int, side: Side) -> Self:
"""
>>> Polygon().set_side(0, Side(5))
Traceback (most recent call last):
...
IndexError: list assignment index out of range
>>> Polygon().add_side(Side(5)).set_side(0, Side(10))
Polygon(sides=[Side(length=10, angle=Angle(degrees=90), next_side=None)])
"""
self.sides[index] = side
return self
class Rectangle(Polygon):
"""
A geometric rectangle on a 2D surface.
>>> rectangle_one = Rectangle(5, 10)
>>> rectangle_one.perimeter()
30
>>> rectangle_one.area()
50
"""
def __init__(self, short_side_length: float, long_side_length: float) -> None:
super().__init__()
self.short_side_length = short_side_length
self.long_side_length = long_side_length
self.post_init()
def post_init(self) -> None:
"""
>>> Rectangle(5, 10) # doctest: +NORMALIZE_WHITESPACE
Rectangle(sides=[Side(length=5, angle=Angle(degrees=90), next_side=None),
Side(length=10, angle=Angle(degrees=90), next_side=None)])
"""
self.short_side = Side(self.short_side_length)
self.long_side = Side(self.long_side_length)
super().add_side(self.short_side)
super().add_side(self.long_side)
def perimeter(self) -> float:
return (self.short_side.length + self.long_side.length) * 2
def area(self) -> float:
return self.short_side.length * self.long_side.length
@dataclass
class Square(Rectangle):
"""
a structure which represents a
geometrical square on a 2D surface
>>> square_one = Square(5)
>>> square_one.perimeter()
20
>>> square_one.area()
25
"""
def __init__(self, side_length: float) -> None:
super().__init__(side_length, side_length)
def perimeter(self) -> float:
return super().perimeter()
def area(self) -> float:
return super().area()
if __name__ == "__main__":
__import__("doctest").testmod()