Repository: MycroftAI/lingua-franca

Issue:
Fails with 0.0x numbers
```
$ python3
Python 3.7.6 (default, Dec 19 2019, 22:52:49) 
[GCC 9.2.1 20190827 (Red Hat 9.2.1-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
@>>> from lingua_franca.format import pronounce_number
@>>> pronounce_number(0.05)
' point'
@>>> pronounce_number(0.05, places=1)
' point'
@>>> pronounce_number(0.05, places=0)
''
```

This causes trouble in the [travel-time-skill](https://github.com/TheLastProject/travel-time-skill) for short distances:
```
        self.speak_dialog(
            dialog,
            {
                'time': time,
                'distance': util.format.pronounce_number(distance, places=0),
                'from': from_,
                'destination': destination
            }
        )
```

Given `distance = 0.05` and the following dialog options:
```
It is {{distance}} kilometers and {{time}} from {{from}} to {{destination}} by car
```

Output will be as follows:
```
It is kilometers and under a minute from A to B by car
```

Discussion:

Patch:
diff --git a/lingua_franca/lang/format_en.py b/lingua_franca/lang/format_en.py
index 5634a95..bba9bc1 100644
--- a/lingua_franca/lang/format_en.py
+++ b/lingua_franca/lang/format_en.py
@@ -97,19 +97,20 @@ def pronounce_number_en(num, places=2, short_scale=True, scientific=False,
                 # handling since each call disables the scientific flag
                 return '{}{} times ten to the {}{} power'.format(
                     'negative ' if float(n) < 0 else '',
-                    pronounce_number_en(abs(float(n)), places, short_scale, False, ordinals=False),
+                    pronounce_number_en(
+                        abs(float(n)), places, short_scale, False, ordinals=False),
                     'negative ' if power < 0 else '',
-                    pronounce_number_en(abs(power), places, short_scale, False,ordinals=True))
+                    pronounce_number_en(abs(power), places, short_scale, False, ordinals=True))
             else:
                 # This handles negatives of powers separately from the normal
                 # handling since each call disables the scientific flag
                 return '{}{} times ten to the power of {}{}'.format(
                     'negative ' if float(n) < 0 else '',
-                    pronounce_number_en(abs(float(n)), places, short_scale, False),
+                    pronounce_number_en(
+                        abs(float(n)), places, short_scale, False),
                     'negative ' if power < 0 else '',
                     pronounce_number_en(abs(power), places, short_scale, False))
 
-
     if short_scale:
         number_names = _NUM_STRING_EN.copy()
         number_names.update(_SHORT_SCALE_EN)
@@ -289,12 +290,13 @@ def pronounce_number_en(num, places=2, short_scale=True, scientific=False,
         return pronounce_number_en(num, places, short_scale, scientific=True)
     # Deal with fractional part
     elif not num == int(num) and places > 0:
+        if abs(num) < 1.0 and (result is "minus " or not result):
+            result += "zero"
         result += " point"
-        place = 10
-        while int(num * place) % 10 > 0 and places > 0:
-            result += " " + number_names[int(num * place) % 10]
-            place *= 10
-            places -= 1
+        _num_str = str(num)
+        _num_str = _num_str.split(".")[1][0:places]
+        for char in _num_str:
+            result += " " + number_names[int(char)]
     return result
 
 
diff --git a/lingua_franca/lang/format_es.py b/lingua_franca/lang/format_es.py
index a77cf5f..6a4abf8 100644
--- a/lingua_franca/lang/format_es.py
+++ b/lingua_franca/lang/format_es.py
@@ -194,12 +194,13 @@ def pronounce_number_es(num, places=2):
     # instead the dot. Decimal part can be written both with comma
     # and dot, but when pronounced, its pronounced "coma"
     if not num == int(num) and places > 0:
+        if abs(num) < 1.0 and (result is "menos " or not result):
+            result += "cero"
         result += " coma"
-        place = 10
-        while int(num*place) % 10 > 0 and places > 0:
-            result += " " + NUM_STRING_ES[int(num*place) % 10]
-            place *= 10
-            places -= 1
+        _num_str = str(num)
+        _num_str = _num_str.split(".")[1][0:places]
+        for char in _num_str:
+            result += " " + NUM_STRING_ES[int(char)]
     return result
 
 
diff --git a/lingua_franca/lang/format_fr.py b/lingua_franca/lang/format_fr.py
index 698b317..7b94704 100644
--- a/lingua_franca/lang/format_fr.py
+++ b/lingua_franca/lang/format_fr.py
@@ -192,12 +192,13 @@ def pronounce_number_fr(num, places=2):
 
     # Deal with decimal part
     if not num == int(num) and places > 0:
+        if abs(num) < 1.0 and (result is "moins " or not result):
+            result += "zéro"
         result += " virgule"
-        place = 10
-        while int(num*place) % 10 > 0 and places > 0:
-            result += " " + NUM_STRING_FR[int(num*place) % 10]
-            place *= 10
-            places -= 1
+        _num_str = str(num)
+        _num_str = _num_str.split(".")[1][0:places]
+        for char in _num_str:
+            result += " " + NUM_STRING_FR[int(char)]
     return result
 
 
diff --git a/lingua_franca/lang/format_it.py b/lingua_franca/lang/format_it.py
index 82788ec..3fd3224 100644
--- a/lingua_franca/lang/format_it.py
+++ b/lingua_franca/lang/format_it.py
@@ -384,12 +384,13 @@ def pronounce_number_it(num, places=2, short_scale=False, scientific=False):
 
     # Deal with fractional part
     if not num == int(num) and places > 0:
+        if abs(num) < 1.0 and (result is "meno " or not result):
+            result += "zero"
         result += " virgola"
-        place = 10
-        while int(num * place) % 10 > 0 and places > 0:
-            result += " " + number_names[int(num * place) % 10]
-            place *= 10
-            places -= 1
+        _num_str = str(num)
+        _num_str = _num_str.split(".")[1][0:places]
+        for char in _num_str:
+            result += " " + number_names[int(char)]
     return result
 
 
diff --git a/lingua_franca/lang/format_pt.py b/lingua_franca/lang/format_pt.py
index ccf06c4..49d91b5 100644
--- a/lingua_franca/lang/format_pt.py
+++ b/lingua_franca/lang/format_pt.py
@@ -105,12 +105,13 @@ def pronounce_number_pt(num, places=2):
     # instead the dot. Decimal part can be written both with comma
     # and dot, but when pronounced, its pronounced "virgula"
     if not num == int(num) and places > 0:
+        if abs(num) < 1.0 and (result is "menos " or not result):
+            result += "zero"
         result += " vírgula"
-        place = 10
-        while int(num * place) % 10 > 0 and places > 0:
-            result += " " + _NUM_STRING_PT[int(num * place) % 10]
-            place *= 10
-            places -= 1
+        _num_str = str(num)
+        _num_str = _num_str.split(".")[1][0:places]
+        for char in _num_str:
+            result += " " + _NUM_STRING_PT[int(char)]
     return result
 
 
diff --git a/lingua_franca/lang/parse_da.py b/lingua_franca/lang/parse_da.py
index 3008e27..405a7f6 100644
--- a/lingua_franca/lang/parse_da.py
+++ b/lingua_franca/lang/parse_da.py
@@ -72,8 +72,10 @@ da_numbers = {
     'million': 1000000
 }
 
-
-def extractnumber_da(text):
+# TODO: short_scale and ordinals don't do anything here.
+# The parameters are present in the function signature for API compatibility
+# reasons.
+def extractnumber_da(text, short_scale=True, ordinals=False):
     """
     This function prepares the given text for parsing by making
     numbers consistent, getting rid of contractions, etc.
@@ -151,10 +153,7 @@ def extractnumber_da(text):
 
         break
 
-    if not val:
-        return False
-
-    return val
+    return val or False
 
 
 def extract_datetime_da(string, currentDate, default_time):
diff --git a/lingua_franca/lang/parse_de.py b/lingua_franca/lang/parse_de.py
index b735f33..453df69 100644
--- a/lingua_franca/lang/parse_de.py
+++ b/lingua_franca/lang/parse_de.py
@@ -77,8 +77,10 @@ de_numbers = {
     'million': 1000000
 }
 
-
-def extractnumber_de(text):
+# TODO: short_scale and ordinals don't do anything here.
+# The parameters are present in the function signature for API compatibility
+# reasons.
+def extractnumber_de(text, short_scale=True, ordinals=False):
     """
     This function prepares the given text for parsing by making
     numbers consistent, getting rid of contractions, etc.
@@ -156,10 +158,7 @@ def extractnumber_de(text):
 
         break
 
-    if not val:
-        return False
-
-    return val
+    return val or False
 
 
 def extract_datetime_de(string, currentDate, default_time):
diff --git a/lingua_franca/lang/parse_es.py b/lingua_franca/lang/parse_es.py
index ab81e2e..bbbdb0a 100644
--- a/lingua_franca/lang/parse_es.py
+++ b/lingua_franca/lang/parse_es.py
@@ -177,9 +177,6 @@ def extractnumber_es(text, short_scale=True, ordinals=False):
             break
         count += 1
 
-    if result is None:
-        return False
-
     # Return the $str with the number related words removed
     # (now empty strings, so strlen == 0)
     # aWords = [word for word in aWords if len(word) > 0]
@@ -190,7 +187,7 @@ def extractnumber_es(text, short_scale=True, ordinals=False):
         if dec == "0":
             result = int(integer)
 
-    return result
+    return result or False
 
 
 # TODO Not parsing 'cero'
diff --git a/lingua_franca/lang/parse_fr.py b/lingua_franca/lang/parse_fr.py
index b3be404..d2ea2ca 100644
--- a/lingua_franca/lang/parse_fr.py
+++ b/lingua_franca/lang/parse_fr.py
@@ -370,8 +370,10 @@ def number_ordinal_fr(words, i):
 
     return None
 
-
-def extractnumber_fr(text):
+# TODO: short_scale and ordinals don't do anything here.
+# The parameters are present in the function signature for API compatibility
+# reasons.
+def extractnumber_fr(text, short_scale=True, ordinals=False):
     """Takes in a string and extracts a number.
     Args:
         text (str): the string to extract a number from
@@ -465,11 +467,7 @@ def extractnumber_fr(text):
             else:
                 result = val
 
-    # if result == False:
-    if not result:
-        return normalize_fr(text, True)
-
-    return result
+    return result or False
 
 
 def extract_datetime_fr(string, currentDate, default_time):
diff --git a/lingua_franca/lang/parse_pt.py b/lingua_franca/lang/parse_pt.py
index e6cc662..62cffeb 100644
--- a/lingua_franca/lang/parse_pt.py
+++ b/lingua_franca/lang/parse_pt.py
@@ -63,8 +63,10 @@ def isFractional_pt(input_str):
 
     return False
 
-
-def extractnumber_pt(text):
+# TODO: short_scale and ordinals don't do anything here.
+# The parameters are present in the function signature for API compatibility
+# reasons.
+def extractnumber_pt(text, short_scale=True, ordinals=False):
     """
     This function prepares the given text for parsing by making
     numbers consistent, getting rid of contractions, etc.
@@ -179,9 +181,6 @@ def extractnumber_pt(text):
             break
         count += 1
 
-    if result is None:
-        return False
-
     # Return the $str with the number related words removed
     # (now empty strings, so strlen == 0)
     # aWords = [word for word in aWords if len(word) > 0]
@@ -192,7 +191,7 @@ def extractnumber_pt(text):
         if dec == "0":
             result = int(integer)
 
-    return result
+    return result or False
 
 
 class PortugueseNormalizer(Normalizer):
diff --git a/lingua_franca/lang/parse_sv.py b/lingua_franca/lang/parse_sv.py
index 30c2f08..9efe78d 100644
--- a/lingua_franca/lang/parse_sv.py
+++ b/lingua_franca/lang/parse_sv.py
@@ -17,8 +17,10 @@ from datetime import datetime
 from dateutil.relativedelta import relativedelta
 from .parse_common import is_numeric, look_for_fractions
 
-
-def extractnumber_sv(text):
+# TODO: short_scale and ordinals don't do anything here.
+# The parameters are present in the function signature for API compatibility
+# reasons.
+def extractnumber_sv(text, short_scale=True, ordinals=False):
     """
     This function prepares the given text for parsing by making
     numbers consistent, getting rid of contractions, etc.
@@ -117,10 +119,7 @@ def extractnumber_sv(text):
 
         break
 
-    if not val:
-        return False
-
-    return val
+    return val or False
 
 
 def extract_datetime_sv(string, currentDate, default_time):


Test patch:
diff --git a/test/test_format.py b/test/test_format.py
index f40d71c..450263e 100755
--- a/test/test_format.py
+++ b/test/test_format.py
@@ -118,6 +118,8 @@ class TestPronounceNumber(unittest.TestCase):
         self.assertEqual(pronounce_number(-33), "minus thirty three")
 
     def test_convert_decimals(self):
+        self.assertEqual(pronounce_number(0.05), "zero point zero five")
+        self.assertEqual(pronounce_number(-0.05), "minus zero point zero five")
         self.assertEqual(pronounce_number(1.234),
                          "one point two three")
         self.assertEqual(pronounce_number(21.234),
diff --git a/test/test_format_es.py b/test/test_format_es.py
index f81fdc8..641a435 100644
--- a/test/test_format_es.py
+++ b/test/test_format_es.py
@@ -132,6 +132,10 @@ class TestPronounceNumber(unittest.TestCase):
                          "menos noventa y nueve")
 
     def test_convert_decimals(self):
+        self.assertEqual(pronounce_number(
+            0.05, lang="es"), "cero coma cero cinco")
+        self.assertEqual(pronounce_number(
+            -0.05, lang="es"), "menos cero coma cero cinco")
         self.assertEqual(pronounce_number(1.234, lang="es"),
                          "uno coma dos tres")
         self.assertEqual(pronounce_number(21.234, lang="es"),
diff --git a/test/test_format_fr.py b/test/test_format_fr.py
index e762c9d..16d4bda 100644
--- a/test/test_format_fr.py
+++ b/test/test_format_fr.py
@@ -135,6 +135,10 @@ class TestPronounceNumber_fr(unittest.TestCase):
                          "moins trente-trois")
 
     def test_convert_decimals_fr(self):
+        self.assertEqual(pronounce_number(0.05, lang="fr-fr"),
+                         "zéro virgule zéro cinq")
+        self.assertEqual(pronounce_number(-0.05, lang="fr-fr"),
+                         "moins zéro virgule zéro cinq")
         self.assertEqual(pronounce_number(1.234, lang="fr-fr"),
                          "un virgule deux trois")
         self.assertEqual(pronounce_number(21.234, lang="fr-fr"),
diff --git a/test/test_format_it.py b/test/test_format_it.py
index 93e13bb..1ae5745 100644
--- a/test/test_format_it.py
+++ b/test/test_format_it.py
@@ -105,6 +105,10 @@ class TestPronounceNumber(unittest.TestCase):
         self.assertEqual(pronounce_number(-83, lang="it"), "meno ottantatre")
 
     def test_convert_decimals(self):
+        self.assertEqual(pronounce_number(
+            0.05, lang="it"), "zero virgola zero cinque")
+        self.assertEqual(pronounce_number(
+            -0.05, lang="it"), "meno zero virgola zero cinque")
         self.assertEqual(pronounce_number(1.234, lang="it"),
                          "uno virgola due tre")
         self.assertEqual(pronounce_number(21.234, lang="it"),
@@ -352,5 +356,6 @@ class TestPronounceNumber(unittest.TestCase):
         self.assertEqual(pronounce_number(float("-inf"),
                                           lang="it"), "meno infinito")
 
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/test/test_format_pt.py b/test/test_format_pt.py
index f49be66..edd99fe 100644
--- a/test/test_format_pt.py
+++ b/test/test_format_pt.py
@@ -87,6 +87,10 @@ class TestPronounceNumber(unittest.TestCase):
                          "menos noventa e nove")
 
     def test_convert_decimals(self):
+        self.assertEqual(pronounce_number(
+            0.05, lang="pt"), "zero vírgula zero cinco")
+        self.assertEqual(pronounce_number(
+            -0.05, lang="pt"), "menos zero vírgula zero cinco")
         self.assertEqual(pronounce_number(1.234, lang="pt"),
                          "um vírgula dois três")
         self.assertEqual(pronounce_number(21.234, lang="pt"),
diff --git a/test/test_parse_fr.py b/test/test_parse_fr.py
index ee19fe2..86d26cd 100644
--- a/test/test_parse_fr.py
+++ b/test/test_parse_fr.py
@@ -71,8 +71,7 @@ class TestNormalize_fr(unittest.TestCase):
                          2.02)
         self.assertEqual(extract_number("ça fait virgule 2 cm", lang="fr-fr"),
                          0.2)
-        self.assertEqual(extract_number("point du tout", lang="fr-fr"),
-                         "point tout")
+        self.assertEqual(extract_number("point du tout", lang="fr-fr"), False)
         self.assertEqual(extract_number("32.00 secondes", lang="fr-fr"), 32)
         self.assertEqual(extract_number("mange trente-et-une bougies",
                                         lang="fr-fr"), 31)