From 4bb99a37ea6076d22368b8e7cda75761d0947e98 Mon Sep 17 00:00:00 2001 From: Alexey Zakharenkov Date: Thu, 13 Oct 2022 13:28:49 +0300 Subject: [PATCH] Refactoring: move a nested function to the module level; add tests for it --- subway_structure.py | 26 ++++--- tests/test_projection.py | 157 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 12 deletions(-) create mode 100644 tests/test_projection.py diff --git a/subway_structure.py b/subway_structure.py index 1cc367e..015185a 100644 --- a/subway_structure.py +++ b/subway_structure.py @@ -130,19 +130,21 @@ def is_near(p1, p2): ) -def project_on_line(p, line): - def project_on_segment(p, p1, p2): - dp = (p2[0] - p1[0], p2[1] - p1[1]) - d2 = dp[0] * dp[0] + dp[1] * dp[1] - if d2 < 1e-14: - return None - # u is the position of projection of p point on line p1p2 - # regarding point p1 and (p2-p1) direction vector - u = ((p[0] - p1[0]) * dp[0] + (p[1] - p1[1]) * dp[1]) / d2 - if not 0 <= u <= 1: - return None - return u +def project_on_segment(p, p1, p2): + """Given three points, return u - the position of projection of + point p onto segment p1p2 regarding point p1 and (p2-p1) direction vector + """ + dp = (p2[0] - p1[0], p2[1] - p1[1]) + d2 = dp[0] * dp[0] + dp[1] * dp[1] + if d2 < 1e-14: + return None + u = ((p[0] - p1[0]) * dp[0] + (p[1] - p1[1]) * dp[1]) / d2 + if not 0 <= u <= 1: + return None + return u + +def project_on_line(p, line): result = { # In the first approximation, position on rails is the index of the # closest vertex of line to the point p. Fractional value means that diff --git a/tests/test_projection.py b/tests/test_projection.py new file mode 100644 index 0000000..44c362f --- /dev/null +++ b/tests/test_projection.py @@ -0,0 +1,157 @@ +import collections +import itertools +import unittest + +from subway_structure import project_on_segment + + +class TestProjection(unittest.TestCase): + """Test subway_structure.project_on_segment function""" + + PRECISION = 10 # decimal places in assertAlmostEqual + + SHIFT = 1e-6 # Small distance between projected point and segment endpoint + + def _test_projection_in_bulk(self, points, segments, answers): + """Test 'project_on_segment' function for array of points and array + of parallel segments projections on which are equal. + """ + for point, ans in zip(points, answers): + for seg in segments: + for segment, answer in zip( + (seg, seg[::-1]), # What if invert the segment? + (ans, None if ans is None else 1 - ans), + ): + u = project_on_segment(point, segment[0], segment[1]) + + if answer is None: + self.assertIsNone( + u, + f"Project of point {point} onto segment {segment} " + f"should be None, but {u} returned", + ) + else: + self.assertAlmostEqual( + u, + answer, + self.PRECISION, + f"Wrong projection of point {point} onto segment " + f"{segment}: {u} returned, {answer} expected", + ) + + def test_projection_on_horizontal_segments(self): + + points = [ + (-2, 0), + (-1 - self.SHIFT, 0), + (-1, 0), + (-1 + self.SHIFT, 0), + (-0.5, 0), + (0, 0), + (0.5, 0), + (1 - self.SHIFT, 0), + (1, 0), + (1 + self.SHIFT, 0), + (2, 0), + ] + horizontal_segments = [ + ((-1, -1), (1, -1)), + ((-1, 0), (1, 0)), + ((-1, 1), (1, 1)), + ] + answers = [ + None, + None, + 0, + self.SHIFT / 2, + 0.25, + 0.5, + 0.75, + 1 - self.SHIFT / 2, + 1, + None, + None, + ] + + self._test_projection_in_bulk(points, horizontal_segments, answers) + + def test_projection_on_vertical_segments(self): + points = [ + (0, -2), + (0, -1 - self.SHIFT), + (0, -1), + (0, -1 + self.SHIFT), + (0, -0.5), + (0, 0), + (0, 0.5), + (0, 1 - self.SHIFT), + (0, 1), + (0, 1 + self.SHIFT), + (0, 2), + ] + vertical_segments = [ + ((-1, -1), (-1, 1)), + ((0, -1), (0, 1)), + ((1, -1), (1, 1)), + ] + answers = [ + None, + None, + 0, + self.SHIFT / 2, + 0.25, + 0.5, + 0.75, + 1 - self.SHIFT / 2, + 1, + None, + None, + ] + + self._test_projection_in_bulk(points, vertical_segments, answers) + + def test_projection_on_inclined_segment(self): + points = [ + (-2, -2), + (-1, -1), + (-0.5, -0.5), + (0, 0), + (0.5, 0.5), + (1, 1), + (2, 2), + ] + segments = [ + ((-2, 0), (0, 2)), + ((-1, -1), (1, 1)), + ((0, -2), (2, 0)), + ] + answers = [None, 0, 0.25, 0.5, 0.75, 1, None] + + self._test_projection_in_bulk(points, segments, answers) + + def test_projection_with_different_collections(self): + """The tested function should accept points as any consecutive + container with index operator. + """ + types = (tuple, list, collections.deque,) + + point = (0, 0.5) + segment_end1 = (0, 0) + segment_end2 = (1, 0) + + for p_type, s1_type, s2_type in itertools.product(types, types, types): + p = p_type(point) + s1 = s1_type(segment_end1) + s2 = s2_type(segment_end2) + project_on_segment(p, s1, s2) + + def test_projection_on_degenerate_segment(self): + coords = [-1, 0, 1] + points = [(x, y) for x, y in itertools.product(coords, coords)] + segments = [ + ((0, 0), (0, 0)), + ((0, 0), (0, 1e-8)), + ] + answers = [None] * len(points) + + self._test_projection_in_bulk(points, segments, answers)