1+ import re
12from enum import IntEnum
23from collections import namedtuple
3- from typing import Optional
4+ from typing import Optional , Tuple , Iterable
45
56from 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
1213import Orange .data
1314import 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+
144210class 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 ():
0 commit comments