Skip to content

Commit 27348a2

Browse files
committed
fix(interval): intersect overlapping and touching intervals correctly
QwtInterval.intersect (and the `&` operator) returned an invalid interval for partially overlapping intervals, e.g. QwtInterval(0, 2) & QwtInterval(1, 3), and for intervals sharing an included bound: after ordering the intervals by their minimum, it tested `i1.maxValue() < i2.maxValue()` instead of `i1.maxValue() < i2.minValue()` (Qwt C++). QwtInterval.intersects gave the opposite result for intervals sharing a bound: `[0, 1]` and `[1, 2]` did not intersect, while `[0, 1)` and `(1, 2]` did. Qwt C++ returns `!(i1 excludes its maximum || i2 excludes its minimum)`. - Restore the Qwt C++ conditions; intersects now always returns a bool. - Results which were already valid are unchanged: checked on all the 4096 pairs of intervals with bounds in 0..3 and any border flags (including invalid intervals), both methods now match a direct transcription of the Qwt C++ code. - Regression tests: table of overlapping, nested, equal, disjoint and touching intervals with various border flags, in both orders, with `intersect` and `&`; `intersects` cases; invalid intervals. - Changelog entry. Refs #115 Assisted-by: Claude Opus 5.5
1 parent 36285dc commit 27348a2

3 files changed

Lines changed: 81 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
- Fixed `QwtDynGridLayout` (used by `QwtLegend`), whose `numRows()` returned a float (e.g. `2.5` for 3 items on 2 columns) and which raised a `TypeError` when laying out its items with vertical expansion (`setExpandingDirections(Qt.Vertical)`) ([Issue #115](https://github.com/PlotPyStack/PythonQwt/issues/115))
2020
- Fixed canvas style sheets with rounded borders (e.g. `plot.canvas().setStyleSheet("border: 2px solid black; border-radius: 8px;")`), which raised a `TypeError` on each style change, resize and paint event (aborting the process with PyQt's default exception hook): the rounded border of the style sheet is now taken into account to clip the plot items and to fill the corners with the background of the parent widget, as in Qwt ([Issue #115](https://github.com/PlotPyStack/PythonQwt/issues/115))
2121
- Fixed the image formats listed by the file dialog of `QwtPlotRenderer.exportTo`, which read `*.b'bmp' *.b'png' ...` instead of `*.bmp *.png ...` ([Issue #115](https://github.com/PlotPyStack/PythonQwt/issues/115))
22+
- Fixed `QwtInterval.intersect` (and the `&` operator), which returned an invalid interval for partially overlapping intervals (e.g. `QwtInterval(0, 2) & QwtInterval(1, 3)` instead of `[1, 2]`), and `QwtInterval.intersects`, which gave the opposite result for intervals sharing a bound (e.g. `False` for `[0, 1]` and `[1, 2]`) ([Issue #115](https://github.com/PlotPyStack/PythonQwt/issues/115))
2223

2324
## Version 0.16.3
2425

‎qwt/interval.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def intersect(self, other):
296296
if i1.borderFlags() & self.ExcludeMinimum:
297297
i1, i2 = i2, i1
298298

299-
if i1.maxValue() < i2.maxValue():
299+
if i1.maxValue() < i2.minValue():
300300
return QwtInterval()
301301

302302
if i1.maxValue() == i2.minValue():
@@ -346,9 +346,9 @@ def intersects(self, other):
346346
if i1.maxValue() > i2.minValue():
347347
return True
348348
elif i1.maxValue() == i2.minValue():
349-
return (
349+
return not (
350350
i1.borderFlags() & self.ExcludeMaximum
351-
and i2.borderFlags() & self.ExcludeMinimum
351+
or i2.borderFlags() & self.ExcludeMinimum
352352
)
353353
return False
354354

‎qwt/tests/test_issue115_porting_bugs.py‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import qwt.plot_renderer
2525
from qwt import (
26+
QwtInterval,
2627
QwtLegend,
2728
QwtLinearScaleEngine,
2829
QwtPlot,
@@ -663,6 +664,78 @@ def fake_getsavefilename(*args, **kwargs):
663664
del app
664665

665666

667+
# ---------------------------------------------------------------------------
668+
# QwtInterval.intersect / QwtInterval.intersects
669+
# ---------------------------------------------------------------------------
670+
671+
INC = QwtInterval.IncludeBorders
672+
EXC_MIN = QwtInterval.ExcludeMinimum
673+
EXC_MAX = QwtInterval.ExcludeMaximum
674+
675+
# (interval 1, interval 2, expected intersection or None if invalid),
676+
# intervals being (min, max, border flags); expected values follow Qwt C++
677+
INTERSECT_CASES = (
678+
((0, 2, INC), (1, 3, INC), (1, 2, INC)),
679+
((1, 3, INC), (0, 2, INC), (1, 2, INC)),
680+
((0, 3, INC), (1, 2, INC), (1, 2, INC)),
681+
((0, 2, INC), (0, 2, INC), (0, 2, INC)),
682+
((0, 1, INC), (2, 3, INC), None),
683+
((0, 1, INC), (1, 2, INC), (1, 1, INC)),
684+
((0, 1, EXC_MAX), (1, 2, INC), None),
685+
((0, 1, INC), (1, 2, EXC_MIN), None),
686+
((0, 2, EXC_MIN), (1, 3, EXC_MAX), (1, 2, INC)),
687+
((0, 2, EXC_MAX), (1, 3, INC), (1, 2, EXC_MAX)),
688+
((0, 2, INC), (0, 1, EXC_MIN), (0, 1, EXC_MIN)),
689+
)
690+
691+
692+
def _interval(values):
693+
return QwtInterval(*values)
694+
695+
696+
@pytest.mark.parametrize("values1, values2, expected", INTERSECT_CASES)
697+
def test_interval_intersect(values1, values2, expected):
698+
"""QwtInterval.intersect and `&` return the intersection of two intervals"""
699+
interval1, interval2 = _interval(values1), _interval(values2)
700+
for result in (interval1.intersect(interval2), interval1 & interval2):
701+
if expected is None:
702+
assert not result.isValid()
703+
else:
704+
assert result == _interval(expected)
705+
706+
707+
def test_interval_intersect_invalid():
708+
"""The intersection with an invalid interval is invalid"""
709+
assert not QwtInterval().intersect(QwtInterval(0, 1)).isValid()
710+
assert not QwtInterval(0, 1).intersect(QwtInterval()).isValid()
711+
712+
713+
@pytest.mark.parametrize(
714+
"values1, values2, expected",
715+
(
716+
((0, 2, INC), (1, 3, INC), True),
717+
((1, 3, INC), (0, 2, INC), True),
718+
((0, 3, INC), (1, 2, INC), True),
719+
((0, 1, INC), (2, 3, INC), False),
720+
((0, 1, INC), (1, 2, INC), True),
721+
((1, 2, INC), (0, 1, INC), True),
722+
((0, 1, EXC_MAX), (1, 2, INC), False),
723+
((0, 1, INC), (1, 2, EXC_MIN), False),
724+
((0, 1, EXC_MAX), (1, 2, EXC_MIN), False),
725+
),
726+
)
727+
def test_interval_intersects(values1, values2, expected):
728+
"""QwtInterval.intersects tells whether two intervals overlap"""
729+
result = _interval(values1).intersects(_interval(values2))
730+
assert result is expected
731+
732+
733+
def test_interval_intersects_invalid():
734+
"""An invalid interval does not intersect any interval"""
735+
assert QwtInterval().intersects(QwtInterval(0, 1)) is False
736+
assert QwtInterval(0, 1).intersects(QwtInterval()) is False
737+
738+
666739
if __name__ == "__main__":
667740
for name in ("toPixmap", "toImage"):
668741
test_graphic_conversion_default_size(name)
@@ -686,4 +759,8 @@ def fake_getsavefilename(*args, **kwargs):
686759
for sheet in ROUNDED_STYLE_SHEETS:
687760
for backing in (False, True):
688761
test_canvas_rounded_style_sheet(sheet, backing)
762+
for values1, values2, expected in INTERSECT_CASES:
763+
test_interval_intersect(values1, values2, expected)
764+
test_interval_intersect_invalid()
765+
test_interval_intersects_invalid()
689766
print("OK")

0 commit comments

Comments
 (0)