Skip to content

Commit df550e5

Browse files
committed
owdiscretize: Implement custom input validator
1 parent 7fad7fc commit df550e5

File tree

2 files changed

+97
-17
lines changed

2 files changed

+97
-17
lines changed

Orange/widgets/data/owdiscretize.py

Lines changed: 72 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1+
import re
12
from enum import IntEnum
23
from collections import namedtuple
3-
from typing import Optional
4+
from typing import Optional, Tuple, Iterable
45

56
from AnyQt.QtWidgets import (
67
QListView, QHBoxLayout, QStyledItemDelegate, QButtonGroup, QWidget,
78
QLineEdit
89
)
9-
from AnyQt.QtGui import QRegularExpressionValidator
10-
from AnyQt.QtCore import Qt, QRegularExpression
10+
from AnyQt.QtGui import QValidator
11+
from AnyQt.QtCore import Qt
1112

1213
import Orange.data
1314
import Orange.preprocess.discretize as disc
@@ -141,6 +142,71 @@ def from_method(method):
141142
return Methods[type(method).__name__]
142143

143144

145+
def parse_float(string: str) -> Optional[float]:
146+
try:
147+
return float(string)
148+
except ValueError:
149+
return None
150+
151+
152+
class IncreasingNumbersListValidator(QValidator):
153+
"""
154+
Match a comma separated list of non-empty and increasing number strings.
155+
156+
Example
157+
-------
158+
>>> v = IncreasingNumbersListValidator()
159+
>>> v.validate("", 0) # Acceptable
160+
(2, '', 0)
161+
>>> v.validate("1", 1) # Acceptable
162+
(2, '1', 1)
163+
>>> v.validate("1,,", 1) # Intermediate
164+
(1, '1,,', 2)
165+
"""
166+
@staticmethod
167+
def itersplit(string: str) -> Iterable[Tuple[int, int]]:
168+
sepiter = re.finditer(r"(?<!\\),", string)
169+
start = 0
170+
for match in sepiter:
171+
yield start, match.start()
172+
start = match.end()
173+
# yield the rest if any
174+
if start < len(string):
175+
yield start, len(string)
176+
177+
def validate(self, string: str, pos: int) -> Tuple[QValidator.State, str, int]:
178+
state = QValidator.Acceptable
179+
# Matches non-complete intermediate numbers (while editing)
180+
intermediate = re.compile(r"([+-]?\s?\d*\s?\d*\.?\d*\s?\d*)")
181+
values = []
182+
for start, end in self.itersplit(string):
183+
valuestr = string[start:end].strip()
184+
if not valuestr:
185+
state = min(state, QValidator.Intermediate)
186+
# Middle element is empty
187+
continue
188+
value = parse_float(valuestr)
189+
if value is None:
190+
if intermediate.fullmatch(valuestr):
191+
state = min(state, QValidator.Intermediate)
192+
continue
193+
return QValidator.Invalid, string, pos
194+
if values and value <= values[-1]:
195+
state = min(state, QValidator.Intermediate)
196+
else:
197+
values.append(value)
198+
return state, string, pos
199+
200+
def fixup(self, string):
201+
# type: (str) -> str
202+
"""
203+
Fixup the input. Remove empty parts from the string.
204+
"""
205+
parts = [string[start: end] for start, end in self.itersplit(string)]
206+
parts = [part for part in parts if part.strip()]
207+
return ", ".join(parts)
208+
209+
144210
class OWDiscretize(widget.OWWidget):
145211
# pylint: disable=too-many-instance-attributes
146212
name = "Discretize"
@@ -242,18 +308,7 @@ def set_manual_default_cuts():
242308
self._default_disc_changed()
243309
self.manual_cuts_edit.editingFinished.connect(set_manual_default_cuts)
244310

245-
reexp = QRegularExpression()
246-
decimal = r"([+-]?(\.\d+|\d+\.?|\d+\.\d+))"
247-
reexp.setPattern(rf"""
248-
\s*(,?|[+-]?)?
249-
\s*({decimal} # single number
250-
|({decimal}(\s*,\s*{decimal})*) # concat
251-
)
252-
\s*,?\s* # optional trailing separator/space
253-
""")
254-
reexp.setPatternOptions(QRegularExpression.ExtendedPatternSyntaxOption)
255-
assert reexp.isValid()
256-
validator = QRegularExpressionValidator(reexp)
311+
validator = IncreasingNumbersListValidator()
257312
self.manual_cuts_edit.setValidator(validator)
258313
ibox = gui.indentedBox(right, orientation=Qt.Horizontal)
259314
ibox.layout().addWidget(self.manual_cuts_edit)
@@ -264,7 +319,7 @@ def set_manual_default_cuts():
264319
self.connect_control(
265320
"default_cutpoints",
266321
lambda values: self.manual_cuts_edit.setText(
267-
", ".join(map(str, values)))
322+
", ".join(map("{:.17g}".format, values)))
268323
)
269324
vlayout = QHBoxLayout()
270325
box = gui.widgetBox(
@@ -301,6 +356,7 @@ def set_manual_default_cuts():
301356
b = gui.appendRadioButton(controlbox, "Manual", id=Methods.Custom)
302357

303358
self.manual_cuts_specific = QLineEdit()
359+
self.manual_cuts_specific.setValidator(validator)
304360
b.toggled[bool].connect(self.manual_cuts_specific.setEnabled)
305361

306362
def set_manual_cuts():

Orange/widgets/data/tests/test_owdiscretize.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# Test methods with long descriptive names can omit docstrings
22
# pylint: disable=missing-docstring,unsubscriptable-object,protected-access
3+
import unittest
34
from unittest.mock import Mock
45

56
from Orange.data import Table
67
from Orange.widgets.data.owdiscretize import OWDiscretize, Default, EqualFreq, \
7-
Remove, Leave, Custom
8+
Remove, Leave, Custom, IncreasingNumbersListValidator
89
from Orange.widgets.tests.base import WidgetTest
910
from Orange.widgets.utils.state_summary import format_summary_details
1011
from Orange.widgets.utils.itemmodels import select_row
@@ -123,3 +124,26 @@ def test_manual_cuts_copy(self):
123124
cc_button.click()
124125
self.assertEqual(widget.method_for_index(0), Custom(points))
125126
self.assertEqual(varbg.checkedId(), OWDiscretize.Custom)
127+
128+
129+
class TestValidator(unittest.TestCase):
130+
def test_validate(self):
131+
v = IncreasingNumbersListValidator()
132+
self.assertEqual(v.validate("", 0), (v.Acceptable, '', 0))
133+
self.assertEqual(v.validate("1", 1), (v.Acceptable, '1', 1))
134+
self.assertEqual(v.validate(",", 0), (v.Intermediate, ',', 0))
135+
self.assertEqual(v.validate("-", 0), (v.Intermediate, '-', 0))
136+
self.assertEqual(v.validate("1,,", 1), (v.Intermediate, '1,,', 1))
137+
self.assertEqual(v.validate("1,a,", 1), (v.Invalid, '1,a,', 1))
138+
self.assertEqual(v.validate("a", 1), (v.Invalid, 'a', 1))
139+
self.assertEqual(v.validate("1,1", 0), (v.Intermediate, '1,1', 0))
140+
self.assertEqual(v.validate("1,12", 0), (v.Acceptable, '1,12', 0))
141+
142+
def test_fixup(self):
143+
v = IncreasingNumbersListValidator()
144+
self.assertEqual(v.fixup(""), "")
145+
self.assertEqual(v.fixup("1,,2"), "1, 2")
146+
self.assertEqual(v.fixup("1,,"), "1")
147+
self.assertEqual(v.fixup("1,"), "1")
148+
self.assertEqual(v.fixup(",1"), "1")
149+
self.assertEqual(v.fixup(","), "")

0 commit comments

Comments
 (0)