mirror of
https://github.com/TheAlgorithms/Python.git
synced 2024-11-30 16:31:08 +00:00
260 lines
6.9 KiB
Python
260 lines
6.9 KiB
Python
|
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()
|