Skip to content

Commit 58e2b2c

Browse files
authored
Added Clamping Tests (#60)
* Starting my testing * Generated first test * Added another test * Added yet more tests * Automated function list * Reworked test bench to automate function collection * Cleaned up code * Reworked all solutions to use simplified test bench * Added comments * Added a large numbers test
1 parent 6a285f0 commit 58e2b2c

10 files changed

+114
-78
lines changed

testing/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@
22
from os.path import dirname, basename, isfile, join
33

44
modules = glob.glob(join(dirname(__file__), "*.py"))
5-
__all__ = [basename(f)[:-3] for f in modules if
6-
isfile(f) and not f.endswith('__init__.py') and not f.endswith("test_bench.py")]
5+
__all__ = [
6+
basename(f)[:-3] for f in modules
7+
if isfile(f) and not f.endswith('__init__.py') and not f.endswith("test_bench.py")
8+
]

testing/how_to_capitalize_a_string.py

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,25 +73,11 @@ def capitalize(string: str) -> str:
7373
return string.capitalize()
7474

7575

76-
def main() -> None:
77-
"""
78-
Tests the performance of all the functions defined in this file.
79-
"""
76+
if __name__ == '__main__':
8077
test_bench(
81-
[
82-
control,
83-
capitalize_by_hand,
84-
capitalize_by_mapping,
85-
capitalize_with_upper,
86-
capitalize
87-
],
8878
{
8979
"One Letter String": ["a"],
9080
"Small String": ["how now brown cow"],
9181
"Large String": ["One Punch Man" * 100]
9282
}
9383
)
94-
95-
96-
if __name__ == '__main__':
97-
main()

testing/how_to_clamp_a_float.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
Tests the performance of all of the solutions listed in the following article:
3+
https://therenegadecoder.com/code/how-to-clamp-a-floating-point-number-in-python/
4+
"""
5+
6+
from test_bench import test_bench
7+
8+
9+
def control(*_) -> None:
10+
"""
11+
Provides a control scenario for testing. In this case, none of the functions
12+
share any overhead, so this function is empty.
13+
14+
:param _: a placeholder for the string input
15+
:return: None
16+
"""
17+
pass
18+
19+
20+
def clamp_float_with_branching_nested(num: float, minimum: float, maximum: float) -> float:
21+
"""
22+
Clamps a float between two bounds using a series of if statements.
23+
24+
:param num: the value to clamp
25+
:param minimum: the lower bound
26+
:param maximum: the upper bound
27+
:return: a value in the range of minimum and maximum
28+
"""
29+
if num < minimum:
30+
return minimum
31+
elif num > maximum:
32+
return maximum
33+
else:
34+
return num
35+
36+
37+
def clamp_float_with_branching_flat(num: float, minimum: float, maximum: float) -> float:
38+
"""
39+
Clamps a float between two bounds using a series of ternary statements.
40+
41+
:param num: the value to clamp
42+
:param minimum: the lower bound
43+
:param maximum: the upper bound
44+
:return: a value in the range of minimum and maximum
45+
"""
46+
return minimum if num < minimum else maximum if num > maximum else num
47+
48+
49+
def clamp_float_with_min_and_max(num: float, minimum: float, maximum: float) -> float:
50+
"""
51+
Clamps a float between two bounds using a mix of min and max functions.
52+
53+
:param num: the value to clamp
54+
:param minimum: the lower bound
55+
:param maximum: the upper bound
56+
:return: a value in the range of minimum and maximum
57+
"""
58+
return max(min(num, maximum), minimum)
59+
60+
61+
def clamp_float_with_sorting(num: float, minimum: float, maximum: float) -> float:
62+
"""
63+
Clamps a float between two bounds using a sorting technique.
64+
65+
:param num: the value to clamp
66+
:param minimum: the lower bound
67+
:param maximum: the upper bound
68+
:return: a value in the range of minimum and maximum
69+
"""
70+
sorted([num, minimum, maximum])[1]
71+
72+
73+
if __name__ == "__main__":
74+
test_bench(
75+
{
76+
"Lower Bound": [-.002, 0, .40],
77+
"Upper Bound": [.402, 0, .40],
78+
"Between Bounds": [.14, 0, .4],
79+
"Large Numbers": [123456789, -432512317, 5487131463]
80+
}
81+
)

testing/how_to_convert_an_integer_to_a_string.py

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,12 @@ def convert_by_interpolation(integer: int) -> str:
4747
return "%s" % integer
4848

4949

50-
def main() -> None:
51-
"""
52-
Tests the performance of all the functions defined in this file.
53-
"""
50+
if __name__ == '__main__':
5451
test_bench(
55-
[
56-
control,
57-
convert_by_type_casting,
58-
convert_by_f_string,
59-
convert_by_interpolation
60-
],
6152
{
6253
"Zero": [0],
6354
"Single Digit": [5],
6455
"Small Number": [1107321],
6556
"Massive Number": [2 ** 64]
6657
}
6758
)
68-
69-
70-
if __name__ == '__main__':
71-
main()

testing/how_to_empty_a_list.py

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def control(anime: list[str]) -> None:
1010
"""
1111
Provides a control scenario for testing. In this case, all of the solutions
1212
rely on copying the input list, so the control function accounts for that.
13-
13+
1414
:param anime: a list of anime
1515
"""
1616
anime = anime.copy()
@@ -19,7 +19,7 @@ def control(anime: list[str]) -> None:
1919
def empty_list_by_hand(anime: list[str]) -> None:
2020
"""
2121
Empties a list by repeatedly removing elements from a list.
22-
22+
2323
:param anime: a list of anime
2424
"""
2525
anime = anime.copy()
@@ -30,7 +30,7 @@ def empty_list_by_hand(anime: list[str]) -> None:
3030
def empty_list_by_assignment(anime: list[str]) -> None:
3131
"""
3232
Empties a list by reassigning the reference.
33-
33+
3434
:param anime: a list of anime
3535
"""
3636
anime = anime.copy()
@@ -40,7 +40,7 @@ def empty_list_by_assignment(anime: list[str]) -> None:
4040
def empty_list_by_clear(anime: list[str]) -> None:
4141
"""
4242
Empties a list by calling the clear method.
43-
43+
4444
:param anime: a list of anime
4545
"""
4646
anime = anime.copy()
@@ -50,7 +50,7 @@ def empty_list_by_clear(anime: list[str]) -> None:
5050
def empty_list_by_del(anime: list[str]) -> None:
5151
"""
5252
Empties a list by deleting a slice of the list.
53-
53+
5454
:param anime: a list of anime
5555
"""
5656
anime = anime.copy()
@@ -60,7 +60,7 @@ def empty_list_by_del(anime: list[str]) -> None:
6060
def empty_list_by_slice_assignment(anime: list[str]) -> None:
6161
"""
6262
Empties a list by replacing a slice of the list with an empty list.
63-
63+
6464
:param anime: a list of anime
6565
"""
6666
anime = anime.copy()
@@ -70,35 +70,19 @@ def empty_list_by_slice_assignment(anime: list[str]) -> None:
7070
def empty_list_by_multiplication(anime: list[str]) -> None:
7171
"""
7272
Empties a list by multiplication.
73-
73+
7474
:param anime: a list of anime
7575
"""
7676
anime = anime.copy()
7777
anime *= 0 # Also, would not work as a function
7878

7979

80-
def main() -> None:
81-
"""
82-
Tests the performance of all the functions defined in this file.
83-
"""
80+
if __name__ == "__main__":
8481
test_bench(
85-
[
86-
control,
87-
empty_list_by_hand,
88-
empty_list_by_assignment,
89-
empty_list_by_clear,
90-
empty_list_by_del,
91-
empty_list_by_slice_assignment,
92-
empty_list_by_multiplication
93-
],
9482
{
9583
"Empty List": [[]],
9684
"One Item List": [["Your Lie in April"]],
9785
"Small List": [["My Hero Academia", "Attack on Titan", "Steins;Gate"]],
9886
"Large List": [["One Punch Man"] * 100]
9987
}
10088
)
101-
102-
103-
if __name__ == "__main__":
104-
main()

testing/test_bench.py

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,14 @@
1-
import importlib
21
import inspect
32
import os
43
import timeit
4+
from inspect import getmembers, isfunction
55

66
import matplotlib.pyplot as plt
77
import pandas as pd
88
import seaborn as sns
99

10-
import testing
1110

12-
13-
def run_suite() -> None:
14-
"""
15-
An experimental function which allows us to run the main function of
16-
all of our test files.
17-
18-
:return: None
19-
"""
20-
for module_name in testing.__all__:
21-
module = importlib.import_module(module_name)
22-
module.main()
23-
24-
25-
def test_bench(funcs: list, test_data: dict):
11+
def test_bench(test_data: dict):
2612
"""
2713
Given a list of functions and a list of dictionary of test data,
2814
this function will execute performance testing using all of the items
@@ -35,6 +21,12 @@ def test_bench(funcs: list, test_data: dict):
3521
:param funcs: a list of functions
3622
:param test_data: a dictionary of test data
3723
"""
24+
frame = inspect.stack()[1]
25+
module = inspect.getmodule(frame[0])
26+
funcs = [
27+
member[1] for member in getmembers(module, isfunction)
28+
if not "test_bench" in member[0]
29+
]
3830
results = _test_performance(funcs, test_data)
3931
_show_results(results)
4032

@@ -86,11 +78,15 @@ def _show_results(results: pd.DataFrame):
8678
aspect=2
8779
)
8880
plt.title("How to Python: Function Performance Comparison", fontsize=16)
89-
plt.legend(bbox_to_anchor=(1.05, 1), loc=2, title="Functions", fontsize='12', title_fontsize='12')
81+
plt.legend(
82+
bbox_to_anchor=(1.05, 1),
83+
loc=2,
84+
title="Functions",
85+
fontsize='12',
86+
title_fontsize='12'
87+
)
9088
plt.tight_layout()
91-
filename = os.path.splitext(os.path.basename(inspect.stack()[2].filename))[0]
92-
plt.savefig(f"{os.path.join('visualizations', filename)}.png")
93-
94-
95-
if __name__ == '__main__':
96-
run_suite()
89+
filename = os.path.splitext(
90+
os.path.basename(inspect.stack()[2].filename)
91+
)[0]
92+
plt.savefig(f"{os.path.join('testing', 'visualizations', filename)}.png")
Loading
58.1 KB
Loading
Loading
817 Bytes
Loading

0 commit comments

Comments
 (0)