Skip to content

Commit 600617a

Browse files
authored
Merge branch 'master' into master
2 parents 3990e2c + 2922216 commit 600617a

5 files changed

Lines changed: 429 additions & 0 deletions

File tree

DIRECTORY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,7 @@
763763
* [Dodecahedron](maths/dodecahedron.py)
764764
* [Double Factorial](maths/double_factorial.py)
765765
* [Dual Number Automatic Differentiation](maths/dual_number_automatic_differentiation.py)
766+
* [Ear Clipping Polygon Triangulation](maths/ear_clipping_polygon_triangulation.py)
766767
* [Entropy](maths/entropy.py)
767768
* [Euclidean Distance](maths/euclidean_distance.py)
768769
* [Euler Method](maths/euler_method.py)
@@ -883,11 +884,14 @@
883884
* [Solovay Strassen Primality Test](maths/solovay_strassen_primality_test.py)
884885
* [Spearman Rank Correlation Coefficient](maths/spearman_rank_correlation_coefficient.py)
885886
* Special Numbers
887+
* [Abundant Numbers](maths/special_numbers/abundant_numbers.py)
886888
* [Armstrong Numbers](maths/special_numbers/armstrong_numbers.py)
887889
* [Automorphic Number](maths/special_numbers/automorphic_number.py)
888890
* [Bell Numbers](maths/special_numbers/bell_numbers.py)
889891
* [Carmichael Number](maths/special_numbers/carmichael_number.py)
890892
* [Catalan Number](maths/special_numbers/catalan_number.py)
893+
* [Deficient Numbers](maths/special_numbers/deficient_numbers.py)
894+
* [Disarum Number](maths/special_numbers/disarum_number.py)
891895
* [Hamming Numbers](maths/special_numbers/hamming_numbers.py)
892896
* [Happy Number](maths/special_numbers/happy_number.py)
893897
* [Harshad Numbers](maths/special_numbers/harshad_numbers.py)
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
"""
2+
An implementation of the ear clipping method for triangulating a simple polygon.
3+
4+
Wikipedia : https://en.wikipedia.org/wiki/Polygon_triangulation
5+
"""
6+
7+
8+
def is_ear(polygon: list[tuple[float, float]], point_idx: int, direction: str) -> bool:
9+
"""
10+
This function determines whether three points form an ear.
11+
12+
>>> is_ear([(0, 2), (2, 2), (2,0), (1, 0), (1, 1), (0, 1)], 4,
13+
... "counter-clockwise")
14+
False
15+
>>> is_ear([(0, 2), (2, 2), (2,0), (1, 0), (1, 1), (0, 1)], 3,
16+
... "counter-clockwise")
17+
True
18+
>>> is_ear([(0, 3), (2, 2), (3, 0), (3, 3)], 3, "clockwise")
19+
False
20+
>>> is_ear([(0, 0), (1, 0), (1, 1), (0, 1)], 0, "clockwise")
21+
True
22+
"""
23+
# Calculate indices for the previous and next vertices in the polygon.
24+
prev_idx = (point_idx - 1) % len(polygon)
25+
next_idx = (point_idx + 1) % len(polygon)
26+
27+
# Retrieve the coordinates of the previous, current, and next vertices.
28+
prev_point = polygon[prev_idx]
29+
point = polygon[point_idx]
30+
next_point = polygon[next_idx]
31+
32+
# Check if the vertex is convex based on the polygon's orientation.
33+
if is_convex(prev_point, point, next_point, direction):
34+
# Check if there are any points inside the triangle formed by the current vertex
35+
# and its neighbors.
36+
for j in range(len(polygon)):
37+
if j not in (prev_idx, point_idx, next_idx) and is_point_inside_triangle(
38+
prev_point, point, next_point, polygon[j]
39+
):
40+
return False # The 'ear' is not valid because there's a point
41+
# inside the triangle.
42+
return True # The vertex is an 'ear' because it's convex and no points are
43+
# inside the triangle.
44+
return False # The vertex is not an 'ear' because it's not convex.
45+
46+
47+
def is_convex(
48+
point: tuple[float, float],
49+
prev_p: tuple[float, float],
50+
next_p: tuple[float, float],
51+
direction: str,
52+
) -> bool:
53+
"""
54+
Determine with the ccw, if 3 points are convex.
55+
>>> is_convex((1,1), (2, 2), (1, 2), "clockwise")
56+
True
57+
>>> is_convex((1,1), (2, 2), (1, 2), "counter-clockwise")
58+
False
59+
>>> is_convex((1,1), (2, 2), (3, 3), "clockwise")
60+
True
61+
>>> is_convex((1,1), (2, 2), (3, 3), "counter-clockwise")
62+
True
63+
"""
64+
# Calculate the cross product based on the polygon's orientation.
65+
if direction == "counter-clockwise":
66+
cross_product = (next_p[0] - point[0]) * (prev_p[1] - point[1]) - (
67+
prev_p[0] - point[0]
68+
) * (next_p[1] - point[1])
69+
else:
70+
cross_product = (prev_p[0] - point[0]) * (next_p[1] - point[1]) - (
71+
next_p[0] - point[0]
72+
) * (prev_p[1] - point[1])
73+
# Determine if the angle is convex (cross product is non-negative).
74+
return cross_product >= 0
75+
76+
77+
def cross_product(
78+
p1: tuple[float, float], p2: tuple[float, float], p3: tuple[float, float]
79+
) -> float:
80+
"""
81+
This function computes the product of two vectors, with 3 points.
82+
If the vectors are collinear, the output is 0.
83+
If the three points rotate counterclockwise, the output is positive.
84+
If three points rotate clockwise, the output is negative.
85+
86+
>>> cross_product((0, 0), (1, 0), (1.5, 0.5))
87+
0.5
88+
>>> cross_product((1.5, 0.5), (1, 1), (1.5, 1.5))
89+
-0.5
90+
>>> cross_product((0, 0), (1, 1), (2, 2))
91+
0
92+
"""
93+
return (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p2[1] - p1[1]) * (p3[0] - p1[0])
94+
95+
96+
def direction(polygon: list[tuple[float, float]]) -> str:
97+
"""
98+
Determine the orientation (clockwise or counterclockwise) of a polygon defined
99+
by a list of points.
100+
>>> direction([(1, 1), (2, 2), (3, 4)])
101+
'clockwise'
102+
>>> direction([(1, 1), (2, 2), (3, -1)])
103+
'counter-clockwise'
104+
"""
105+
# Find the point with the lowest y-coordinate (and leftmost if tied).
106+
point_0 = min(polygon, key=lambda point: (point[1], point[0]))
107+
idx_p0 = polygon.index(point_0)
108+
109+
# Calculate the indices of the previous and next points.
110+
prev_idx = (idx_p0 - 1) % len(polygon)
111+
next_idx = (idx_p0 + 1) % len(polygon)
112+
113+
prev_point = polygon[prev_idx]
114+
next_point = polygon[next_idx]
115+
# Handle cases where multiple points share the same y-coordinate.
116+
while cross_product(point_0, next_point, prev_point) == 0:
117+
next_idx += 1
118+
next_point = polygon[next_idx]
119+
# Determine the polygon's orientation based on the cross product.
120+
if cross_product(point_0, next_point, prev_point) > 0:
121+
return "clockwise" # The polygon is in a clockwise direction.
122+
return "counter-clockwise" # The polygon is in a counter-clockwise direction.
123+
124+
125+
def is_point_inside_triangle(
126+
p1: tuple[float, float],
127+
p2: tuple[float, float],
128+
p3: tuple[float, float],
129+
test_point: tuple[float, float],
130+
) -> bool:
131+
"""
132+
Determine whether a given point is located inside a triangle
133+
formed by three other points.
134+
135+
This function calculates the area of both the triangle formed by
136+
the three input points (p1, p2, p3)
137+
and the sub-triangles formed by replacing one vertex of the main
138+
triangle with the test_point.
139+
If the sum of the areas of the sub-triangles is equal to the area of
140+
the main triangle, the test_point
141+
is considered to be inside the triangle. Otherwise, it is considered outside.
142+
143+
>>> is_point_inside_triangle((0, 0), (0, 2), (2, 0), (3, 3))
144+
False
145+
>>> is_point_inside_triangle((0, 0), (0, 2), (2, 0), (1, 1))
146+
True
147+
>>> is_point_inside_triangle((0, 0), (2, 1), (2, 0), (1, 1))
148+
False
149+
>>> is_point_inside_triangle((0, 0), (2, 1), (2, 0), (2, 0))
150+
True
151+
>>> is_point_inside_triangle((0, 0), (1, 1), (2, 0), (1, 0))
152+
True
153+
"""
154+
# Calculate the area of the main triangle.
155+
area_triangle = abs(
156+
0.5
157+
* (p1[0] * (p2[1] - p3[1]) + p2[0] * (p3[1] - p1[1]) + p3[0] * (p1[1] - p2[1]))
158+
)
159+
# Calculate the areas of the sub-triangles formed by replacing one vertex
160+
# with the test_point.
161+
area1 = abs(
162+
0.5
163+
* (
164+
test_point[0] * (p2[1] - p3[1])
165+
+ p2[0] * (p3[1] - test_point[1])
166+
+ p3[0] * (test_point[1] - p2[1])
167+
)
168+
)
169+
area2 = abs(
170+
0.5
171+
* (
172+
p1[0] * (test_point[1] - p3[1])
173+
+ test_point[0] * (p3[1] - p1[1])
174+
+ p3[0] * (p1[1] - test_point[1])
175+
)
176+
)
177+
area3 = abs(
178+
0.5
179+
* (
180+
p1[0] * (p2[1] - test_point[1])
181+
+ p2[0] * (test_point[1] - p1[1])
182+
+ test_point[0] * (p1[1] - p2[1])
183+
)
184+
)
185+
186+
# Check if the test_point is inside the triangle by comparing areas.
187+
return area_triangle == area1 + area2 + area3
188+
189+
190+
def triangulate_polygon(
191+
coordinates: list[tuple[float, float]],
192+
) -> list[list[tuple[float, float]]]:
193+
"""
194+
Triangulate a polygon and provide the points of the resulting triangles.
195+
This function takes a list of coordinates that represent the vertices of a polygon.
196+
It iteratively finds and removes 'ears' from the polygon to create a list
197+
of triangles that triangulate the entire polygon.
198+
The order of vertices in the coordinates list is assumed to be
199+
consistent (either clockwise or counterclockwise).
200+
The function uses helper functions 'direction' to determine the polygon's
201+
orientation and 'is_ear' to identify 'ear' vertices.
202+
203+
Note: The function assumes that the input coordinates form a valid simple polygon.
204+
205+
>>> triangulate_polygon([(0, 2), (2, 2), (2, 0), (1, 0), (1, 1), (0, 1)])
206+
[[(0, 1), (0, 2), (2, 2)], [(2, 2), (2, 0), (1, 0)], [(2, 2), (1, 0), (1, 1)], \
207+
[(0, 1), (2, 2), (1, 1)]]
208+
>>> triangulate_polygon([(0, 3), (2, 2), (3, 0), (3, 3)])
209+
[[(3, 3), (0, 3), (2, 2)], [(3, 3), (2, 2), (3, 0)]]
210+
>>> triangulate_polygon([(0, 0),(2, 0), (1, 1), (2, 2), (0,2)])
211+
[[(0, 0), (2, 0), (1, 1)], [(0, 2), (0, 0), (1, 1)], [(0, 2), (1, 1), (2, 2)]]
212+
213+
"""
214+
polygon: list[tuple[float, float]] = coordinates.copy()
215+
216+
# Initialize an empty list to store the triangles
217+
triangles: list = []
218+
219+
# Determine the orientation (clockwise or counterclockwise) of the polygon.
220+
orientation: str = direction(polygon)
221+
222+
# Iterate while there are at least three vertices in the polygon.
223+
while len(polygon) >= 3:
224+
ear_found: bool = False
225+
226+
# Find an 'ear' vertex and append the triangle to the list.
227+
for i in range(len(polygon)):
228+
if is_ear(polygon, i, orientation):
229+
ear_found = True
230+
prev_idx: int = (i - 1) % len(polygon)
231+
next_idx: int = (i + 1) % len(polygon)
232+
prev_point: tuple[float, float] = polygon[prev_idx]
233+
point: tuple[float, float] = polygon[i]
234+
next_point: tuple[float, float] = polygon[next_idx]
235+
triangles.append(
236+
[
237+
prev_point,
238+
point,
239+
next_point,
240+
]
241+
)
242+
polygon.pop(i)
243+
break
244+
245+
# If no 'ear' is found, exit the loop.
246+
if not ear_found:
247+
break
248+
249+
return triangles
250+
251+
252+
if __name__ == "__main__":
253+
import doctest
254+
255+
doctest.testmod()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
A number n is said to be an Abundant number if
3+
the sum of its proper divisors is greater than the number itself.
4+
5+
Examples of Abundant Numbers: 12, 18, 20, 24, 30, 36, 40, 42, 48, 54, ...
6+
7+
https://en.wikipedia.org/wiki/Abundant_number
8+
"""
9+
10+
11+
def is_abundant_number(number: int) -> bool:
12+
"""
13+
This function takes an integer number as input.
14+
Returns True if the number is abundant.
15+
16+
>>> is_abundant_number(-1)
17+
False
18+
>>> is_abundant_number(0)
19+
False
20+
>>> is_abundant_number(12)
21+
True
22+
>>> is_abundant_number(18)
23+
True
24+
>>> is_abundant_number(28)
25+
False
26+
>>> is_abundant_number(20)
27+
True
28+
>>> is_abundant_number(6)
29+
False
30+
>>> is_abundant_number(1)
31+
False
32+
>>> is_abundant_number(945)
33+
True
34+
>>> is_abundant_number(28.0)
35+
Traceback (most recent call last):
36+
...
37+
TypeError: Input value of [number=28.0] must be an integer
38+
"""
39+
if not isinstance(number, int):
40+
msg = f"Input value of [number={number}] must be an integer"
41+
raise TypeError(msg)
42+
if number < 1:
43+
return False
44+
45+
divisor_sum = 1 # 1 is always a proper divisor
46+
for i in range(2, int(number**0.5) + 1):
47+
if number % i == 0:
48+
divisor_sum += i
49+
if i != number // i:
50+
divisor_sum += number // i
51+
return divisor_sum > number
52+
53+
54+
if __name__ == "__main__":
55+
import doctest
56+
57+
doctest.testmod()
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""
2+
A number n is said to be a Deficient number if
3+
the sum of its proper divisors is less than the number itself.
4+
5+
Examples of Deficient Numbers: 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 13, ...
6+
7+
https://en.wikipedia.org/wiki/Deficient_number
8+
"""
9+
10+
11+
def is_deficient_number(number: int) -> bool:
12+
"""
13+
This function takes an integer number as input.
14+
Returns True if the number is a deficient number.
15+
16+
>>> is_deficient_number(-1)
17+
False
18+
>>> is_deficient_number(0)
19+
False
20+
>>> is_deficient_number(1)
21+
True
22+
>>> is_deficient_number(2)
23+
True
24+
>>> is_deficient_number(6)
25+
False
26+
>>> is_deficient_number(12)
27+
False
28+
>>> is_deficient_number(7)
29+
True
30+
>>> is_deficient_number(28)
31+
False
32+
>>> is_deficient_number(15)
33+
True
34+
>>> is_deficient_number(8.0)
35+
Traceback (most recent call last):
36+
...
37+
TypeError: Input value of [number=8.0] must be an integer
38+
"""
39+
40+
if not isinstance(number, int):
41+
msg = f"Input value of [number={number}] must be an integer"
42+
raise TypeError(msg)
43+
if number < 1:
44+
return False
45+
if number == 1:
46+
return True
47+
48+
divisor_sum = 1
49+
for i in range(2, int(number**0.5) + 1):
50+
if number % i == 0:
51+
divisor_sum += i
52+
if i != number // i:
53+
divisor_sum += number // i
54+
return divisor_sum < number
55+
56+
57+
if __name__ == "__main__":
58+
import doctest
59+
60+
doctest.testmod()

0 commit comments

Comments
 (0)