Repository: allo-media/text2num

Issue:
text2num should raise ValueError instead of AssertionError
From the docs
```py
>>> text2num('mille mille deux cents')
AssertionError
```

First, an error message would be helpful to know what went wrong.

Second, the use of `AssertionError` here is a bad choice. An assertion error should only be raised if the program is internally inconsistent (has a bug). It should not be possible to cause an assertion error by supplying bad input (unless the program _is_ internally inconsistent, and therefore is indicating it has a bug).

I think `ValueError` would be a better choice in this case since the user supplied a bad value. This is the same as passing a bad value to the built in `int` function:
```py
>>> int("not a number")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'not a number'
```
`text2num` can't do anything about bad values passed to it other than raise an error indicating that it got a bad value.


Discussion:

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index f4de54e..7dfbf33 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -23,6 +23,7 @@ jobs:
             python3 -m venv venv
             . venv/bin/activate
             pip install sphinx
+            pip install flake8
             python setup.py develop
 
       - save_cache:
@@ -37,6 +38,7 @@ jobs:
           command: |
             . venv/bin/activate
             python setup.py test
+            flake8 --max-line-length=110 text_to_num
 
       # Build the documentation
       - run:
diff --git a/README.rst b/README.rst
index 84edb04..81fd418 100644
--- a/README.rst
+++ b/README.rst
@@ -6,8 +6,8 @@ text2num
 
 ``text2num`` is a python package that provides functions and parser classes for:
 
-- parsing numbers expressed as words in french and convert them to integer values;
-- detect ordinal, cardinal and decimal numbers in a stream of french words and get their decimal digit representations.
+- parsing numbers expressed as words in French and convert them to integer values;
+- detect ordinal, cardinal and decimal numbers in a stream of French words and get their decimal digit representations.
 
 Compatibility
 -------------
@@ -39,17 +39,23 @@ Parse and convert
 .. code-block:: python
 
     >>> from text_to_num import text2num
+    >>> text2num('quatre-vingt-quinze')
+    95
+
     >>> text2num('nonante-cinq')
     95
 
     >>> text2num('mille neuf cent quatre-vingt dix-neuf')
     1999
 
+    >>> text2num('dix-neuf cent quatre-vingt dix-neuf')
+    1999
+
     >>> text2num("cinquante et un million cinq cent soixante dix-huit mille trois cent deux")
     51578302
 
     >>> text2num('mille mille deux cents')
-    AssertionError
+    ValueError: invalid literal for text2num: 'mille mille deux cent'
 
 
 Find and transcribe
diff --git a/doc/index.rst b/doc/index.rst
index da4a1f2..e169bdb 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -8,8 +8,8 @@ Welcome to text2num's documentation!
 
 ``text2num`` is a package that provides functions and parser classes for:
 
-- parsing numbers expressed as words in french and convert them to integer values;
-- detect ordinals, cardinals and decimal numbers in a stream of french words and get their decimal digit representations.
+- parsing numbers expressed as words in French and convert them to integer values;
+- detect ordinals, cardinals and decimal numbers in a stream of French words and get their decimal digit representations.
 
 ``text2num`` is distributed under the MIT license and is known to work on python version 3.6 and above.
 
diff --git a/doc/quickstart.rst b/doc/quickstart.rst
index 570b9ab..1216c80 100644
--- a/doc/quickstart.rst
+++ b/doc/quickstart.rst
@@ -23,17 +23,23 @@ Integers only.
 .. code-block:: python
 
     >>> from text_to_num import text2num
+    >>> text2num('quatre-vingt-quinze')
+    95
+
     >>> text2num('nonante-cinq')
     95
 
     >>> text2num('mille neuf cent quatre-vingt dix-neuf')
     1999
 
+    >>> text2num('dix-neuf cent quatre-vingt dix-neuf')
+    1999
+
     >>> text2num("cinquante et un million cinq cent soixante dix-huit mille trois cent deux")
     51578302
 
     >>> text2num('mille mille deux cents')
-    AssertionError
+    ValueError: invalid literal for text2num: 'mille mille deux cent'
 
 
 Find and transcribe
diff --git a/setup.py b/setup.py
index 68a7fd9..7cff48b 100644
--- a/setup.py
+++ b/setup.py
@@ -1,10 +1,6 @@
-import os
-import sys
-
 from setuptools import setup, find_packages
-from setuptools.command.install import install
 
-VERSION = '1.0.0'
+VERSION = '1.0.0.dev1'
 
 
 def readme():
@@ -14,7 +10,7 @@ def readme():
 
 setup(name='text2num',
       version=VERSION,
-      description='Parse and convert numbers written in french into their digit representation.',
+      description='Parse and convert numbers written in French into their digit representation.',
       long_description=readme(),
       classifiers=[
         'Development Status :: 5 - Production/Stable',
@@ -24,7 +20,7 @@ setup(name='text2num',
         'Topic :: Text Processing :: Filters',
         'Natural Language :: French'
       ],
-      keywords='french NLP words-to-numbers',
+      keywords='French NLP words-to-numbers',
       url='https://github.com/allo-media/text2num',
       author='Allo-Media',
       author_email='contact@allo-media.fr',
diff --git a/text_to_num/__init__.py b/text_to_num/__init__.py
index 9aa0bc4..b83bd39 100644
--- a/text_to_num/__init__.py
+++ b/text_to_num/__init__.py
@@ -20,4 +20,4 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-from .transforms import text2num, alpha2digit
+from .transforms import text2num, alpha2digit  # noqa: F401
diff --git a/text_to_num/parsers.py b/text_to_num/parsers.py
index 667333b..d3ffb5f 100644
--- a/text_to_num/parsers.py
+++ b/text_to_num/parsers.py
@@ -1,5 +1,27 @@
+# MIT License
+
+# Copyright (c) 2018 Groupe Allo-Media
+
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
 """
-Convert french spelled numbers into numeric values or digit strings.
+Convert French spelled numbers into numeric values or digit strings.
 """
 
 #
@@ -45,11 +67,12 @@ NUMBERS.update(STENS)
 # Exceptions: "soixante" & "quatre-ving" (see Rules)
 MTENS = {
     word: value * 10
-    for value, word in enumerate("vingt trente quarante cinquante soixante septante octante nonante".split(),
+    for value, word in enumerate("vingt trente quarante cinquante soixante septante huitante nonante".split(),
                                  2)
 }
-# Variant
+# Variants
 MTENS['quatre-vingt'] = 80
+MTENS['octante'] = 80
 
 NUMBERS.update(MTENS)
 
@@ -71,7 +94,7 @@ COMPOSITES.update({
     "-".join((ten_word, et_word)): ten_val + et_val
     for ten_word, ten_val in MTENS.items()
     for et_word, et_val in (('et-un', 1), ('et-une', 1))
-    if 10 < ten_val < 80
+    if 10 < ten_val <= 90
 })
 
 COMPOSITES['quatre-vingt-un'] = 81
@@ -151,7 +174,8 @@ class WordStreamValueParser:
         expected = False
         if self.last_word is None:
             expected = True
-        elif self.last_word in UNITS and self.grp_val < 10:
+        elif (self.last_word in UNITS and self.grp_val < 10 or
+              self.last_word in STENS and self.grp_val < 20):
             expected = word in CENT
         elif self.last_word in MTENS:
             expected = word in UNITS or word in STENS and self.last_word in ("soixante", "quatre-vingt")
diff --git a/text_to_num/transforms.py b/text_to_num/transforms.py
index 39bea74..cfb99d2 100644
--- a/text_to_num/transforms.py
+++ b/text_to_num/transforms.py
@@ -26,7 +26,17 @@ from .parsers import WordStreamValueParser, WordToDigitParser
 
 
 def look_ahead(sequence):
-    """Look-ahead iterator"""
+    """Look-ahead iterator.
+
+    Iterate over a sequence by returning couples (current element, next element).
+    The last couple returned before StopIteration is raised, is (last element, None).
+
+    Example:
+
+    >>> for elt, nxt_elt in look_ahead(sequence):
+    ... # do something
+
+    """
     maxi = len(sequence) - 1
     for i, val in enumerate(sequence):
         ahead = sequence[i + 1] if i < maxi else None
@@ -34,7 +44,7 @@ def look_ahead(sequence):
 
 
 def text2num(text, relaxed=False):
-    """Convert the ``text`` string containing an integer number written in french
+    """Convert the ``text`` string containing an integer number written in French
     into an integer value.
 
     Set ``relaxed`` to True if you want to accept "quatre vingt(s)" as "quatre-vingt".
@@ -45,12 +55,13 @@ def text2num(text, relaxed=False):
 
     num_parser = WordStreamValueParser(relaxed=relaxed)
     tokens = text.split()
-    assert all(num_parser.push(word, ahead) for word, ahead in look_ahead(tokens))
+    if not all(num_parser.push(word, ahead) for word, ahead in look_ahead(tokens)):
+        raise ValueError('invalid literal for text2num: {}'.format(repr(text)))
     return num_parser.value
 
 
 def alpha2digit(text, relaxed=False):
-    """Return the text of ``text`` with all the french spelled numbers converted to digits.
+    """Return the text of ``text`` with all the French spelled numbers converted to digits.
     Takes care of punctuation.
     Set ``relaxed`` to True if you want to accept "quatre vingt(s)" as "quatre-vingt".
     """


Test patch:
diff --git a/text_to_num/tests/test_text_to_num.py b/text_to_num/tests/test_text_to_num.py
index d857316..1a19d9a 100644
--- a/text_to_num/tests/test_text_to_num.py
+++ b/text_to_num/tests/test_text_to_num.py
@@ -42,8 +42,25 @@ class TestTextToNum(TestCase):
         test4 = "quatre-vingt un"
         self.assertEqual(text2num(test4), 81)
 
+        self.assertEqual(text2num('quinze'), 15)
+        self.assertEqual(text2num('soixante quinze mille'), 75000)
+
+    def test_text2num_variants(self):
+        self.assertEqual(text2num('quatre-vingt dix-huit'), 98)
+        self.assertEqual(text2num('nonante-huit'), 98)
+        self.assertEqual(text2num('soixante-dix-huit'), 78)
+        self.assertEqual(text2num('septante-huit'), 78)
+        self.assertEqual(text2num('quatre-vingt-huit'), 88)
+        self.assertEqual(text2num('octante-huit'), 88)
+        self.assertEqual(text2num('huitante-huit'), 88)
+        self.assertEqual(text2num('huitante-et-un'), 81)
+
+    def test_text2num_centuries(self):
+        self.assertEqual(text2num('dix-neuf cent soixante-treize'), 1973)
+
     def test_text2num_exc(self):
-        self.assertRaises(AssertionError, text2num, "mille mille deux cent")
+        self.assertRaises(ValueError, text2num, "mille mille deux cent")
+        self.assertRaises(ValueError, text2num, "soixante quinze cent")
 
     def test_alpha2digit_integers(self):
         source = "Vingt-cinq vaches, douze poulets et cent vingt-cinq kg de pommes de terre."