Repository: biolab/orange3

Issue:
Error reading column header including char '#' in Excel .xlsx file
<!-- 
Thanks for taking the time to report a bug!
If you're raising an issue about an add-on (i.e., installed via Options > Add-ons), raise an issue in the relevant add-on's issue tracker instead. See: https://github.com/biolab?q=orange3
To fix the bug, we need to be able to reproduce it. Please answer the following questions to the best of your ability.
-->


**What's wrong?**
<!-- Be specific, clear, and concise. Include screenshots if relevant. -->
<!-- If you're getting an error message, copy it, and enclose it with three backticks (```). -->
If a column header is 'abc #1' in .xlsx file, File widget loads the file and shows the header as '1'.




**How can we reproduce the problem?** 
<!-- Upload a zip with the .ows file and data. -->
<!-- Describe the steps (open this widget, click there, then add this...) -->
<img width="128" alt="xlsx" src="https://github.com/biolab/orange3/assets/43062047/537943f8-1738-4a10-886b-b3bc97f7537f">
<img width="234" alt="file widget" src="https://github.com/biolab/orange3/assets/43062047/346b2dc4-b021-484e-903b-4e44158d0dd5">





**What's your environment?**
<!-- To find your Orange version, see "Help → About → Version" or `Orange.version.full_version` in code -->
- Operating system: Win 10
- Orange version: 3.35.0
- How you installed Orange: conda



Discussion:

Resolving patch:
diff --git a/Orange/data/io_base.py b/Orange/data/io_base.py
--- a/Orange/data/io_base.py
+++ b/Orange/data/io_base.py
@@ -48,7 +48,7 @@ class Flags:
         ('weight', 'w'),
         ('.+?=.*?', ''),  # general key=value attributes
     ))
-    _RE_ALL = re.compile(r'^({})$'.format('|'.join(
+    RE_ALL = re.compile(r'^({})$'.format('|'.join(
         filter(None, flatten(ALL.items())))))
 
     def __init__(self, flags):
@@ -57,7 +57,7 @@ def __init__(self, flags):
         self.attributes = {}
         for flag in flags or []:
             flag = flag.strip()
-            if self._RE_ALL.match(flag):
+            if self.RE_ALL.match(flag):
                 if '=' in flag:
                     k, v = flag.split('=', 1)
                     if not Flags._RE_ATTR_UNQUOTED_STR(v):
@@ -167,8 +167,15 @@ def _header1(cls, headers: List[List[str]]) -> Tuple[List, List, List]:
           2) -||- with type and flags prepended, separated by #,
              e.g. d#sex,c#age,cC#IQ
         """
+
+        def is_flag(x):
+            return bool(Flags.RE_ALL.match(cls._type_from_flag([x])[0]) or
+                        Flags.RE_ALL.match(cls._flag_from_flag([x])[0]))
+
         flags, names = zip(*[i.split(cls.HEADER1_FLAG_SEP, 1)
-                             if cls.HEADER1_FLAG_SEP in i else ('', i)
+                             if cls.HEADER1_FLAG_SEP in i and
+                             is_flag(i.split(cls.HEADER1_FLAG_SEP)[0])
+                             else ('', i)
                              for i in headers[0]])
         names = list(names)
         return names, cls._type_from_flag(flags), cls._flag_from_flag(flags)


Test patch:
diff --git a/Orange/tests/test_xlsx_reader.py b/Orange/tests/test_xlsx_reader.py
--- a/Orange/tests/test_xlsx_reader.py
+++ b/Orange/tests/test_xlsx_reader.py
@@ -143,6 +143,25 @@ def test_no_flags(self, reader: Callable[[str], io.FileFormat]):
                                                  [0, 0, np.nan, 0]]))
         np.testing.assert_equal(table.Y, np.array([]).reshape(3, 0))
 
+    def test_hash(self):
+        table = Table.from_file(get_dataset("header_1_hash.xlsx"))
+        domain = table.domain
+        self.assertEqual(len(domain.metas), 0)
+        self.assertEqual(len(domain.attributes), 3)
+        self.assertEqual(len(domain.class_vars), 1)
+        self.assertIsInstance(domain[0], DiscreteVariable)
+        self.assertIsInstance(domain[1], ContinuousVariable)
+        self.assertIsInstance(domain[2], ContinuousVariable)
+        self.assertIsInstance(domain[3], DiscreteVariable)
+        self.assertEqual([v.name for v in domain.variables],
+                         ["#", "b#", "d", "Feature 1"])
+        self.assertEqual(domain[0].values, ("green", "red"))
+        np.testing.assert_almost_equal(table.X, np.array([[1, 0.5, 21],
+                                                          [1, 0.1, 123],
+                                                          [0, 0, 0]]))
+        np.testing.assert_equal(table.Y, [0, 0, np.nan])
+
+
     @test_xlsx_xls
     def test_flags(self, reader: Callable[[str], io.FileFormat]):
         table = read_file(reader, "header_1_flags")
diff --git a/Orange/tests/xlsx_files/header_1_hash.xlsx b/Orange/tests/xlsx_files/header_1_hash.xlsx
new file mode 100644
Binary files /dev/null and b/Orange/tests/xlsx_files/header_1_hash.xlsx differ