diff --git a/.gitignore b/.gitignore index f3a9967..d578da0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.pyc build dist -*.egg-info \ No newline at end of file +*.egg-info +/.tox diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6df6b36 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +sudo: false +language: python +python: + - "2.7" + - "3.4" + - "3.5" + - "3.6" +install: pip install tox-travis +script: tox diff --git a/CHANGES.rst b/CHANGES.rst index f7f9d28..d9dd429 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,21 @@ Changelog ========= +Version 0.5.4 -- 2016/10/18 +--------------------------- + +* Tons of new languages! +* Add Polish localization. (#23) +* Add Swiss-French localization. (#38) +* Add Russian localization. (#28, #46, #48) +* Add Indonesian localization. (#29) +* Add Norwegian localization. (#33) +* Add Danish localization. (#40) +* Add Brazilian localization. (#37, #47) +* Improve German localization. (#25, #27, #49) +* Improve Lithuanian localization. (#52) +* Improve floating point spelling. (#24) + Version 0.5.3 -- 2015/06/09 --------------------------- diff --git a/README.rst b/README.rst index 580504b..beb2da6 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,8 @@ num2words - Convert numbers to words in multiple languages ========================================================== +.. image:: https://travis-ci.org/savoirfairelinux/num2words.svg?branch=master :target: https://travis-ci.org/savoirfairelinux/num2words + ``num2words`` is a library that converts numbers like ``42`` to words like ``forty-two``. It supports multiple languages (English, French, Spanish, German and Lithuanian) and can even generate ordinal numbers like ``forty-second`` (altough this last feature is a bit buggy at the moment). @@ -49,6 +51,13 @@ Besides the numerical argument, there's two optional arguments. * ``lv`` (Latvian) * ``en_GB`` (British English) * ``en_IN`` (Indian English) +* ``no`` (Norwegian) +* ``pl`` (Polish) +* ``ru`` (Russian) +* ``dk`` (Danish) +* ``pt_BR`` (Brazilian Portuguese) +* ``he`` (Hebrew) +* ``it`` (Italian) You can supply values like ``fr_FR``, the code will be correctly interpreted. If you supply an unsupported language, ``NotImplementedError`` is raised. @@ -62,7 +71,7 @@ Therefore, if you want to call ``num2words`` with a fallback, you can do:: History ------- -``num2words`` is based on an old library , ``pynum2word`` created by Taro Ogawa in 2003. +``num2words`` is based on an old library, ``pynum2word`` created by Taro Ogawa in 2003. Unfortunately, the library stopped being maintained and the author can't be reached. There was another developer, Marius Grigaitis, who in 2011 added Lithuanian support, but didn't take over maintenance of the project. diff --git a/num2words/__init__.py b/num2words/__init__.py index 1a753ce..57c4d57 100644 --- a/num2words/__init__.py +++ b/num2words/__init__.py @@ -20,22 +20,38 @@ from . import lang_EN from . import lang_EN_GB from . import lang_EN_IN from . import lang_FR +from . import lang_FR_CH from . import lang_DE from . import lang_ES from . import lang_LT from . import lang_LV from . import lang_PL +from . import lang_RU +from . import lang_ID +from . import lang_NO +from . import lang_DK +from . import lang_PT_BR +from . import lang_HE +from . import lang_IT CONVERTER_CLASSES = { 'en': lang_EN.Num2Word_EN(), 'en_GB': lang_EN_GB.Num2Word_EN_GB(), 'en_IN': lang_EN_IN.Num2Word_EN_IN(), 'fr': lang_FR.Num2Word_FR(), + 'fr_CH': lang_FR_CH.Num2Word_FR_CH(), 'de': lang_DE.Num2Word_DE(), 'es': lang_ES.Num2Word_ES(), + 'id': lang_ID.Num2Word_ID(), 'lt': lang_LT.Num2Word_LT(), 'lv': lang_LV.Num2Word_LV(), 'pl': lang_PL.Num2Word_PL(), + 'ru': lang_RU.Num2Word_RU(), + 'no': lang_NO.Num2Word_NO(), + 'dk': lang_DK.Num2Word_DK(), + 'pt_BR': lang_PT_BR.Num2Word_PT_BR(), + 'he': lang_HE.Num2Word_HE(), + 'it': lang_IT.Num2Word_IT() } def num2words(number, ordinal=False, lang='en'): diff --git a/num2words/base.py b/num2words/base.py index f892d42..272d5bb 100644 --- a/num2words/base.py +++ b/num2words/base.py @@ -15,7 +15,11 @@ # MA 02110-1301 USA from __future__ import unicode_literals + +import math + from .orderedmapping import OrderedMapping +from .compat import to_s class Num2Word_Base(object): @@ -36,7 +40,7 @@ class Num2Word_Base(object): self.set_numwords() self.MAXVAL = 1000 * self.cards.order[0] - + def set_numwords(self): self.set_high_numwords(self.high_numwords) @@ -64,7 +68,7 @@ class Num2Word_Base(object): for elem in self.cards: if elem > value: continue - + out = [] if value == 0: div, mod = 1, 0 @@ -75,7 +79,7 @@ class Num2Word_Base(object): out.append((self.cards[1], 1)) else: if div == value: # The system tallies, eg Roman Numerals - return [(div * self.cards[elem], div*elem)] + return [(div * self.cards[elem], div*elem)] out.append(self.splitnum(div)) out.append((self.cards[elem], elem)) @@ -88,7 +92,7 @@ class Num2Word_Base(object): def to_cardinal(self, value): try: - assert long(value) == value + assert int(value) == value except (ValueError, TypeError, AssertionError): return self.to_cardinal_float(value) @@ -101,7 +105,6 @@ class Num2Word_Base(object): if value >= self.MAXVAL: raise OverflowError(self.errmsg_toobig % (value, self.MAXVAL)) - val = self.splitnum(value) words, num = self.clean(val) @@ -114,18 +117,26 @@ class Num2Word_Base(object): except (ValueError, TypeError, AssertionError): raise TypeError(self.errmsg_nonnum % value) + value = float(value) pre = int(value) - post = abs(value - pre) + post = abs(value - pre) * 10**self.precision + if abs(round(post) - post) < 0.01: + # We generally floor all values beyond our precision (rather than rounding), but in + # cases where we have something like 1.239999999, which is probably due to python's + # handling of floats, we actually want to consider it as 1.24 instead of 1.23 + post = int(round(post)) + else: + post = int(math.floor(post)) + post = str(post) + post = '0' * (self.precision - len(post)) + post out = [self.to_cardinal(pre)] if self.precision: out.append(self.title(self.pointword)) for i in range(self.precision): - post *= 10 - curr = int(post) - out.append(str(self.to_cardinal(curr))) - post -= curr + curr = int(post[i]) + out.append(to_s(self.to_cardinal(curr))) return " ".join(out) @@ -170,10 +181,10 @@ class Num2Word_Base(object): def verify_ordinal(self, value): - if not value == long(value): - raise TypeError, self.errmsg_floatord %(value) + if not value == int(value): + raise TypeError(self.errmsg_floatord % value) if not abs(value) == value: - raise TypeError, self.errmsg_negord %(value) + raise TypeError(self.errmsg_negord % value) def verify_num(self, value): @@ -183,8 +194,8 @@ class Num2Word_Base(object): def set_wordnums(self): pass - - def to_ordinal(value): + + def to_ordinal(self, value): return self.to_cardinal(value) @@ -260,6 +271,6 @@ class Num2Word_Base(object): _ordnum = self.to_ordinal_num(value) except: _ordnum = "invalid" - + print ("For %s, card is %s;\n\tord is %s; and\n\tordnum is %s." % (value, _card, _ord, _ordnum)) diff --git a/num2words/compat.py b/num2words/compat.py new file mode 100644 index 0000000..7395f4c --- /dev/null +++ b/num2words/compat.py @@ -0,0 +1,26 @@ +# Copyright (c) 2003, Taro Ogawa. All Rights Reserved. +# Copyright (c) 2016, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +import sys + +PY3 = sys.version_info[0] == 3 + +def to_s(val): + if PY3: + return str(val) + else: + return unicode(val) + diff --git a/num2words/lang_DE.py b/num2words/lang_DE.py index fc2653e..1c2fa0f 100644 --- a/num2words/lang_DE.py +++ b/num2words/lang_DE.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- # Copyright (c) 2003, Taro Ogawa. All Rights Reserved. # Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. @@ -14,10 +15,9 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301 USA -from __future__ import unicode_literals +from __future__ import unicode_literals, print_function from .lang_EU import Num2Word_EU -#//TODO: Use German error messages class Num2Word_DE(Num2Word_EU): def set_high_numwords(self, high): max = 3 + 6*len(high) @@ -26,12 +26,13 @@ class Num2Word_DE(Num2Word_EU): self.cards[10**n] = word + "illiarde" self.cards[10**(n-3)] = word + "illion" - def setup(self): self.negword = "minus " self.pointword = "Komma" - self.errmsg_nonnum = "Only numbers may be converted to words." - self.errmsg_toobig = "Number is too large to convert to words." + self.errmsg_floatord = "Die Gleitkommazahl %s kann nicht in eine Ordnungszahl konvertiert werden." # "Cannot treat float %s as ordinal." + self.errmsg_nonnum = "Nur Zahlen (type(%s)) können in Wörter konvertiert werden." # "type(((type(%s)) ) not in [long, int, float]" + self.errmsg_negord = "Die negative Zahl %s kann nicht in eine Ordnungszahl konvertiert werden." # "Cannot treat negative num %s as ordinal." + self.errmsg_toobig = "Die Zahl %s muss kleiner als %s sein." # "abs(%s) must be less than %s." self.exclude_title = [] lows = ["non", "okt", "sept", "sext", "quint", "quadr", "tr", "b", "m"] @@ -44,66 +45,64 @@ class Num2Word_DE(Num2Word_EU): (90, "neunzig"), (80, "achtzig"), (70, "siebzig"), (60, "sechzig"), (50, "f\xFCnfzig"), (40, "vierzig"), (30, "drei\xDFig")] - self.low_numwords = ["zwanzig", "neunzehn", "achtzen", "siebzehn", + self.low_numwords = ["zwanzig", "neunzehn", "achtzehn", "siebzehn", "sechzehn", "f\xFCnfzehn", "vierzehn", "dreizehn", "zw\xF6lf", "elf", "zehn", "neun", "acht", "sieben", "sechs", "f\xFCnf", "vier", "drei", "zwei", "eins", "null"] - self.ords = { "eins" : "ers", - "drei" : "drit", - "acht" : "ach", - "sieben" : "sieb", - "ig" : "igs" } - self.ordflag = False - + self.ords = {"eins": "ers", + "drei": "drit", + "acht": "ach", + "sieben": "sieb", + "ig": "igs", + "ert": "erts", + "end": "ends", + "ion": "ions", + "nen": "nens", + "rde": "rdes", + "rden": "rdens"} def merge(self, curr, next): ctext, cnum, ntext, nnum = curr + next if cnum == 1: - if nnum < 10**6 or self.ordflag: + if nnum < 10**6: return next ctext = "eine" if nnum > cnum: if nnum >= 10**6: if cnum > 1: - if ntext.endswith("e") or self.ordflag: - ntext += "s" + if ntext.endswith("e"): + ntext += "n" else: - ntext += "es" + ntext += "en" ctext += " " val = cnum * nnum else: if nnum < 10 < cnum < 100: if nnum == 1: ntext = "ein" - ntext, ctext = ctext, ntext + "und" + ntext, ctext = ctext, ntext + "und" elif cnum >= 10**6: ctext += " " val = cnum + nnum word = ctext + ntext return (word, val) - def to_ordinal(self, value): self.verify_ordinal(value) - self.ordflag = True outword = self.to_cardinal(value) - self.ordflag = False for key in self.ords: if outword.endswith(key): outword = outword[:len(outword) - len(key)] + self.ords[key] break return outword + "te" - - # Is this correct?? def to_ordinal_num(self, value): self.verify_ordinal(value) - return str(value) + "te" - + return str(value) + "." def to_currency(self, val, longval=True, old=False): if old: @@ -117,8 +116,6 @@ class Num2Word_DE(Num2Word_EU): return self.to_cardinal(val) return self.to_splitnum(val, hightxt="hundert", longval=longval) - - n2w = Num2Word_DE() to_card = n2w.to_cardinal to_ord = n2w.to_ordinal @@ -126,15 +123,20 @@ to_ordnum = n2w.to_ordinal_num def main(): - for val in [ 1, 11, 12, 21, 31, 33, 71, 80, 81, 91, 99, 100, 101, 102, 155, + for val in [1, 7, 8, 12, 17, 81, 91, 99, 100, 101, 102, 155, 180, 300, 308, 832, 1000, 1001, 1061, 1100, 1500, 1701, 3000, - 8280, 8291, 150000, 500000, 1000000, 2000000, 2000001, + 8280, 8291, 150000, 500000, 3000000, 1000000, 2000001, 1000000000, 2000000000, -21212121211221211111, -2.121212, -1.0000100]: n2w.test(val) - n2w.test(1325325436067876801768700107601001012212132143210473207540327057320957032975032975093275093275093270957329057320975093272950730) - print n2w.to_currency(112121) - print n2w.to_year(2000) + # n2w.test(1325325436067876801768700107601001012212132143210473207540327057320957032975032975093275093275093270957329057320975093272950730) + n2w.test(3000000) + n2w.test(3000000000001) + n2w.test(3000000324566) + print(n2w.to_currency(112121)) + print(n2w.to_year(2000)) + print(n2w.to_year(1820)) + print(n2w.to_year(2001)) if __name__ == "__main__": main() diff --git a/num2words/lang_DK.py b/num2words/lang_DK.py new file mode 100644 index 0000000..64cf793 --- /dev/null +++ b/num2words/lang_DK.py @@ -0,0 +1,154 @@ +# Copyright (c) 2003, Taro Ogawa. All Rights Reserved. +# Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +from __future__ import division, unicode_literals, print_function +from num2words import lang_EU + +class Num2Word_DK(lang_EU.Num2Word_EU): + def set_high_numwords(self, high): + max = 3 + 6*len(high) + for word, n in zip(high, range(max, 3, -6)): + self.cards[10**n] = word + "illarder" + self.cards[10**(n-3)] = word + "illioner" + + def setup(self): + self.negword = "minus " + self.pointword = "komma" + self.errmsg_nornum = "Kun tal kan blive konverteret til ord." + self.exclude_title = ["og", "komma", "minus"] + + self.mid_numwords = [(1000, "tusind"), (100, "hundrede"), + (90, "halvfems"), (80, "firs"), (70, "halvfjerds"), + (60, "treds"), (50, "halvtreds"), (40, "fyrre"), + (30, "tredive")] + self.low_numwords = ["tyve", "nitten", "atten", "sytten", + "seksten", "femten", "fjorten", "tretten", + "tolv", "elleve", "ti", "ni", "otte", + "syv", "seks", "fem", "fire", "tre", "to", + "et", "nul"] + self.ords = { "nul" : "nul", + "et" : "f\xf8rste", + "to" : "anden", + "tre" : "tredje", + "fire" : "fjerde", + "fem" : "femte", + "seks" : "sjette", + "syv" : "syvende", + "otte" : "ottende", + "ni" : "niende", + "ti" : "tiende", + "elleve" : "ellevte", + "tolv" : "tolvte", + "tretten" : "trett", + "fjorten" : "fjort", + "femten" : "femt", + "seksten" : "sekst", + "sytten" : "sytt", + "atten" : "att", + "nitten" : "nitt", + "tyve" : "tyv"} + + def merge(self, curr, next): + ctext, cnum, ntext, nnum = curr + next + if next[1] == 100 or next[1] == 1000: + lst = list(next) + lst[0] = 'et' + lst[0] + next = tuple(lst) + + if cnum == 1: + if nnum < 10**6 or self.ordflag: + return next + ctext = "en" + if nnum > cnum: + if nnum >= 10**6: + ctext += " " + val = cnum * nnum + else: + if cnum >= 100 and cnum < 1000: + ctext += " og " + elif cnum >= 1000 and cnum <= 100000: + ctext += "e og " + if nnum < 10 < cnum < 100: + if nnum == 1: + ntext = "en" + ntext, ctext = ctext, ntext + "og" + elif cnum >= 10**6: + ctext += " " + val = cnum + nnum + word = ctext + ntext + return (word, val) + + + def to_ordinal(self, value): + self.verify_ordinal(value) + self.ordflag = True + outword = self.to_cardinal(value) + self.ordflag = False + for key in self.ords: + if outword.endswith(key): + outword = outword[:len(outword) - len(key)] + self.ords[key] + break + if value %100 >= 30 and value %100 <= 39 or value %100 == 0: + outword += "te" + elif value % 100 > 12 or value %100 == 0: + outword += "ende" + return outword + + def to_ordinal_num(self, value): + self.verify_ordinal(value) + vaerdte = (0,1,5,6,11,12) + if value %100 >= 30 and value %100 <= 39 or value % 100 in vaerdte: + return str(value) + "te" + elif value % 100 == 2: + return str(value) + "en" + return str(value) + "ende" + + + def to_currency(self, val, longval=True): + if val//100 == 1 or val == 1: + ret = self.to_splitnum(val, hightxt="kr", lowtxt="\xf8re", + jointxt="og",longval=longval) + return "en " + ret[3:] + return self.to_splitnum(val, hightxt="kr", lowtxt="\xf8re", + jointxt="og",longval=longval) + + def to_year(self, val, longval=True): + if val == 1: + return 'en' + if not (val//100)%10: + return self.to_cardinal(val) + return self.to_splitnum(val, hightxt="hundrede", longval=longval) + +n2w = Num2Word_DK() +to_card = n2w.to_cardinal +to_ord = n2w.to_ordinal +to_ordnum = n2w.to_ordinal_num +to_year = n2w.to_year + +def main(): + for val in [ 1, 11, 12, 21, 31, 33, 71, 80, 81, 91, 99, 100, 101, 102, 155, + 180, 300, 308, 832, 1000, 1001, 1061, 1100, 1500, 1701, 3000, + 8280, 8291, 150000, 500000, 1000000, 2000000, 2000001, + -21212121211221211111, -2.121212, -1.0000100]: + n2w.test(val) + n2w.test(1325325436067876801768700107601001012212132143210473207540327057320957032975032975093275093275093270957329057320975093272950730) + for val in [1,120, 160, 1000,1120,1800, 1976,2000,2010,2099,2171]: + print(val, "er", n2w.to_currency(val)) + print(val, "er", n2w.to_year(val)) + n2w.test(65132) + +if __name__ == "__main__": + main() diff --git a/num2words/lang_EN.py b/num2words/lang_EN.py index 54646f5..656abfc 100644 --- a/num2words/lang_EN.py +++ b/num2words/lang_EN.py @@ -14,7 +14,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301 USA -from __future__ import division, unicode_literals +from __future__ import division, unicode_literals, print_function from . import lang_EU class Num2Word_EN(lang_EU.Num2Word_EU): @@ -47,7 +47,9 @@ class Num2Word_EN(lang_EU.Num2Word_EU): "twelve" : "twelfth" } - def merge(self, (ltext, lnum), (rtext, rnum)): + def merge(self, lpair, rpair): + ltext, lnum = lpair + rtext, rnum = rpair if lnum == 1 and rnum < 100: return (rtext, rnum) elif 100 > lnum > rnum : @@ -68,9 +70,9 @@ class Num2Word_EN(lang_EU.Num2Word_EU): lastword = self.ords[lastword] except KeyError: if lastword[-1] == "y": - lastword = lastword[:-1] + "ie" + lastword = lastword[:-1] + "ie" lastword += "th" - lastwords[-1] = self.title(lastword) + lastwords[-1] = self.title(lastword) outwords[-1] = "-".join(lastwords) return " ".join(outwords) @@ -105,9 +107,9 @@ def main(): n2w.test(val) n2w.test(1325325436067876801768700107601001012212132143210473207540327057320957032975032975093275093275093270957329057320975093272950730) for val in [1,120,1000,1120,1800, 1976,2000,2010,2099,2171]: - print val, "is", n2w.to_currency(val) - print val, "is", n2w.to_year(val) - + print(val, "is", n2w.to_currency(val)) + print(val, "is", n2w.to_year(val)) + if __name__ == "__main__": main() diff --git a/num2words/lang_EN_GB.py b/num2words/lang_EN_GB.py index 568594d..a01c41a 100644 --- a/num2words/lang_EN_GB.py +++ b/num2words/lang_EN_GB.py @@ -14,10 +14,10 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301 USA -from __future__ import unicode_literals +from __future__ import unicode_literals, print_function from .lang_EN import Num2Word_EN - + class Num2Word_EN_GB(Num2Word_EN): def to_currency(self, val, longval=True): return self.to_splitnum(val, hightxt="pound/s", lowtxt="pence", @@ -38,9 +38,9 @@ def main(): n2w.test(val) n2w.test(1325325436067876801768700107601001012212132143210473207540327057320957032975032975093275093275093270957329057320975093272950730) for val in [1,120,1000,1120,1800, 1976,2000,2010,2099,2171]: - print val, "is", n2w.to_currency(val) - print val, "is", n2w.to_year(val) - + print(val, "is", n2w.to_currency(val)) + print(val, "is", n2w.to_year(val)) + if __name__ == "__main__": main() diff --git a/num2words/lang_ES.py b/num2words/lang_ES.py index 71103a8..c010d52 100644 --- a/num2words/lang_ES.py +++ b/num2words/lang_ES.py @@ -16,7 +16,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301 USA -from __future__ import unicode_literals +from __future__ import unicode_literals, print_function from .lang_EU import Num2Word_EU class Num2Word_ES(Num2Word_EU): @@ -173,9 +173,9 @@ def main(): n2w.test(val) n2w.test(1325325436067876801768700107601001012212132143210473207540327057320957032975032975093275093275093270957329057320975093272950730) - print n2w.to_currency(1222) - print n2w.to_currency(1222, old=True) - print n2w.to_year(1222) + print(n2w.to_currency(1222)) + print(n2w.to_currency(1222, old=True)) + print(n2w.to_year(1222)) if __name__ == "__main__": main() diff --git a/num2words/lang_FR.py b/num2words/lang_FR.py index ce2773a..1209eab 100644 --- a/num2words/lang_FR.py +++ b/num2words/lang_FR.py @@ -15,16 +15,16 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301 USA -from __future__ import unicode_literals +from __future__ import unicode_literals, print_function from .lang_EU import Num2Word_EU -#//TODO: error messages in French + class Num2Word_FR(Num2Word_EU): def setup(self): self.negword = "moins " self.pointword = "virgule" - self.errmsg_nonnum = "Only numbers may be converted to words." - self.errmsg_toobig = "Number is too large to convert to words." + self.errmsg_nonnum = u"Seulement des nombres peuvent être convertis en mots." + self.errmsg_toobig = u"Nombre trop grand pour être converti en mots." self.exclude_title = ["et", "virgule", "moins"] self.mid_numwords = [(1000, "mille"), (100, "cent"), (80, "quatre-vingts"), (60, "soixante"), @@ -106,8 +106,8 @@ def main(): n2w.test(val) n2w.test(1325325436067876801768700107601001012212132143210473207540327057320957032975032975093275093275093270957329057320975093272950730) - print n2w.to_currency(112121) - print n2w.to_year(1996) + print(n2w.to_currency(112121)) + print(n2w.to_year(1996)) if __name__ == "__main__": diff --git a/num2words/lang_FR_CH.py b/num2words/lang_FR_CH.py new file mode 100644 index 0000000..b6d40a0 --- /dev/null +++ b/num2words/lang_FR_CH.py @@ -0,0 +1,109 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2003, Taro Ogawa. All Rights Reserved. +# Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +from __future__ import unicode_literals, print_function +from .lang_EU import Num2Word_EU + +class Num2Word_FR_CH(Num2Word_EU): + def setup(self): + self.negword = "moins " + self.pointword = "virgule" + self.errmsg_nonnum = u"Seulement des nombres peuvent être convertis en mots." + self.errmsg_toobig = u"Nombre trop grand pour être converti en mots." + self.exclude_title = ["et", "virgule", "moins"] + self.mid_numwords = [(1000, "mille"), (100, "cent"), (90, "nonante"), + (80, "huitante"), (70, "septante"), (60, "soixante"), + (50, "cinquante"), (40, "quarante"), + (30, "trente")] + self.low_numwords = ["vingt", "dix-neuf", "dix-huit", "dix-sept", + "seize", "quinze", "quatorze", "treize", "douze", + "onze", "dix", "neuf", "huit", "sept", "six", + "cinq", "quatre", "trois", "deux", "un", "zéro"] + self.ords = { + "cinq": "cinquième", + "neuf": "neuvième", + } + + + def merge(self, curr, next): + ctext, cnum, ntext, nnum = curr + next + + if cnum == 1: + if nnum < 1000000: + return next + if cnum < 1000 and nnum != 1000 and ntext[-1] != "s" and not nnum % 100: + ntext += "s" + + if nnum < cnum < 100: + if nnum % 10 == 1: + return ("%s et %s"%(ctext, ntext), cnum + nnum) + return ("%s-%s"%(ctext, ntext), cnum + nnum) + elif nnum > cnum: + return ("%s %s"%(ctext, ntext), cnum * nnum) + return ("%s %s"%(ctext, ntext), cnum + nnum) + + + # Is this right for such things as 1001 - "mille unième" instead of + # "mille premier"?? "millième"?? + + def to_ordinal(self,value): + self.verify_ordinal(value) + if value == 1: + return "premier" + word = self.to_cardinal(value) + for src, repl in self.ords.items(): + if word.endswith(src): + word = word[:-len(src)] + repl + break + else: + if word[-1] == "e": + word = word[:-1] + word = word + "ième" + return word + + def to_ordinal_num(self, value): + self.verify_ordinal(value) + out = str(value) + out += {"1" : "er" }.get(out[-1], "me") + return out + + def to_currency(self, val, longval=True, old=False): + hightxt = "Euro/s" + if old: + hightxt="franc/s" + return self.to_splitnum(val, hightxt=hightxt, lowtxt="centime/s", + jointxt="et",longval=longval) + +n2w = Num2Word_FR_CH() +to_card = n2w.to_cardinal +to_ord = n2w.to_ordinal +to_ordnum = n2w.to_ordinal_num + +def main(): + for val in [ 1, 11, 12, 21, 31, 33, 71, 80, 81, 91, 99, 100, 101, 102, 155, + 180, 300, 308, 832, 1000, 1001, 1061, 1100, 1500, 1701, 3000, + 8280, 8291, 150000, 500000, 1000000, 2000000, 2000001, + -21212121211221211111, -2.121212, -1.0000100]: + n2w.test(val) + + n2w.test(1325325436067876801768700107601001012212132143210473207540327057320957032975032975093275093275093270957329057320975093272950730) + print(n2w.to_currency(112121)) + print(n2w.to_year(1996)) + + +if __name__ == "__main__": + main() diff --git a/num2words/lang_HE.py b/num2words/lang_HE.py new file mode 100644 index 0000000..15e67c8 --- /dev/null +++ b/num2words/lang_HE.py @@ -0,0 +1,162 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2003, Taro Ogawa. All Rights Reserved. +# Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + + +from __future__ import unicode_literals, print_function + +ZERO = (u'אפס',) + +ONES = { + 1: (u'אחד',), + 2: (u'שנים',), + 3: (u'שלש',), + 4: (u'ארבע',), + 5: (u'חמש',), + 6: (u'שש',), + 7: (u'שבע',), + 8: (u'שמנה',), + 9: (u'תשע',), +} + +TENS = { + 0: (u'עשר',), + 1: (u'אחד עשרה',), + 2: (u'שנים עשרה',), + 3: (u'שלש עשרה',), + 4: (u'ארבע עשרה',), + 5: (u'חמש עשרה',), + 6: (u'שש עשרה',), + 7: (u'שבע עשרה',), + 8: (u'שמנה עשרה',), + 9: (u'תשע עשרה',), +} + +TWENTIES = { + 2: (u'עשרים',), + 3: (u'שלשים',), + 4: (u'ארבעים',), + 5: (u'חמישים',), + 6: (u'ששים',), + 7: (u'שבעים',), + 8: (u'שמנים',), + 9: (u'תשעים',), +} + +HUNDRED = { + 1: (u'מאה',), + 2: (u'מאתיים',), + 3: (u'מאות',) +} + +THOUSANDS = { + 1: (u'אלף',), + 2: (u'אלפיים',), +} + +AND = u'ו' + +def splitby3(n): + length = len(n) + if length > 3: + start = length % 3 + if start > 0: + yield int(n[:start]) + for i in range(start, length, 3): + yield int(n[i:i+3]) + else: + yield int(n) + + +def get_digits(n): + return [int(x) for x in reversed(list(('%03d' % n)[-3:]))] + + +def pluralize(n, forms): + # gettext implementation: + # (n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2) + + form = 0 if (n % 10 == 1 and n % 100 != 11) else 1 if n != 0 else 2 + + return forms[form] + + +def int2word(n): + if n > 9999: #doesn't yet work for numbers this big + raise NotImplementedError() + + if n == 0: + return ZERO[0] + + words = [] + + chunks = list(splitby3(str(n))) + i = len(chunks) + for x in chunks: + i -= 1 + n1, n2, n3 = get_digits(x) + + # print str(n3) + str(n2) + str(n1) + + if n3 > 0: + if n3 <= 2: + words.append(HUNDRED[n3][0]) + else: + words.append(ONES[n3][0]) + words.append(HUNDRED[3][0]) + + if n2 > 1: + words.append(TWENTIES[n2][0]) + + if n2 == 1: + words.append(TENS[n1][0]) + elif n1 > 0 and not (i > 0 and x == 1): + words.append(ONES[n1][0]) + + if i > 0: + if i <= 2: + words.append(THOUSANDS[i][0]) + else: + words.append(ONES[i][0]) + words.append(THOUSANDS[1][0]) + + if len(words) > 1: + words[-1] = AND + words[-1] + return ' '.join(words) + + +def n2w(n): + return int2word(int(n)) + + +def to_currency(n, currency='EUR', cents=True, seperator=','): + raise NotImplementedError() + + +class Num2Word_HE(object): + def to_cardinal(self, number): + return n2w(number) + + def to_ordinal(self, number): + raise NotImplementedError() + + +if __name__ == '__main__': + yo = Num2Word_HE() + nums = [1, 11, 21, 24, 99, 100, 101, 200, 211, 345, 1000, 1011] + for num in nums: + print(num, yo.to_cardinal(num)) + diff --git a/num2words/lang_ID.py b/num2words/lang_ID.py new file mode 100644 index 0000000..9437db7 --- /dev/null +++ b/num2words/lang_ID.py @@ -0,0 +1,196 @@ +# Copyright (c) 2003, Taro Ogawa. All Rights Reserved. +# Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +from __future__ import unicode_literals, print_function + +class Num2Word_ID(): + + BASE = {0: [], + 1: ["satu"], + 2: ["dua"], + 3: ["tiga"], + 4: ["empat"], + 5: ["lima"], + 6: ["enam"], + 7: ["tujuh"], + 8: ["delapan"], + 9: ["sembilan"]} + + TENS_TO = {3: "ribu", + 6: "juta", + 9: "miliar", + 12: "triliun", + 15: "kuadriliun", + 18: "kuantiliun", + 21: "sekstiliun", + 24: "septiliun", + 27: "oktiliun", + 30: "noniliun", + 33: "desiliun"} + + errmsg_floatord = "Cannot treat float number as ordinal" + errmsg_negord = "Cannot treat negative number as ordinal" + errmsg_toobig = "Too large" + max_num = 10**36 + + def split_by_koma(self, number): + return str(number).split('.') + + def split_by_3(self, number): + """ + starting here, it groups the number by three from the tail + '1234567' -> (('1',),('234',),('567',)) + :param number:str + :rtype:tuple + """ + blocks = () + length = len(number) + + if length < 3: + blocks += ((number,),) + else: + len_of_first_block = length % 3 + + if len_of_first_block > 0: + first_block = number[0:len_of_first_block], + blocks += first_block, + + for i in range(len_of_first_block, length, 3): + next_block = (number[i:i+3],), + blocks += next_block + + return blocks + + def spell(self, blocks): + """ + it adds the list of spelling to the blocks + (('1',),('034',)) -> (('1',['satu']),('234',['tiga', 'puluh', 'empat'])) + :param blocks: tuple + :rtype: tuple + """ + word_blocks = () + first_block = blocks[0] + if len(first_block[0]) == 1: + if first_block[0] == '0': + spelling = ['nol'] + else: + spelling = self.BASE[int(first_block[0])] + elif len(first_block[0]) == 2: + spelling = self.puluh(first_block[0]) + else: + spelling = self.ratus(first_block[0][0]) + self.puluh(first_block[0][1:3]) + + word_blocks += (first_block[0], spelling), + + for block in blocks[1:]: + spelling = self.ratus(block[0][0]) + self.puluh(block[0][1:3]) + block += spelling, + word_blocks += block, + + return word_blocks + + def ratus(self, number): + # it is used to spell + if number == '1': + return ['seratus'] + elif number == '0': + return [] + else: + return self.BASE[int(number)]+['ratus'] + + def puluh(self, number): + # it is used to spell + if number[0] == '1': + if number[1]== '0': + return ['sepuluh'] + elif number[1] == '1': + return ['sebelas'] + else: + return self.BASE[int(number[1])]+['belas'] + elif number[0] == '0': + return self.BASE[int(number[1])] + else: + return self.BASE[int(number[0])]+['puluh']+ self.BASE[int(number[1])] + + def spell_float(self, float_part): + # spell the float number + word_list = [] + for n in float_part: + if n == '0': + word_list += ['nol'] + continue + word_list += self.BASE[int(n)] + return ' '.join(['','koma']+word_list) + + def join(self, word_blocks, float_part): + """ + join the words by first join lists in the tuple + :param word_blocks: tuple + :rtype: str + """ + word_list = [] + length = len(word_blocks)-1 + first_block = word_blocks[0], + start = 0 + + if length == 1 and first_block[0][0] == '1': + word_list += ['seribu'] + start = 1 + + for i in range(start, length+1, 1): + word_list += word_blocks[i][1] + if not word_blocks[i][1]: + continue + if i == length: + break + word_list += [self.TENS_TO[(length-i)*3]] + + return ' '.join(word_list)+float_part + + def to_cardinal(self, number): + if number >= self.max_num: + raise OverflowError(self.errmsg_toobig % (number, self.maxnum)) + minus = '' + if number < 0: + minus = 'min ' + float_word = '' + n = self.split_by_koma(abs(number)) + if len(n)==2: + float_word = self.spell_float(n[1]) + return minus + self.join(self.spell(self.split_by_3(n[0])), float_word) + + def to_ordinal(self, number): + self.verify_ordinal(number) + out_word = self.to_cardinal(number) + if out_word == "satu": + return "pertama" + return "ke" + out_word + + def to_ordinal_num(self, number): + self.verify_ordinal(number) + return "ke-" + str(number) + + def to_currency(self, value): + return self.to_cardinal(value)+" rupiah" + + def to_year(self, value): + return self.to_cardinal(value) + + def verify_ordinal(self, value): + if not value == int(value): + raise TypeError(self.errmsg_floatord % value) + if not abs(value) == value: + raise TypeError(self.errmsg_negord % value) diff --git a/num2words/lang_IT.py b/num2words/lang_IT.py new file mode 100644 index 0000000..227883b --- /dev/null +++ b/num2words/lang_IT.py @@ -0,0 +1,217 @@ +# -*- encoding: utf-8 -*- +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +from __future__ import unicode_literals +from .lang_EU import Num2Word_EU + +import re +import math + +class Num2Word_IT(object): + def __init__(self): + self._minus = "meno " + + self._exponent = { + 0 : ('',''), + 3 : ('mille','mila'), + 6 : ('milione','miloni'), + 12 : ('miliardo','miliardi'), + 18 : ('trillone','trilloni'), + 24 : ('quadrilione','quadrilioni')} + + self._digits = ['zero', 'uno', 'due', 'tre', 'quattro', 'cinque', 'sei', 'sette', 'otto', 'nove'] + + self._sep = '' + + def _toWords(self, num, power=0): + str_num = str(num) + # The return string; + ret = '' + + # add a the word for the minus sign if necessary + if num < 0: + ret = self._sep + self._minus + + if len(str_num) > 6: + current_power = 6 + # check for highest power + if power in self._exponent: + # convert the number above the first 6 digits + # with it's corresponding $power. + snum = str_num[0:-6] + if snum != '': + ret = ret + self._toWords(int(snum), power + 6) + + num = int(str_num[-6:]) + if num == 0: + return ret + + elif num == 0 or str_num == '': + return ' ' + self._digits[0] + ' ' + else: + current_power = len(str_num) + + # See if we need "thousands" + thousands = math.floor(num / 1000) + if thousands == 1: + ret = ret + self._sep + 'mille' + self._sep + elif thousands > 1: + ret = ret + self._toWords(int(thousands), 3) + self._sep + + # values for digits, tens and hundreds + h = int(math.floor((num / 100) % 10)) + t = int(math.floor((num / 10) % 10)) + d = int(math.floor(num % 10)) + + # centinaia: duecento, trecento, etc... + if h == 1: + if ((d==0) and (t == 0)):# is it's '100' use 'cien' + ret = ret + self._sep + 'cento' + else: + ret = ret + self._sep + 'cento' + elif h == 2 or h == 3 or h == 4 or h == 6 or h == 8: + ret = ret + self._sep + self._digits[h] + 'cento' + elif h == 5: + ret = ret + self._sep + 'cinquecento' + elif h == 7: + ret = ret + self._sep + 'settecento' + elif h == 9: + ret = ret + self._sep + 'novecento' + + # decine: venti trenta, etc... + if t == 9: + if d == 1 or d == 8: + ret = ret + self._sep + 'novant' + else: + ret = ret + self._sep + 'novanta' + if t == 8: + if d == 1 or d == 8: + ret = ret + self._sep + 'ottant' + else: + ret = ret + self._sep + 'ottanta' + if t == 7: + if d == 1 or d == 8: + ret = ret + self._sep + 'settant' + else: + ret = ret + self._sep + 'settanta' + if t == 6: + if d == 1 or d == 8: + ret = ret + self._sep + 'sessant' + else: + ret = ret + self._sep + 'sessanta' + if t == 5: + if d == 1 or d == 8: + ret = ret + self._sep + 'cinquant' + else: + ret = ret + self._sep + 'cinquanta' + if t == 4: + if d == 1 or d == 8: + ret = ret + self._sep + 'quarant' + else: + ret = ret + self._sep + 'quaranta' + if t == 3: + if d == 1 or d == 8: + ret = ret + self._sep + 'trent' + else: + ret = ret + self._sep + 'trenta' + if t == 2: + if d == 0: + ret = ret + self._sep + 'venti' + elif (d == 1 or d == 8): + ret = ret + self._sep + 'vent' + self._digits[d] + else: + ret = ret + self._sep + 'venti' + self._digits[d] + if t == 1: + if d == 0: + ret = ret + self._sep + 'dieci' + elif d == 1: + ret = ret + self._sep + 'undici' + elif d == 2: + ret = ret + self._sep + 'dodici' + elif d == 3: + ret = ret + self._sep + 'tredici' + elif d == 4: + ret = ret + self._sep + 'quattordici' + elif d == 5: + ret = ret + self._sep + 'quindici' + elif d == 6: + ret = ret + self._sep + 'sedici' + elif d == 7: + ret = ret + self._sep + 'diciassette' + elif d == 8: + ret = ret + self._sep + 'diciotto' + elif d == 9: + ret = ret + self._sep + 'diciannove' + + # add digits only if it is a multiple of 10 and not 1x or 2x + if t != 1 and t != 2 and d > 0: + # don't add 'e' for numbers below 10 + if t != 0: + # use 'un' instead of 'uno' when there is a suffix ('mila', 'milloni', etc...) + if (power > 0) and ( d == 1): + ret = ret + self._sep + 'e un' + else: + ret = ret + self._sep + '' + self._digits[d] + else: + if power > 0 and d == 1: + ret = ret + self._sep + 'un ' + else: + ret = ret + self._sep + self._digits[d] + + if power > 0: + if power in self._exponent: + lev = self._exponent[power] + + if lev is None: + return None + + # if it's only one use the singular suffix + if d == 1 and t == 0 and h == 0: + suffix = lev[0] + else: + suffix = lev[1] + + if num != 0: + ret = ret + self._sep + suffix + + return ret + + + def to_cardinal(self, number): + return self._toWords(number) + + def to_ordinal_num(self, number): + pass + + def to_ordinal(self,value): + if 0 <= value <= 10: + return ["primo", "secondo", "terzo", "quarto", "quinto", "sesto", "settimo", "ottavo", "nono", "decimo"][value - 1] + else: + as_word = self._toWords(value) + if as_word.endswith("dici"): + return re.sub("dici$", "dicesimo", as_word) + elif as_word.endswith("to"): + return re.sub("to$", "tesimo", as_word) + elif as_word.endswith("ta"): + return re.sub("ta$", "tesimo", as_word) + else: + return as_word + "simo" + + +n2w = Num2Word_IT() +to_card = n2w.to_cardinal +to_ord = n2w.to_ordinal +to_ordnum = n2w.to_ordinal_num + diff --git a/num2words/lang_LT.py b/num2words/lang_LT.py index d70c39b..a274e4a 100644 --- a/num2words/lang_LT.py +++ b/num2words/lang_LT.py @@ -85,7 +85,13 @@ vienas litas, nulis centų vienas tūkstantis du šimtai trisdešimt keturi litai, penkiasdešimt šeši centai >>> print(to_currency(-1251985, cents = False)) -minus dvylika tūkstančių penki šimtai devyniolika litų, 85 centai +minus dvylika tūkstančių penki šimtai devyniolika eurų, 85 centai + +>>> print(to_currency(1.0, 'EUR')) +vienas euras, nulis centų + +>>> print(to_currency(1234.56, 'EUR')) +vienas tūkstantis du šimtai trisdešimt keturi eurai, penkiasdešimt šeši centai """ from __future__ import unicode_literals @@ -144,6 +150,7 @@ THOUSANDS = { CURRENCIES = { 'LTL': ((u'litas', u'litai', u'litų'), (u'centas', u'centai', u'centų')), + 'EUR': ((u'euras', u'eurai', u'eurų'), (u'centas', u'centai', u'centų')), } def splitby3(n): @@ -210,7 +217,7 @@ def n2w(n): else: return int2word(int(n)) -def to_currency(n, currency='LTL', cents = True): +def to_currency(n, currency='EUR', cents = True): if type(n) == int: if n < 0: minus = True diff --git a/num2words/lang_NO.py b/num2words/lang_NO.py new file mode 100644 index 0000000..2c744e1 --- /dev/null +++ b/num2words/lang_NO.py @@ -0,0 +1,123 @@ +# Copyright (c) 2003, Taro Ogawa. All Rights Reserved. +# Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +from __future__ import division, unicode_literals, print_function +from . import lang_EU + +class Num2Word_NO(lang_EU.Num2Word_EU): + def set_high_numwords(self, high): + max = 3 + 6*len(high) + for word, n in zip(high, range(max, 3, -6)): + self.cards[10**n] = word + "illard" + self.cards[10**(n-3)] = word + "illion" + + def setup(self): + self.negword = "minus " + self.pointword = "komma" + self.errmsg_nornum = "Bare tall kan bli konvertert til ord." + self.exclude_title = ["og", "komma", "minus"] + + self.mid_numwords = [(1000, "tusen"), (100, "hundre"), + (90, "nitti"), (80, "\xe5tti"), (70, "sytti"), + (60, "seksti"), (50, "femti"), (40, "f\xf8rti"), + (30, "tretti")] + self.low_numwords = ["tjue", "nitten", "atten", "sytten", + "seksten", "femten", "fjorten", "tretten", + "tolv", "elleve", "ti", "ni", "\xe5tte", + "syv", "seks", "fem", "fire", "tre", "to", + "en", "null"] + self.ords = { "en" : "f\xf8rste", + "to" : "andre", + "tre" : "tredje", + "fire" : "fjerde", + "fem" : "femte", + "seks" : "sjette", + "syv" : "syvende", + "\xe5tte" : "\xe5ttende", + "ni" : "niende", + "ti" : "tiende", + "elleve" : "ellevte", + "tolv" : "tolvte", + "tjue" : "tjuende" } + + + def merge(self, lpair, rpair): + ltext, lnum = lpair + rtext, rnum = rpair + if lnum == 1 and rnum < 100: + return (rtext, rnum) + elif 100 > lnum > rnum : + return ("%s-%s"%(ltext, rtext), lnum + rnum) + elif lnum >= 100 > rnum: + return ("%s og %s"%(ltext, rtext), lnum + rnum) + elif rnum > lnum: + return ("%s %s"%(ltext, rtext), lnum * rnum) + return ("%s, %s"%(ltext, rtext), lnum + rnum) + + + def to_ordinal(self, value): + self.verify_ordinal(value) + outwords = self.to_cardinal(value).split(" ") + lastwords = outwords[-1].split("-") + lastword = lastwords[-1].lower() + try: + lastword = self.ords[lastword] + except KeyError: + if lastword[-2:] == "ti": + lastword = lastword + "ende" + else: + lastword += "de" + lastwords[-1] = self.title(lastword) + outwords[-1] = "".join(lastwords) + return " ".join(outwords) + + + def to_ordinal_num(self, value): + self.verify_ordinal(value) + return "%s%s"%(value, self.to_ordinal(value)[-2:]) + + + def to_year(self, val, longval=True): + if not (val//100)%10: + return self.to_cardinal(val) + return self.to_splitnum(val, hightxt="hundre", jointxt="og", + longval=longval) + + def to_currency(self, val, longval=True): + return self.to_splitnum(val, hightxt="krone/r", lowtxt="\xf8re/r", + jointxt="og", longval=longval, cents = True) + + +n2w = Num2Word_NO() +to_card = n2w.to_cardinal +to_ord = n2w.to_ordinal +to_ordnum = n2w.to_ordinal_num +to_year = n2w.to_year + +def main(): + for val in [ 1, 11, 12, 21, 31, 33, 71, 80, 81, 91, 99, 100, 101, 102, 155, + 180, 300, 308, 832, 1000, 1001, 1061, 1100, 1500, 1701, 3000, + 8280, 8291, 150000, 500000, 1000000, 2000000, 2000001, + -21212121211221211111, -2.121212, -1.0000100]: + n2w.test(val) + n2w.test(1325325436067876801768700107601001012212132143210473207540327057320957032975032975093275093275093270957329057320975093272950730) + for val in [1,120,1000,1120,1800, 1976,2000,2010,2099,2171]: + print(val, "er", n2w.to_currency(val)) + print(val, "er", n2w.to_year(val)) + + +if __name__ == "__main__": + main() diff --git a/num2words/lang_PT_BR.py b/num2words/lang_PT_BR.py new file mode 100644 index 0000000..88a8416 --- /dev/null +++ b/num2words/lang_PT_BR.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2003, Taro Ogawa. All Rights Reserved. +# Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +from __future__ import division, unicode_literals +import re + +from . import lang_EU + + +class Num2Word_PT_BR(lang_EU.Num2Word_EU): + def set_high_numwords(self, high): + max = 3 + 3*len(high) + for word, n in zip(high, range(max, 3, -3)): + self.cards[10**n] = word + "ilhão" + + def setup(self): + self.negword = "menos " + self.pointword = "vírgula" + self.errmsg_nornum = "Somente números podem ser convertidos para palavras" + self.exclude_title = ["e", "vírgula", "menos"] + + self.mid_numwords = [ + (1000, "mil"), (100, "cem"), (90, "noventa"), + (80, "oitenta"), (70, "setenta"), (60, "sessenta"), (50, "cinquenta"), + (40, "quarenta"), (30, "trinta") + ] + self.low_numwords = [ + "vinte", "dezenove", "dezoito", "dezessete", "dezesseis", + "quinze", "catorze", "treze", "doze", "onze", "dez", + "nove", "oito", "sete", "seis", "cinco", "quatro", "três", "dois", + "um", "zero" + ] + self.ords = [ + { + 0: "", + 1: "primeiro", + 2: "segundo", + 3: "terceiro", + 4: "quarto", + 5: "quinto", + 6: "sexto", + 7: "sétimo", + 8: "oitavo", + 9: "nono", + }, + { + 0: "", + 1: "décimo", + 2: "vigésimo", + 3: "trigésimo", + 4: "quadragésimo", + 5: "quinquagésimo", + 6: "sexagésimo", + 7: "septuagésimo", + 8: "octogésimo", + 9: "nonagésimo", + }, + { + 0: "", + 1: "centésimo", + 2: "ducentésimo", + 3: "tricentésimo", + 4: "quadrigentésimo", + 5: "quingentésimo", + 6: "seiscentésimo", + 7: "septigentésimo", + 8: "octigentésimo", + 9: "nongentésimo", + }, + ] + self.thousand_separators = { + 3: "milésimo", + 6: "milionésimo", + 9: "bilionésimo", + 12: "trilionésimo", + 15: "quadrilionésimo" + } + self.hundreds = { + 1: "cento", + 2: "duzentos", + 3: "trezentos", + 4: "quatrocentos", + 5: "quinhentos", + 6: "seiscentos", + 7: "setecentos", + 8: "oitocentos", + 9: "novecentos", + } + + def merge(self, curr, next): + ctext, cnum, ntext, nnum = curr + next + + if cnum == 1: + if nnum < 1000000: + return next + ctext = "um" + elif cnum == 100 and not nnum == 1000: + ctext = "cento" + + if nnum < cnum: + if cnum < 100: + return ("%s e %s" % (ctext, ntext), cnum + nnum) + return ("%s e %s" % (ctext, ntext), cnum + nnum) + + elif (not nnum % 1000000) and cnum > 1: + ntext = ntext[:-4] + "lhões" + + if nnum == 100: + ctext = self.hundreds[cnum] + ntext = "" + + else: + ntext = " " + ntext + + return (ctext + ntext, cnum * nnum) + + def to_cardinal(self, value): + result = super(Num2Word_PT_BR, self).to_cardinal(value) + + # Transforms "mil E cento e catorze reais" into "mil, cento e catorze reais" + for ext in ( + 'mil', 'milhão', 'milhões', 'bilhão', 'bilhões', + 'trilhão', 'trilhões', 'quatrilhão', 'quatrilhões'): + if re.match('.*{} e \w*ento'.format(ext), result): + result = result.replace('{} e'.format(ext), '{},'.format(ext), 1) + + return result + + def to_ordinal(self, value): + self.verify_ordinal(value) + + result = [] + value = str(value) + thousand_separator = '' + + for idx, char in enumerate(value[::-1]): + if idx and idx % 3 == 0: + thousand_separator = self.thousand_separators[idx] + + if char != '0' and thousand_separator: + # avoiding "segundo milionésimo milésimo" for 6000000, for instance + result.append(thousand_separator) + thousand_separator = '' + + result.append(self.ords[idx % 3][int(char)]) + + result = ' '.join(result[::-1]) + result = result.strip() + result = re.sub('\s+', ' ', result) + + if result.startswith('primeiro') and value != '1': + # avoiding "primeiro milésimo", "primeiro milionésimo" and so on + result = result[9:] + + return result + + def to_ordinal_num(self, value): + self.verify_ordinal(value) + return "%sº" % (value) + + def to_year(self, val, longval=True): + if val < 0: + return self.to_cardinal(abs(val)) + ' antes de Cristo' + return self.to_cardinal(val) + + def to_currency(self, val, longval=True): + integer_part, decimal_part = ('%.2f' % val).split('.') + + result = self.to_cardinal(int(integer_part)) + + appended_currency = False + for ext in ( + 'milhão', 'milhões', 'bilhão', 'bilhões', + 'trilhão', 'trilhões', 'quatrilhão', 'quatrilhões'): + if result.endswith(ext): + result += ' de reais' + appended_currency = True + + if result in ['um', 'menos um']: + result += ' real' + appended_currency = True + if not appended_currency: + result += ' reais' + + if int(decimal_part): + cents = self.to_cardinal(int(decimal_part)) + result += ' e ' + cents + + if cents == 'um': + result += ' centavo' + else: + result += ' centavos' + + return result diff --git a/num2words/lang_RU.py b/num2words/lang_RU.py new file mode 100644 index 0000000..922a852 --- /dev/null +++ b/num2words/lang_RU.py @@ -0,0 +1,312 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2003, Taro Ogawa. All Rights Reserved. +# Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA +u""" +>>> from textwrap import fill + +>>> ' '.join([str(i) for i in splitby3('1')]) +u'1' +>>> ' '.join([str(i) for i in splitby3('1123')]) +u'1 123' +>>> ' '.join([str(i) for i in splitby3('1234567890')]) +u'1 234 567 890' + +>>> print(' '.join([n2w(i) for i in range(10)])) +ноль один два три четыре пять шесть семь восемь девять + +>>> print(fill(' '.join([n2w(i+10) for i in range(10)]))) +десять одиннадцать двенадцать тринадцать четырнадцать пятнадцать +шестнадцать семнадцать восемнадцать девятнадцать + +>>> print(fill(' '.join([n2w(i*10) for i in range(10)]))) +ноль десять двадцать тридцать сорок пятьдесят шестьдесят семьдесят +восемьдесят девяносто + +>>> print(n2w(100)) +сто +>>> print(n2w(101)) +сто один +>>> print(n2w(110)) +сто десять +>>> print(n2w(115)) +сто пятнадцать +>>> print(n2w(123)) +сто двадцать три +>>> print(n2w(1000)) +тысяча +>>> print(n2w(1001)) +тысяча один +>>> print(n2w(2012)) +две тысячи двенадцать + +>>> print(n2w(12519.85)) +двенадцать тысяч пятьсот девятнадцать запятая восемьдесят пять + +>>> print(fill(n2w(1234567890))) +миллиард двести тридцать четыре миллиона пятьсот шестьдесят семь тысяч +восемьсот девяносто + +>>> print(fill(n2w(215461407892039002157189883901676))) +двести пятнадцать нониллионов четыреста шестьдесят один октиллион +четыреста семь септиллионов восемьсот девяносто два секстиллиона +тридцать девять квинтиллионов два квадриллиона сто пятьдесят семь +триллионов сто восемьдесят девять миллиардов восемьсот восемьдесят три +миллиона девятьсот одна тысяча шестьсот семьдесят шесть + +>>> print(fill(n2w(719094234693663034822824384220291))) +семьсот девятнадцать нониллионов девяносто четыре октиллиона двести +тридцать четыре септиллиона шестьсот девяносто три секстиллиона +шестьсот шестьдесят три квинтиллиона тридцать четыре квадриллиона +восемьсот двадцать два триллиона восемьсот двадцать четыре миллиарда +триста восемьдесят четыре миллиона двести двадцать тысяч двести +девяносто один + +>>> print(to_currency(1.0, 'EUR')) +один евро, ноль центов + +>>> print(to_currency(1.0, 'RUB')) +один рубль, ноль копеек + +>>> print(to_currency(1234.56, 'EUR')) +тысяча двести тридцать четыре евро, пятьдесят шесть центов + +>>> print(to_currency(1234.56, 'RUB')) +тысяча двести тридцать четыре рубля, пятьдесят шесть копеек + +>>> print(to_currency(10111, 'EUR', seperator=u' и')) +сто один евро и одиннадцать центов + +>>> print(to_currency(10121, 'RUB', seperator=u' и')) +сто один рубль и двадцать одна копейка + +>>> print(to_currency(10122, 'RUB', seperator=u' и')) +сто один рубль и двадцать две копейки + +>>> print(to_currency(10121, 'EUR', seperator=u' и')) +сто один евро и двадцать один цент + +>>> print(to_currency(-1251985, cents = False)) +минус двенадцать тысяч пятьсот девятнадцать евро, 85 центов +""" +from __future__ import unicode_literals + +ZERO = (u'ноль',) + +ONES_FEMININE = { + 1: (u'одна',), + 2: (u'две',), + 3: (u'три',), + 4: (u'четыре',), + 5: (u'пять',), + 6: (u'шесть',), + 7: (u'семь',), + 8: (u'восемь',), + 9: (u'девять',), +} + +ONES = { + 1: (u'один',), + 2: (u'два',), + 3: (u'три',), + 4: (u'четыре',), + 5: (u'пять',), + 6: (u'шесть',), + 7: (u'семь',), + 8: (u'восемь',), + 9: (u'девять',), +} + +TENS = { + 0: (u'десять',), + 1: (u'одиннадцать',), + 2: (u'двенадцать',), + 3: (u'тринадцать',), + 4: (u'четырнадцать',), + 5: (u'пятнадцать',), + 6: (u'шестнадцать',), + 7: (u'семнадцать',), + 8: (u'восемнадцать',), + 9: (u'девятнадцать',), +} + +TWENTIES = { + 2: (u'двадцать',), + 3: (u'тридцать',), + 4: (u'сорок',), + 5: (u'пятьдесят',), + 6: (u'шестьдесят',), + 7: (u'семьдесят',), + 8: (u'восемьдесят',), + 9: (u'девяносто',), +} + +HUNDREDS = { + 1: (u'сто',), + 2: (u'двести',), + 3: (u'триста',), + 4: (u'четыреста',), + 5: (u'пятьсот',), + 6: (u'шестьсот',), + 7: (u'семьсот',), + 8: (u'восемьсот',), + 9: (u'девятьсот',), +} + +THOUSANDS = { + 1: (u'тысяча', u'тысячи', u'тысяч'), # 10^3 + 2: (u'миллион', u'миллиона', u'миллионов'), # 10^6 + 3: (u'миллиард', u'миллиарда', u'миллиардов'), # 10^9 + 4: (u'триллион', u'триллиона', u'триллионов'), # 10^12 + 5: (u'квадриллион', u'квадриллиона', u'квадриллионов'), # 10^15 + 6: (u'квинтиллион', u'квинтиллиона', u'квинтиллионов'), # 10^18 + 7: (u'секстиллион', u'секстиллиона', u'секстиллионов'), # 10^21 + 8: (u'септиллион', u'септиллиона', u'септиллионов'), # 10^24 + 9: (u'октиллион', u'октиллиона', u'октиллионов'), #10^27 + 10: (u'нониллион', u'нониллиона', u'нониллионов'), # 10^30 +} + +CURRENCIES = { + 'RUB': ( + (u'рубль', u'рубля', u'рублей'), (u'копейка', u'копейки', u'копеек') + ), + 'EUR': ( + (u'евро', u'евро', u'евро'), (u'цент', u'цента', u'центов') + ), +} + + +def splitby3(n): + length = len(n) + if length > 3: + start = length % 3 + if start > 0: + yield int(n[:start]) + for i in range(start, length, 3): + yield int(n[i:i+3]) + else: + yield int(n) + + +def get_digits(n): + return [int(x) for x in reversed(list(('%03d' % n)[-3:]))] + + +def pluralize(n, forms): + if (n % 100 < 10 or n % 100 > 20): + if n % 10 == 1: + form = 0 + elif (n % 10 > 1 and n % 10 < 5): + form = 1 + else: + form = 2 + else: + form = 2 + return forms[form] + + +def int2word(n, feminine=False): + if n < 0: + return ' '.join([u'минус', int2word(abs(n))]) + + if n == 0: + return ZERO[0] + + words = [] + chunks = list(splitby3(str(n))) + i = len(chunks) + for x in chunks: + i -= 1 + n1, n2, n3 = get_digits(x) + + if n3 > 0: + words.append(HUNDREDS[n3][0]) + + if n2 > 1: + words.append(TWENTIES[n2][0]) + + if n2 == 1: + words.append(TENS[n1][0]) + elif n1 > 0 and not (i > 0 and x == 1): + ones = ONES_FEMININE if i == 1 or feminine and i == 0 else ONES + words.append(ones[n1][0]) + + if i > 0: + words.append(pluralize(x, THOUSANDS[i])) + + return ' '.join(words) + + +def n2w(n): + n = str(n).replace(',', '.') + if '.' in n: + left, right = n.split('.') + return u'%s запятая %s' % (int2word(int(left)), int2word(int(right))) + else: + return int2word(int(n)) + + +def to_currency(n, currency='EUR', cents=True, seperator=','): + if type(n) == int: + if n < 0: + minus = True + else: + minus = False + + n = abs(n) + left = n / 100 + right = n % 100 + else: + n = str(n).replace(',', '.') + if '.' in n: + left, right = n.split('.') + else: + left, right = n, 0 + left, right = int(left), int(right) + minus = False + cr1, cr2 = CURRENCIES[currency] + + if minus: + minus_str = "минус " + else: + minus_str = "" + + if cents: + cents_feminine = currency == 'RUB' + cents_str = int2word(right, cents_feminine) + else: + cents_str = "%02d" % right + + return u'%s%s %s%s %s %s' % ( + minus_str, + int2word(left), + pluralize(left, cr1), + seperator, + cents_str, + pluralize(right, cr2) + ) + + +class Num2Word_RU(object): + def to_cardinal(self, number): + return n2w(number) + + def to_ordinal(self, number): + raise NotImplementedError() + + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/setup.py b/setup.py index 693ea27..ab6aaf2 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ LONG_DESC = open('README.rst', 'rt').read() + '\n\n' + open('CHANGES.rst', 'rt') setup( name='num2words', - version='0.5.3', + version='0.5.4', description='Modules to convert numbers to words. Easily extensible.', long_description=LONG_DESC, license='LGPL', @@ -29,5 +29,4 @@ setup( url='https://github.com/savoirfairelinux/num2words', packages=find_packages(exclude=['tests']), test_suite='tests', - use_2to3=True, ) diff --git a/tests/test_de.py b/tests/test_de.py new file mode 100644 index 0000000..98e34f2 --- /dev/null +++ b/tests/test_de.py @@ -0,0 +1,51 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2015, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +from __future__ import unicode_literals + +from unittest import TestCase + +from num2words import num2words + +class Num2WordsDETest(TestCase): + def test_ordinal_less_than_twenty(self): + self.assertEqual(num2words(7, ordinal=True, lang='de'), "siebte") + self.assertEqual(num2words(8, ordinal=True, lang='de'), "achte") + self.assertEqual(num2words(12, ordinal=True, lang='de'), "zwölfte") + self.assertEqual(num2words(17, ordinal=True, lang='de'), "siebzehnte") + + def test_ordinal_more_than_twenty(self): + self.assertEqual(num2words(81, ordinal=True, lang='de'), "einundachtzigste") + + def test_ordinal_at_crucial_number(self): + self.assertEqual(num2words(100, ordinal=True, lang='de'), "hundertste") + self.assertEqual(num2words(1000, ordinal=True, lang='de'), "tausendste") + self.assertEqual(num2words(4000, ordinal=True, lang='de'), "viertausendste") + self.assertEqual(num2words(2000000, ordinal=True, lang='de'), "zwei millionenste") + self.assertEqual(num2words(5000000000, ordinal=True, lang='de'), "fünf milliardenste") + + def test_cardinal_at_some_numbers(self): + self.assertEqual(num2words(2000000, lang='de'), "zwei millionen") + self.assertEqual(num2words(4000000000, lang='de'), "vier milliarden") + + def test_cardinal_for_decimal_number(self): + self.assertEqual(num2words(3.486, lang='de'), "drei Komma vier acht") + + def test_ordinal_for_negative_numbers(self): + self.assertRaises(TypeError, num2words, -12, ordinal=True, lang='de') + + def test_ordinal_for_floating_numbers(self): + self.assertRaises(TypeError, num2words, 2.453, ordinal=True, lang='de') \ No newline at end of file diff --git a/tests/test_en.py b/tests/test_en.py index 20eb294..7897e82 100644 --- a/tests/test_en.py +++ b/tests/test_en.py @@ -21,3 +21,10 @@ class Num2WordsENTest(TestCase): def test_and_join_199(self): # ref https://github.com/savoirfairelinux/num2words/issues/8 self.assertEqual(num2words(199), "one hundred and ninety-nine") + + def test_cardinal_for_float_number(self): + # issue 24 + self.assertEqual(num2words(12.50), "twelve point five zero") + self.assertEqual(num2words(12.51), "twelve point five one") + self.assertEqual(num2words(12.53), "twelve point five three") + self.assertEqual(num2words(12.59), "twelve point five nine") \ No newline at end of file diff --git a/tests/test_fr_ch.py b/tests/test_fr_ch.py new file mode 100644 index 0000000..97ba4e5 --- /dev/null +++ b/tests/test_fr_ch.py @@ -0,0 +1,36 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2015, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +from __future__ import unicode_literals + +from unittest import TestCase + +from num2words import num2words + +class Num2WordsENTest(TestCase): + def test_ordinal_special_joins(self): + self.assertEqual(num2words(5, ordinal=True, lang='fr_CH'), "cinquième") + self.assertEqual(num2words(6, ordinal=True, lang='fr_CH'), "sixième") + self.assertEqual(num2words(35, ordinal=True, lang='fr_CH'), "trente-cinquième") + self.assertEqual(num2words(9, ordinal=True, lang='fr_CH'), "neuvième") + self.assertEqual(num2words(49, ordinal=True, lang='fr_CH'), "quarante-neuvième") + self.assertEqual(num2words(71, lang='fr_CH'), "septante et un") + self.assertEqual(num2words(81, lang='fr_CH'), "huitante et un") + self.assertEqual(num2words(80, lang='fr_CH'), "huitante") + self.assertEqual(num2words(880, lang='fr_CH'), "huit cents huitante") + self.assertEqual(num2words(91, ordinal=True, lang='fr_CH'), "nonante et unième") + self.assertEqual(num2words(53, lang='fr_CH'), "cinquante-trois") + diff --git a/tests/test_id.py b/tests/test_id.py new file mode 100644 index 0000000..a1716a0 --- /dev/null +++ b/tests/test_id.py @@ -0,0 +1,49 @@ +# Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +from unittest import TestCase + +from num2words import num2words + +class Num2WordsIDTest(TestCase): + def test_cardinal_for_natural_number(self): + self.assertEqual(num2words(10, lang='id'), "sepuluh") + self.assertEqual(num2words(11, lang='id'), "sebelas") + self.assertEqual(num2words(108, lang='id'), "seratus delapan") + self.assertEqual(num2words(1075, lang='id'), "seribu tujuh puluh lima") + self.assertEqual(num2words(1087231, lang='id'), "satu juta delapan puluh tujuh ribu dua ratus tiga puluh satu") + self.assertEqual(num2words(1000000408, lang='id'), "satu miliar empat ratus delapan") + + def test_cardinal_for_decimal_number(self): + self.assertEqual(num2words(12.234, lang='id'), "dua belas koma dua tiga empat") + self.assertEqual(num2words(9.076, lang='id'), "sembilan koma nol tujuh enam") + + def test_cardinal_for_negative_number(self): + self.assertEqual(num2words(-923, lang='id'), "min sembilan ratus dua puluh tiga") + self.assertEqual(num2words(-0.234, lang='id'), "min nol koma dua tiga empat") + + def test_ordinal_for_natural_number(self): + self.assertEqual(num2words(1, ordinal=True, lang='id'), "pertama") + self.assertEqual(num2words(10, ordinal=True, lang='id'), "kesepuluh") + + #def test_ordinal_numeric_for_natural_number(self): + # self.assertEqual(num2words(1, ordinal=True, lang='id'), "ke-1") + # self.assertEqual(num2words(10, ordinal=True, lang='id'), "ke-10") + + def test_ordinal_for_negative_number(self): + self.assertRaises(TypeError, num2words, -12, ordinal=True, lang='id') + + def test_ordinal_for_floating_number(self): + self.assertRaises(TypeError, num2words, 3.243, ordinal=True, lang='id') diff --git a/tests/test_it.py b/tests/test_it.py new file mode 100644 index 0000000..57bf646 --- /dev/null +++ b/tests/test_it.py @@ -0,0 +1,96 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2015, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +from __future__ import unicode_literals + +from unittest import TestCase + +from num2words import num2words + +class Num2WordsITTest(TestCase): + + def test_number(self): + + test_cases = ( + (1,'uno'), + (2,'due'), + (3,'tre'), + (11,'undici'), + (12,'dodici'), + (16,'sedici'), + (19,'diciannove'), + (20,'venti'), + (21,'ventuno'), + (26,'ventisei'), + (28,'ventotto'), + (30,'trenta'), + (31,'trentuno'), + (40,'quaranta'), + (43,'quarantatre'), + (50,'cinquanta'), + (55,'cinquantacinque'), + (60,'sessanta'), + (67,'sessantasette'), + (70,'settanta'), + (79,'settantanove'), + (100,'cento'), + (101,'centouno'), + (199,'centonovantanove'), + (203,'duecentotre'), + (287,'duecentoottantasette'), + (300,'trecento'), + (356,'trecentocinquantasei'), + (410,'quattrocentodieci'), + (434,'quattrocentotrentaquattro'), + (578,'cinquecentosettantotto'), + (689,'seicentoottantanove'), + (729,'settecentoventinove'), + (894,'ottocentonovantaquattro'), + (999,'novecentonovantanove'), + (1000,'mille'), + (1001,'milleuno'), + (1097,'millenovantasette'), + (1104,'millecentoquattro'), + (1243,'milleduecentoquarantatre'), + (2385,'duemilatrecentoottantacinque'), + (3766,'tremilasettecentosessantasei'), + (4196,'quattromilacentonovantasei'), + (5846,'cinquemilaottocentoquarantasei'), + (6459,'seimilaquattrocentocinquantanove'), + (7232,'settemiladuecentotrentadue'), + (8569,'ottomilacinquecentosessantanove'), + (9539,'novemilacinquecentotrentanove'), + (1000000,'un milione'), + (1000001,'un milioneuno'), + # (1000000100,'un miliardocento'), # DOES NOT WORK TODO: FIX + ) + + for test in test_cases: + self.assertEqual(num2words(test[0], lang='it'), test[1]) + + def test_ordinal(self): + + test_cases = ( + (1,'primo'), + (8,'ottavo'), + (12,'dodicesimo'), + (14,'quattordicesimo'), + (28,'ventottesimo'), + (100,'centesimo'), + ) + + for test in test_cases: + self.assertEqual(num2words(test[0], lang='it', ordinal=True), test[1]) diff --git a/tests/test_pt_BR.py b/tests/test_pt_BR.py new file mode 100644 index 0000000..a681d91 --- /dev/null +++ b/tests/test_pt_BR.py @@ -0,0 +1,219 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2015, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +from __future__ import unicode_literals + +from decimal import Decimal +from unittest import TestCase + +from num2words import num2words +from num2words.lang_PT_BR import Num2Word_PT_BR + + +class Num2WordsPTBRTest(TestCase): + def setUp(self): + super(Num2WordsPTBRTest, self).setUp() + self.n2w = Num2Word_PT_BR() + + def test_cardinal_integer(self): + self.assertEqual(num2words(1, lang='pt_BR'), 'um') + self.assertEqual(num2words(2, lang='pt_BR'), 'dois') + self.assertEqual(num2words(3, lang='pt_BR'), 'três') + self.assertEqual(num2words(4, lang='pt_BR'), 'quatro') + self.assertEqual(num2words(5, lang='pt_BR'), 'cinco') + self.assertEqual(num2words(6, lang='pt_BR'), 'seis') + self.assertEqual(num2words(7, lang='pt_BR'), 'sete') + self.assertEqual(num2words(8, lang='pt_BR'), 'oito') + self.assertEqual(num2words(9, lang='pt_BR'), 'nove') + self.assertEqual(num2words(10, lang='pt_BR'), 'dez') + self.assertEqual(num2words(11, lang='pt_BR'), 'onze') + self.assertEqual(num2words(12, lang='pt_BR'), 'doze') + self.assertEqual(num2words(13, lang='pt_BR'), 'treze') + self.assertEqual(num2words(14, lang='pt_BR'), 'catorze') + self.assertEqual(num2words(15, lang='pt_BR'), 'quinze') + self.assertEqual(num2words(16, lang='pt_BR'), 'dezesseis') + self.assertEqual(num2words(17, lang='pt_BR'), 'dezessete') + self.assertEqual(num2words(18, lang='pt_BR'), 'dezoito') + self.assertEqual(num2words(19, lang='pt_BR'), 'dezenove') + self.assertEqual(num2words(20, lang='pt_BR'), 'vinte') + + self.assertEqual(num2words(21, lang='pt_BR'), 'vinte e um') + self.assertEqual(num2words(22, lang='pt_BR'), 'vinte e dois') + self.assertEqual(num2words(35, lang='pt_BR'), 'trinta e cinco') + self.assertEqual(num2words(99, lang='pt_BR'), 'noventa e nove') + + self.assertEqual(num2words(100, lang='pt_BR'), 'cem') + self.assertEqual(num2words(101, lang='pt_BR'), 'cento e um') + self.assertEqual(num2words(128, lang='pt_BR'), 'cento e vinte e oito') + self.assertEqual(num2words(713, lang='pt_BR'), 'setecentos e treze') + + self.assertEqual(num2words(1000, lang='pt_BR'), 'mil') + self.assertEqual(num2words(1001, lang='pt_BR'), 'mil e um') + self.assertEqual(num2words(1111, lang='pt_BR'), 'mil, cento e onze') + self.assertEqual(num2words(2114, lang='pt_BR'), 'dois mil, cento e catorze') + self.assertEqual(num2words(73421, lang='pt_BR'), 'setenta e três mil, quatrocentos e vinte e um') + + self.assertEqual(num2words(100000, lang='pt_BR'), 'cem mil') + self.assertEqual(num2words(250050, lang='pt_BR'), 'duzentos e cinquenta mil e cinquenta') + self.assertEqual(num2words(6000000, lang='pt_BR'), 'seis milhões') + self.assertEqual(num2words(19000000000, lang='pt_BR'), 'dezenove bilhões') + self.assertEqual(num2words(145000000002, lang='pt_BR'), 'cento e quarenta e cinco bilhões e dois') + + def test_cardinal_integer_negative(self): + self.assertEqual(num2words(-1, lang='pt_BR'), 'menos um') + self.assertEqual(num2words(-256, lang='pt_BR'), 'menos duzentos e cinquenta e seis') + self.assertEqual(num2words(-1000, lang='pt_BR'), 'menos mil') + self.assertEqual(num2words(-1000000, lang='pt_BR'), 'menos um milhão') + self.assertEqual(num2words(-1234567, lang='pt_BR'), 'menos um milhão, duzentos e trinta e quatro mil, quinhentos e sessenta e sete') + + def test_cardinal_float(self): + self.assertEqual(num2words(Decimal('1.00'), lang='pt_BR'), 'um') + self.assertEqual(num2words(Decimal('1.01'), lang='pt_BR'), 'um vírgula zero um') + self.assertEqual(num2words(Decimal('1.035'), lang='pt_BR'), 'um vírgula zero três') + self.assertEqual(num2words(Decimal('1.35'), lang='pt_BR'), 'um vírgula três cinco') + self.assertEqual(num2words(Decimal('3.14159'), lang='pt_BR'), 'três vírgula um quatro') + self.assertEqual(num2words(Decimal('101.22'), lang='pt_BR'), 'cento e um vírgula dois dois') + self.assertEqual(num2words(Decimal('2345.75'), lang='pt_BR'), 'dois mil, trezentos e quarenta e cinco vírgula sete cinco') + + def test_cardinal_float_negative(self): + self.assertEqual(num2words(Decimal('-2.34'), lang='pt_BR'), 'menos dois vírgula três quatro') + self.assertEqual(num2words(Decimal('-9.99'), lang='pt_BR'), 'menos nove vírgula nove nove') + self.assertEqual(num2words(Decimal('-7.01'), lang='pt_BR'), 'menos sete vírgula zero um') + self.assertEqual(num2words(Decimal('-222.22'), lang='pt_BR'), 'menos duzentos e vinte e dois vírgula dois dois') + + def test_ordinal(self): + self.assertEqual(num2words(1, lang='pt_BR', ordinal=True), 'primeiro') + self.assertEqual(num2words(2, lang='pt_BR', ordinal=True), 'segundo') + self.assertEqual(num2words(3, lang='pt_BR', ordinal=True), 'terceiro') + self.assertEqual(num2words(4, lang='pt_BR', ordinal=True), 'quarto') + self.assertEqual(num2words(5, lang='pt_BR', ordinal=True), 'quinto') + self.assertEqual(num2words(6, lang='pt_BR', ordinal=True), 'sexto') + self.assertEqual(num2words(7, lang='pt_BR', ordinal=True), 'sétimo') + self.assertEqual(num2words(8, lang='pt_BR', ordinal=True), 'oitavo') + self.assertEqual(num2words(9, lang='pt_BR', ordinal=True), 'nono') + self.assertEqual(num2words(10, lang='pt_BR', ordinal=True), 'décimo') + self.assertEqual(num2words(11, lang='pt_BR', ordinal=True), 'décimo primeiro') + self.assertEqual(num2words(12, lang='pt_BR', ordinal=True), 'décimo segundo') + self.assertEqual(num2words(13, lang='pt_BR', ordinal=True), 'décimo terceiro') + self.assertEqual(num2words(14, lang='pt_BR', ordinal=True), 'décimo quarto') + self.assertEqual(num2words(15, lang='pt_BR', ordinal=True), 'décimo quinto') + self.assertEqual(num2words(16, lang='pt_BR', ordinal=True), 'décimo sexto') + self.assertEqual(num2words(17, lang='pt_BR', ordinal=True), 'décimo sétimo') + self.assertEqual(num2words(18, lang='pt_BR', ordinal=True), 'décimo oitavo') + self.assertEqual(num2words(19, lang='pt_BR', ordinal=True), 'décimo nono') + self.assertEqual(num2words(20, lang='pt_BR', ordinal=True), 'vigésimo') + + self.assertEqual(num2words(21, lang='pt_BR', ordinal=True), 'vigésimo primeiro') + self.assertEqual(num2words(22, lang='pt_BR', ordinal=True), 'vigésimo segundo') + self.assertEqual(num2words(35, lang='pt_BR', ordinal=True), 'trigésimo quinto') + self.assertEqual(num2words(99, lang='pt_BR', ordinal=True), 'nonagésimo nono') + + self.assertEqual(num2words(100, lang='pt_BR', ordinal=True), 'centésimo') + self.assertEqual(num2words(101, lang='pt_BR', ordinal=True), 'centésimo primeiro') + self.assertEqual(num2words(128, lang='pt_BR', ordinal=True), 'centésimo vigésimo oitavo') + self.assertEqual(num2words(713, lang='pt_BR', ordinal=True), 'septigentésimo décimo terceiro') + + self.assertEqual(num2words(1000, lang='pt_BR', ordinal=True), 'milésimo') + self.assertEqual(num2words(1001, lang='pt_BR', ordinal=True), 'milésimo primeiro') + self.assertEqual(num2words(1111, lang='pt_BR', ordinal=True), 'milésimo centésimo décimo primeiro') + self.assertEqual(num2words(2114, lang='pt_BR', ordinal=True), 'segundo milésimo centésimo décimo quarto') + self.assertEqual(num2words(73421, lang='pt_BR', ordinal=True), 'septuagésimo terceiro milésimo quadrigentésimo vigésimo primeiro') + + self.assertEqual(num2words(100000, lang='pt_BR', ordinal=True), 'centésimo milésimo') + self.assertEqual(num2words(250050, lang='pt_BR', ordinal=True), 'ducentésimo quinquagésimo milésimo quinquagésimo') + self.assertEqual(num2words(6000000, lang='pt_BR', ordinal=True), 'sexto milionésimo') + self.assertEqual(num2words(19000000000, lang='pt_BR', ordinal=True), 'décimo nono bilionésimo') + self.assertEqual(num2words(145000000002, lang='pt_BR', ordinal=True), 'centésimo quadragésimo quinto bilionésimo segundo') + + def test_currency_integer(self): + self.assertEqual(self.n2w.to_currency(1), 'um real') + self.assertEqual(self.n2w.to_currency(2), 'dois reais') + self.assertEqual(self.n2w.to_currency(3), 'três reais') + self.assertEqual(self.n2w.to_currency(4), 'quatro reais') + self.assertEqual(self.n2w.to_currency(5), 'cinco reais') + self.assertEqual(self.n2w.to_currency(6), 'seis reais') + self.assertEqual(self.n2w.to_currency(7), 'sete reais') + self.assertEqual(self.n2w.to_currency(8), 'oito reais') + self.assertEqual(self.n2w.to_currency(9), 'nove reais') + self.assertEqual(self.n2w.to_currency(10), 'dez reais') + self.assertEqual(self.n2w.to_currency(11), 'onze reais') + self.assertEqual(self.n2w.to_currency(12), 'doze reais') + self.assertEqual(self.n2w.to_currency(13), 'treze reais') + self.assertEqual(self.n2w.to_currency(14), 'catorze reais') + self.assertEqual(self.n2w.to_currency(15), 'quinze reais') + self.assertEqual(self.n2w.to_currency(16), 'dezesseis reais') + self.assertEqual(self.n2w.to_currency(17), 'dezessete reais') + self.assertEqual(self.n2w.to_currency(18), 'dezoito reais') + self.assertEqual(self.n2w.to_currency(19), 'dezenove reais') + self.assertEqual(self.n2w.to_currency(20), 'vinte reais') + + self.assertEqual(self.n2w.to_currency(21), 'vinte e um reais') + self.assertEqual(self.n2w.to_currency(22), 'vinte e dois reais') + self.assertEqual(self.n2w.to_currency(35), 'trinta e cinco reais') + self.assertEqual(self.n2w.to_currency(99), 'noventa e nove reais') + + self.assertEqual(self.n2w.to_currency(100), 'cem reais') + self.assertEqual(self.n2w.to_currency(101), 'cento e um reais') + self.assertEqual(self.n2w.to_currency(128), 'cento e vinte e oito reais') + self.assertEqual(self.n2w.to_currency(713), 'setecentos e treze reais') + + self.assertEqual(self.n2w.to_currency(1000), 'mil reais') + self.assertEqual(self.n2w.to_currency(1001), 'mil e um reais') + self.assertEqual(self.n2w.to_currency(1111), 'mil, cento e onze reais') + self.assertEqual(self.n2w.to_currency(2114), 'dois mil, cento e catorze reais') + self.assertEqual(self.n2w.to_currency(73421), 'setenta e três mil, quatrocentos e vinte e um reais') + + self.assertEqual(self.n2w.to_currency(100000), 'cem mil reais') + self.assertEqual(self.n2w.to_currency(250050), 'duzentos e cinquenta mil e cinquenta reais') + self.assertEqual(self.n2w.to_currency(6000000), 'seis milhões de reais') + self.assertEqual(self.n2w.to_currency(19000000000), 'dezenove bilhões de reais') + self.assertEqual(self.n2w.to_currency(145000000002), 'cento e quarenta e cinco bilhões e dois reais') + + def test_currency_integer_negative(self): + self.assertEqual(self.n2w.to_currency(-1), 'menos um real') + self.assertEqual(self.n2w.to_currency(-256), 'menos duzentos e cinquenta e seis reais') + self.assertEqual(self.n2w.to_currency(-1000), 'menos mil reais') + self.assertEqual(self.n2w.to_currency(-1000000), 'menos um milhão de reais') + self.assertEqual(self.n2w.to_currency(-1234567), 'menos um milhão, duzentos e trinta e quatro mil, quinhentos e sessenta e sete reais') + + def test_currency_float(self): + self.assertEqual(self.n2w.to_currency(Decimal('1.00')), 'um real') + self.assertEqual(self.n2w.to_currency(Decimal('1.01')), 'um real e um centavo') + self.assertEqual(self.n2w.to_currency(Decimal('1.035')), 'um real e três centavos') + self.assertEqual(self.n2w.to_currency(Decimal('1.35')), 'um real e trinta e cinco centavos') + self.assertEqual(self.n2w.to_currency(Decimal('3.14159')), 'três reais e catorze centavos') + self.assertEqual(self.n2w.to_currency(Decimal('101.22')), 'cento e um reais e vinte e dois centavos') + self.assertEqual(self.n2w.to_currency(Decimal('2345.75')), 'dois mil, trezentos e quarenta e cinco reais e setenta e cinco centavos') + + def test_currency_float_negative(self): + self.assertEqual(self.n2w.to_currency(Decimal('-2.34')), 'menos dois reais e trinta e quatro centavos') + self.assertEqual(self.n2w.to_currency(Decimal('-9.99')), 'menos nove reais e noventa e nove centavos') + self.assertEqual(self.n2w.to_currency(Decimal('-7.01')), 'menos sete reais e um centavo') + self.assertEqual(self.n2w.to_currency(Decimal('-222.22')), 'menos duzentos e vinte e dois reais e vinte e dois centavos') + + def test_year(self): + self.assertEqual(self.n2w.to_year(1001), 'mil e um') + self.assertEqual(self.n2w.to_year(1789), 'mil, setecentos e oitenta e nove') + self.assertEqual(self.n2w.to_year(1942), 'mil, novecentos e quarenta e dois') + self.assertEqual(self.n2w.to_year(1984), 'mil, novecentos e oitenta e quatro') + self.assertEqual(self.n2w.to_year(2000), 'dois mil') + self.assertEqual(self.n2w.to_year(2001), 'dois mil e um') + self.assertEqual(self.n2w.to_year(2016), 'dois mil e dezesseis') + + def test_year_negative(self): + self.assertEqual(self.n2w.to_year(-30), 'trinta antes de Cristo') + self.assertEqual(self.n2w.to_year(-744), 'setecentos e quarenta e quatro antes de Cristo') + self.assertEqual(self.n2w.to_year(-10000), 'dez mil antes de Cristo') diff --git a/tests/test_ru.py b/tests/test_ru.py new file mode 100644 index 0000000..9ba224b --- /dev/null +++ b/tests/test_ru.py @@ -0,0 +1,31 @@ +# -*- encoding: utf-8 -*- +# Copyright (c) 2013, Savoir-faire Linux inc. All Rights Reserved. + +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301 USA + +from unittest import TestCase + +from num2words import num2words + +class Num2WordsRUTest(TestCase): + + def test_cardinal(self): + self.assertEqual(num2words(5, lang='ru'), u"пять") + self.assertEqual(num2words(15, lang='ru'), u"пятнадцать") + self.assertEqual(num2words(154, lang='ru'), u"сто пятьдесят четыре") + self.assertEqual(num2words(418531, lang='ru'), u"четыреста восемнадцать тысяч пятьсот тридцать один") + + def test_floating_point(self): + self.assertEqual(num2words(5.2, lang='ru'), u"пять запятая два") + self.assertEqual(num2words(561.42, lang='ru'), u"пятьсот шестьдесят один запятая сорок два") diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..0e59285 --- /dev/null +++ b/tox.ini @@ -0,0 +1,5 @@ +[tox] +envlist = py27,py34,py35,py36 + +[testenv] +commands = python -m unittest discover