Skip to content

Commit 3999abf

Browse files
meg-1cclausspre-commit-ci[bot]
authored
adding a geometry module (#11138)
* adding a geometry module * fixing errors and adding type hints * Create code_review_feedback.py * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * implementing suggestions * fixing ruff errors * Update geometry/code_review_feedback.py * Update geometry/code_review_feedback.py * Update geometry/geometry.py * Apply suggestions from code review * Delete geometry/code_review_feedback.py * Update geometry/geometry.py * Update geometry/geometry.py --------- Co-authored-by: Christian Clauss <cclauss@me.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 5f61af4 commit 3999abf

File tree

1 file changed

+259
-0
lines changed

1 file changed

+259
-0
lines changed

geometry/geometry.py

+259
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
from __future__ import annotations
2+
3+
import math
4+
from dataclasses import dataclass, field
5+
from types import NoneType
6+
from typing import Self
7+
8+
# Building block classes
9+
10+
11+
@dataclass
12+
class Angle:
13+
"""
14+
An Angle in degrees (unit of measurement)
15+
16+
>>> Angle()
17+
Angle(degrees=90)
18+
>>> Angle(45.5)
19+
Angle(degrees=45.5)
20+
>>> Angle(-1)
21+
Traceback (most recent call last):
22+
...
23+
TypeError: degrees must be a numeric value between 0 and 360.
24+
>>> Angle(361)
25+
Traceback (most recent call last):
26+
...
27+
TypeError: degrees must be a numeric value between 0 and 360.
28+
"""
29+
30+
degrees: float = 90
31+
32+
def __post_init__(self) -> None:
33+
if not isinstance(self.degrees, (int, float)) or not 0 <= self.degrees <= 360:
34+
raise TypeError("degrees must be a numeric value between 0 and 360.")
35+
36+
37+
@dataclass
38+
class Side:
39+
"""
40+
A side of a two dimensional Shape such as Polygon, etc.
41+
adjacent_sides: a list of sides which are adjacent to the current side
42+
angle: the angle in degrees between each adjacent side
43+
length: the length of the current side in meters
44+
45+
>>> Side(5)
46+
Side(length=5, angle=Angle(degrees=90), next_side=None)
47+
>>> Side(5, Angle(45.6))
48+
Side(length=5, angle=Angle(degrees=45.6), next_side=None)
49+
>>> Side(5, Angle(45.6), Side(1, Angle(2))) # doctest: +ELLIPSIS
50+
Side(length=5, angle=Angle(degrees=45.6), next_side=Side(length=1, angle=Angle(d...
51+
"""
52+
53+
length: float
54+
angle: Angle = field(default_factory=Angle)
55+
next_side: Side | None = None
56+
57+
def __post_init__(self) -> None:
58+
if not isinstance(self.length, (int, float)) or self.length <= 0:
59+
raise TypeError("length must be a positive numeric value.")
60+
if not isinstance(self.angle, Angle):
61+
raise TypeError("angle must be an Angle object.")
62+
if not isinstance(self.next_side, (Side, NoneType)):
63+
raise TypeError("next_side must be a Side or None.")
64+
65+
66+
@dataclass
67+
class Ellipse:
68+
"""
69+
A geometric Ellipse on a 2D surface
70+
71+
>>> Ellipse(5, 10)
72+
Ellipse(major_radius=5, minor_radius=10)
73+
>>> Ellipse(5, 10) is Ellipse(5, 10)
74+
False
75+
>>> Ellipse(5, 10) == Ellipse(5, 10)
76+
True
77+
"""
78+
79+
major_radius: float
80+
minor_radius: float
81+
82+
@property
83+
def area(self) -> float:
84+
"""
85+
>>> Ellipse(5, 10).area
86+
157.07963267948966
87+
"""
88+
return math.pi * self.major_radius * self.minor_radius
89+
90+
@property
91+
def perimeter(self) -> float:
92+
"""
93+
>>> Ellipse(5, 10).perimeter
94+
47.12388980384689
95+
"""
96+
return math.pi * (self.major_radius + self.minor_radius)
97+
98+
99+
class Circle(Ellipse):
100+
"""
101+
A geometric Circle on a 2D surface
102+
103+
>>> Circle(5)
104+
Circle(radius=5)
105+
>>> Circle(5) is Circle(5)
106+
False
107+
>>> Circle(5) == Circle(5)
108+
True
109+
>>> Circle(5).area
110+
78.53981633974483
111+
>>> Circle(5).perimeter
112+
31.41592653589793
113+
"""
114+
115+
def __init__(self, radius: float) -> None:
116+
super().__init__(radius, radius)
117+
self.radius = radius
118+
119+
def __repr__(self) -> str:
120+
return f"Circle(radius={self.radius})"
121+
122+
@property
123+
def diameter(self) -> float:
124+
"""
125+
>>> Circle(5).diameter
126+
10
127+
"""
128+
return self.radius * 2
129+
130+
def max_parts(self, num_cuts: float) -> float:
131+
"""
132+
Return the maximum number of parts that circle can be divided into if cut
133+
'num_cuts' times.
134+
135+
>>> circle = Circle(5)
136+
>>> circle.max_parts(0)
137+
1.0
138+
>>> circle.max_parts(7)
139+
29.0
140+
>>> circle.max_parts(54)
141+
1486.0
142+
>>> circle.max_parts(22.5)
143+
265.375
144+
>>> circle.max_parts(-222)
145+
Traceback (most recent call last):
146+
...
147+
TypeError: num_cuts must be a positive numeric value.
148+
>>> circle.max_parts("-222")
149+
Traceback (most recent call last):
150+
...
151+
TypeError: num_cuts must be a positive numeric value.
152+
"""
153+
if not isinstance(num_cuts, (int, float)) or num_cuts < 0:
154+
raise TypeError("num_cuts must be a positive numeric value.")
155+
return (num_cuts + 2 + num_cuts**2) * 0.5
156+
157+
158+
@dataclass
159+
class Polygon:
160+
"""
161+
An abstract class which represents Polygon on a 2D surface.
162+
163+
>>> Polygon()
164+
Polygon(sides=[])
165+
"""
166+
167+
sides: list[Side] = field(default_factory=list)
168+
169+
def add_side(self, side: Side) -> Self:
170+
"""
171+
>>> Polygon().add_side(Side(5))
172+
Polygon(sides=[Side(length=5, angle=Angle(degrees=90), next_side=None)])
173+
"""
174+
self.sides.append(side)
175+
return self
176+
177+
def get_side(self, index: int) -> Side:
178+
"""
179+
>>> Polygon().get_side(0)
180+
Traceback (most recent call last):
181+
...
182+
IndexError: list index out of range
183+
>>> Polygon().add_side(Side(5)).get_side(-1)
184+
Side(length=5, angle=Angle(degrees=90), next_side=None)
185+
"""
186+
return self.sides[index]
187+
188+
def set_side(self, index: int, side: Side) -> Self:
189+
"""
190+
>>> Polygon().set_side(0, Side(5))
191+
Traceback (most recent call last):
192+
...
193+
IndexError: list assignment index out of range
194+
>>> Polygon().add_side(Side(5)).set_side(0, Side(10))
195+
Polygon(sides=[Side(length=10, angle=Angle(degrees=90), next_side=None)])
196+
"""
197+
self.sides[index] = side
198+
return self
199+
200+
201+
class Rectangle(Polygon):
202+
"""
203+
A geometric rectangle on a 2D surface.
204+
205+
>>> rectangle_one = Rectangle(5, 10)
206+
>>> rectangle_one.perimeter()
207+
30
208+
>>> rectangle_one.area()
209+
50
210+
"""
211+
212+
def __init__(self, short_side_length: float, long_side_length: float) -> None:
213+
super().__init__()
214+
self.short_side_length = short_side_length
215+
self.long_side_length = long_side_length
216+
self.post_init()
217+
218+
def post_init(self) -> None:
219+
"""
220+
>>> Rectangle(5, 10) # doctest: +NORMALIZE_WHITESPACE
221+
Rectangle(sides=[Side(length=5, angle=Angle(degrees=90), next_side=None),
222+
Side(length=10, angle=Angle(degrees=90), next_side=None)])
223+
"""
224+
self.short_side = Side(self.short_side_length)
225+
self.long_side = Side(self.long_side_length)
226+
super().add_side(self.short_side)
227+
super().add_side(self.long_side)
228+
229+
def perimeter(self) -> float:
230+
return (self.short_side.length + self.long_side.length) * 2
231+
232+
def area(self) -> float:
233+
return self.short_side.length * self.long_side.length
234+
235+
236+
@dataclass
237+
class Square(Rectangle):
238+
"""
239+
a structure which represents a
240+
geometrical square on a 2D surface
241+
>>> square_one = Square(5)
242+
>>> square_one.perimeter()
243+
20
244+
>>> square_one.area()
245+
25
246+
"""
247+
248+
def __init__(self, side_length: float) -> None:
249+
super().__init__(side_length, side_length)
250+
251+
def perimeter(self) -> float:
252+
return super().perimeter()
253+
254+
def area(self) -> float:
255+
return super().area()
256+
257+
258+
if __name__ == "__main__":
259+
__import__("doctest").testmod()

0 commit comments

Comments
 (0)