|
| 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