Repository: biolab/orange3

Issue:
File widget: change attribute type from nominal to numeric
##### Orange version
3.4.4.

##### Expected behavior
If attribute type change from nominal to numeric, the number values shall remain the same. E.g. if there were two nominal values 1 and 2, the same two values should remain after the attribute type change.

##### Actual behavior
If attribute type change from nominal to numeric, the number representation change. E.g. if there were two nominal values 1 and 2, after the attribute type change we have 0 and 1. This happens only if values are 1 and 2, if the values are 22 and 33 (or other) this does not happen.

##### Steps to reproduce the behavior
open attached data sheet (test.xlsx) with file widget
change attribute type for '2nd-code' from nominal to numeric
see '2nd-code' values in the data table widget


##### Additional info (worksheets, data, screenshots, ...)
data file

[test.xlsx](https://github.com/biolab/orange3/files/1095164/test.xlsx)

before attribute type change

![one](https://user-images.githubusercontent.com/11367976/27440205-505fe4fc-576a-11e7-96bd-bdb9c872e391.png)

after attribute type change

![nominal-numeric](https://user-images.githubusercontent.com/11367976/27440216-5952c048-576a-11e7-965e-9536b97517af.png)






Discussion:

Resolving patch:
diff --git a/Orange/widgets/utils/domaineditor.py b/Orange/widgets/utils/domaineditor.py
--- a/Orange/widgets/utils/domaineditor.py
+++ b/Orange/widgets/utils/domaineditor.py
@@ -241,6 +241,9 @@ def get_domain(self, domain, data):
         -------
         (new_domain, [attribute_columns, class_var_columns, meta_columns])
         """
+        # Allow type-checking with type() instead of isinstance() for exact comparison
+        # pylint: disable=unidiomatic-typecheck
+
         variables = self.model().variables
         places = [[], [], []]  # attributes, class_vars, metas
         cols = [[], [], []]  # Xcols, Ycols, Mcols
@@ -255,6 +258,12 @@ def get_domain(self, domain, data):
 
             col_data = self._get_column(data, orig_var, orig_plc)
             is_sparse = sp.issparse(col_data)
+
+            cont_ints = type(orig_var) == ContinuousVariable and \
+                        all(x.is_integer() for x in self._iter_vals(col_data) if not np.isnan(x))
+            disc_ints = type(orig_var) == DiscreteVariable and \
+                        all(x.isdecimal() for x in orig_var.values)
+
             if name == orig_var.name and tpe == type(orig_var):
                 var = orig_var
             elif tpe == type(orig_var):
@@ -263,17 +272,30 @@ def get_domain(self, domain, data):
                 var = orig_var
             elif tpe == DiscreteVariable:
                 values = list(str(i) for i in unique(col_data) if not self._is_missing(i))
-                var = tpe(name, values)
                 col_data = [np.nan if self._is_missing(x) else values.index(str(x))
                             for x in self._iter_vals(col_data)]
+                if cont_ints:
+                    values = [str(int(float(v))) for v in values]
+                var = tpe(name, values)
                 col_data = self._to_column(col_data, is_sparse)
-            elif tpe == StringVariable and type(orig_var) == DiscreteVariable:
+            elif tpe == StringVariable:
                 var = tpe(name)
-                col_data = [orig_var.repr_val(x) if not np.isnan(x) else ""
-                            for x in self._iter_vals(col_data)]
+                if type(orig_var) == DiscreteVariable:
+                    col_data = [orig_var.repr_val(x) if not np.isnan(x) else ""
+                                for x in self._iter_vals(col_data)]
+                elif type(orig_var) == ContinuousVariable:
+                    col_data = [str(int(x)) if cont_ints else orig_var.repr_val(x)
+                                if not np.isnan(x) else ""
+                                for x in self._iter_vals(col_data)]
                 # don't obey sparsity for StringVariable since they are
                 # in metas which are transformed to dense below
                 col_data = self._to_column(col_data, False, dtype=object)
+            elif tpe == ContinuousVariable and type(orig_var) == DiscreteVariable:
+                var = tpe(name)
+                if disc_ints:
+                    col_data = [np.nan if self._is_missing(x) else float(orig_var.values[int(x)])
+                                for x in self._iter_vals(col_data)]
+                col_data = self._to_column(col_data, is_sparse)
             else:
                 var = tpe(name)
             places[place].append(var)


Test patch:
diff --git a/Orange/widgets/data/tests/test_owfile.py b/Orange/widgets/data/tests/test_owfile.py
--- a/Orange/widgets/data/tests/test_owfile.py
+++ b/Orange/widgets/data/tests/test_owfile.py
@@ -2,6 +2,7 @@
 # pylint: disable=missing-docstring
 from os import path, remove
 from unittest.mock import Mock
+from unittest import TestCase
 import pickle
 import tempfile
 
@@ -245,3 +246,31 @@ def test_add_new_format(self):
         # test adding file formats after registering the widget
         formats = dialog_formats()
         self.assertTrue(".123" in formats)
+
+    def test_domain_editor_conversions(self):
+        dat = """V0\tV1\tV2\tV3\tV4\tV5
+                 c\tc\td\td\tc\td
+                  \t \t \t \t \t
+                 3.0\t1.0\t4\ta\t0.0\tx
+                 1.0\t2.0\t4\tb\t0.0\ty
+                 2.0\t1.0\t7\ta\t0.0\ty
+                 0.0\t2.0\t7\ta\t0.0\tz"""
+        with named_file(dat, suffix=".tab") as filename:
+            self.open_dataset(filename)
+            data1 = self.get_output(self.widget.Outputs.data)
+            model = self.widget.domain_editor.model()
+            # check the ordering of attributes
+            for i, a in enumerate(data1.domain.attributes):
+                self.assertEqual(str(a), model.data(model.createIndex(i, 0), Qt.DisplayRole))
+            # make conversions
+            model.setData(model.createIndex(0, 1), "categorical", Qt.EditRole)
+            model.setData(model.createIndex(1, 1), "string", Qt.EditRole)
+            model.setData(model.createIndex(2, 1), "numeric", Qt.EditRole)
+            model.setData(model.createIndex(3, 1), "numeric", Qt.EditRole)
+            self.widget.apply_button.click()
+            data2 = self.get_output(self.widget.Outputs.data)
+            # round continuous values should be converted to integers (3.0 -> 3, "3")
+            self.assertEqual(len(data2.domain.attributes[0].values[0]), 1)
+            self.assertEqual(len(data2[0].metas[0]), 1)
+            # discrete integer values should stay the same after conversion to continuous
+            self.assertAlmostEqual(float(data1[0][2].value), data2[0][1])