Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] owimpute: Make default_numeric locale independant #5209

Merged
merged 2 commits into from
Jan 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 31 additions & 24 deletions Orange/widgets/data/owimpute.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
from AnyQt.QtWidgets import (
QGroupBox, QRadioButton, QPushButton, QHBoxLayout, QGridLayout,
QVBoxLayout, QStackedWidget, QComboBox, QWidget,
QButtonGroup, QStyledItemDelegate, QListView, QDoubleSpinBox, QLabel
QButtonGroup, QStyledItemDelegate, QListView, QLabel
)
from AnyQt.QtCore import Qt, QThread, QModelIndex, QDateTime, QLocale
from AnyQt.QtCore import Qt, QThread, QModelIndex, QDateTime
from AnyQt.QtCore import pyqtSlot as Slot
from AnyQt.QtGui import QDoubleValidator

from orangewidget.utils.listview import ListViewSearch

Expand All @@ -28,6 +27,7 @@
from Orange.widgets.utils.sql import check_sql_input
from Orange.widgets.utils.widgetpreview import WidgetPreview
from Orange.widgets.utils.state_summary import format_summary_details
from Orange.widgets.utils.spinbox import DoubleSpinBox
from Orange.widgets.widget import OWWidget, Msg, Input, Output
from Orange.classification import SimpleTreeLearner

Expand Down Expand Up @@ -127,6 +127,10 @@ def var_key(var):
return qname, var.name


DBL_MIN = np.finfo(float).min
DBL_MAX = np.finfo(float).max


class OWImpute(OWWidget):
name = "Impute"
description = "Impute missing values in the data table."
Expand Down Expand Up @@ -156,7 +160,7 @@ class Warning(OWWidget.Warning):
_variable_imputation_state = settings.ContextSetting({}) # type: VariableState

autocommit = settings.Setting(True)
default_numeric = settings.Setting("")
default_numeric_value = settings.Setting(0.0)
default_time = settings.Setting(0)

want_main_area = False
Expand Down Expand Up @@ -206,17 +210,19 @@ def set_default_time(datetime):
button.setChecked(Method.Default == self.default_method_index)
hlayout.addWidget(button)

locale = QLocale()
locale.setNumberOptions(locale.NumberOption.RejectGroupSeparator)
validator = QDoubleValidator()
validator.setLocale(locale)
self.numeric_value_widget = le = gui.lineEdit(
None, self, "default_numeric",
validator=validator, alignment=Qt.AlignRight,
callback=self._invalidate,
enabled=self.default_method_index == Method.Default
self.numeric_value_widget = DoubleSpinBox(
minimum=DBL_MIN, maximum=DBL_MAX, singleStep=.1,
value=self.default_numeric_value,
alignment=Qt.AlignRight,
enabled=self.default_method_index == Method.Default,
)
hlayout.addWidget(le)
self.numeric_value_widget.editingFinished.connect(
self.__on_default_numeric_value_edited
)
self.connect_control(
"default_numeric_value", self.numeric_value_widget.setValue
)
hlayout.addWidget(self.numeric_value_widget)

hlayout.addWidget(QLabel(", time:"))

Expand Down Expand Up @@ -276,9 +282,9 @@ def set_default_time(datetime):
sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength,
activated=self._on_value_selected
)
self.value_double = QDoubleSpinBox(
self.value_double = DoubleSpinBox(
editingFinished=self._on_value_selected,
minimum=-1000., maximum=1000., singleStep=.1, decimals=3,
minimum=DBL_MIN, maximum=DBL_MAX, singleStep=.1,
)
self.value_stack = value_stack = QStackedWidget()
value_stack.addWidget(self.value_combo)
Expand Down Expand Up @@ -323,15 +329,9 @@ def create_imputer(self, method, *args):
m.method = default
return m
elif method == Method.Default and not args: # global default values
if self.default_numeric == "":
default_num = np.nan
else:
default_num, ok = QLocale().toDouble(self.default_numeric)
if not ok:
default_num = np.nan
return impute.FixedValueByType(
default_continuous=default_num,
default_time=self.default_time or np.nan
default_continuous=self.default_numeric_value,
default_time=self.default_time
)
else:
return METHODS[method](*args)
Expand All @@ -357,6 +357,13 @@ def set_default_method(self, index):
"""
self.default_method_index = index

def __on_default_numeric_value_edited(self):
val = self.numeric_value_widget.value()
if val != self.default_numeric_value:
self.default_numeric_value = val
if self.default_method_index == Method.Default:
self._invalidate()

@Inputs.data
@check_sql_input
def set_data(self, data):
Expand Down
8 changes: 6 additions & 2 deletions Orange/widgets/data/tests/test_owimpute.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from unittest.mock import Mock
import numpy as np

from AnyQt.QtCore import Qt, QItemSelection, QLocale
from AnyQt.QtCore import Qt, QItemSelection
from AnyQt.QtTest import QTest

from Orange.data import Table, Domain, ContinuousVariable, TimeVariable
Expand Down Expand Up @@ -132,7 +132,7 @@ def test_overall_default(self):
data = Table(domain, x, np.empty((2, 0)))

widget = self.widget
widget.default_numeric = QLocale().toString(3.14)
widget.default_numeric_value = 3.14
widget.default_time = 42
widget.default_method_index = Method.Default

Expand All @@ -144,6 +144,10 @@ def test_overall_default(self):
[2, 3.14, 1, 42, 2000, 2000]]
)

widget.numeric_value_widget.setValue(100)
QTest.keyClick(widget.numeric_value_widget, Qt.Key_Enter)
self.assertEqual(widget.default_numeric_value, 100)

def test_value_edit(self):
data = Table("heart_disease")[::10]
self.send_signal(self.widget.Inputs.data, data)
Expand Down
109 changes: 109 additions & 0 deletions Orange/widgets/utils/spinbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import math
from decimal import Decimal

import numpy as np

from AnyQt.QtCore import QLocale
from AnyQt.QtWidgets import QDoubleSpinBox

DBL_MIN = float(np.finfo(float).min)
DBL_MAX = float(np.finfo(float).max)
DBL_MAX_10_EXP = math.floor(math.log10(DBL_MAX))
DBL_DIG = math.floor(math.log10(2 ** np.finfo(float).nmant))


class DoubleSpinBox(QDoubleSpinBox):
"""
A QDoubleSpinSubclass with non-fixed decimal precision/rounding.
"""
def __init__(self, parent=None, decimals=-1, minimumStep=1e-5, **kwargs):
self.__decimals = decimals
self.__minimumStep = minimumStep
stepType = kwargs.pop("stepType", DoubleSpinBox.DefaultStepType)
super().__init__(parent, **kwargs)
if decimals < 0:
super().setDecimals(DBL_MAX_10_EXP + DBL_DIG)
else:
super().setDecimals(decimals)
self.setStepType(stepType)

def setDecimals(self, prec: int) -> None:
"""
Set the number of decimals in display/edit

If negative value then no rounding takes place and the value is
displayed using `QLocale.FloatingPointShortest` precision.
"""
self.__decimals = prec
if prec < 0:
# disable rounding in base implementation.
super().setDecimals(DBL_MAX_10_EXP + DBL_DIG)
else:
super().setDecimals(prec)

def decimals(self):
return self.__decimals

def setMinimumStep(self, step):
"""
Minimum step size when `stepType() == AdaptiveDecimalStepType`
and `decimals() < 0`.
"""
self.__minimumStep = step

def minimumStep(self):
return self.__minimumStep

def textFromValue(self, v: float) -> str:
"""Reimplemented."""
if self.__decimals < 0:
locale = self.locale()
return locale.toString(v, 'f', QLocale.FloatingPointShortest)
else:
return super().textFromValue(v)

def stepBy(self, steps: int) -> None:
"""
Reimplemented.
"""
# Compute up/down step using decimal type without rounding
value = self.value()
value_dec = Decimal(str(value))
if self.stepType() == DoubleSpinBox.AdaptiveDecimalStepType:
step_dec = self.__adaptiveDecimalStep(steps)
else:
step_dec = Decimal(str(self.singleStep()))
# print(str(step_dec.fma(steps, value_dec)))
value_dec = value_dec + step_dec * steps
# print(str(value), "+", str(step_dec), "*", steps, "=", str(vinc))
self.setValue(float(value_dec))

def __adaptiveDecimalStep(self, steps: int) -> Decimal:
# adapted from QDoubleSpinBoxPrivate::calculateAdaptiveDecimalStep
decValue: Decimal = Decimal(str(self.value()))
decimals = self.__decimals
if decimals < 0:
minStep = Decimal(str(self.__minimumStep))
else:
minStep = Decimal(10) ** -decimals

absValue = abs(decValue)
if absValue < minStep:
return minStep
valueNegative = decValue < 0
stepsNegative = steps < 0
if valueNegative != stepsNegative:
absValue /= Decimal("1.01")
step = Decimal(10) ** (math.floor(absValue.log10()) - 1)
return max(minStep, step)

if not hasattr(QDoubleSpinBox, "stepType"): # pragma: no cover
DefaultStepType = 0
AdaptiveDecimalStepType = 1
__stepType = AdaptiveDecimalStepType

def setStepType(self, stepType):
self.__stepType = stepType

def stepType(self):
return self.__stepType
45 changes: 45 additions & 0 deletions Orange/widgets/utils/tests/test_spinbox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from orangewidget.tests.base import GuiTest
from Orange.widgets.utils.spinbox import DoubleSpinBox


class TestDoubleSpinBox(GuiTest):
def test_double_spin_box(self):
w = DoubleSpinBox(
minimum=-1, maximum=1, value=0, singleStep=0.1, decimals=-1,
minimumStep=1e-7,
)
self.assertEqual(w.minimum(), -1)
self.assertEqual(w.maximum(), 1)
self.assertEqual(w.value(), 0)
self.assertEqual(w.singleStep(), 0.1)
self.assertEqual(w.decimals(), -1)
self.assertEqual(w.minimumStep(), 1e-7)

w.setValue(2)
self.assertEqual(w.value(), 1)
w.setValue(0.999999)
self.assertEqual(w.value(), 0.999999)
w.stepBy(-1)
self.assertEqual(w.value(), 0.899999)
w.stepBy(1)
self.assertEqual(w.value(), 0.999999)
w.stepBy(1)
self.assertEqual(w.value(), 1.0)

w.setStepType(DoubleSpinBox.AdaptiveDecimalStepType)
w.stepBy(-1)
self.assertEqual(w.value(), 0.99)

w.setValue(0.123456789)
w.stepBy(1)
self.assertEqual(w.value(), 0.133456789)
w.stepBy(-1)
self.assertEqual(w.value(), 0.123456789)
w.setMinimumStep(0.001)
w.setValue(0.00005)
w.stepBy(1)
w.setValue(0.00105)
w.setDecimals(3)
self.assertEqual(w.value(), 0.001)
w.stepBy(1)
self.assertEqual(w.value(), 0.002)