607 lines
20 KiB
Python
607 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2023, Johannes Heinecke. 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 .currency import parse_currency_parts
|
|
from .lang_EU import Num2Word_EU
|
|
|
|
# Welsh numerals differs to many other languages since the counted
|
|
# object does not follow the numeral but is inserted between
|
|
# e.g. "23 hours" is
|
|
# tri awr ar hugain
|
|
# 3 hour on twenty
|
|
# in addition to that some numeral trigger a mutation on the following word
|
|
# either another numeral or the counted object
|
|
# (https://en.wikipedia.org/wiki/Consonant_mutation#Welsh)
|
|
# e.g. "23 dogs" (aspirated mutation, c -> ch)
|
|
# tri chi ar hugain
|
|
# 3 dog on twenty
|
|
# but "22 dogs" (soft mutation, c -> g)
|
|
# dau gi ar hugain
|
|
# 2 dog on twenty
|
|
# and "24 dogs" (no mutation)
|
|
# pedwar ci ar hugain
|
|
# 4 dog on twenty
|
|
# (BTW, the counted word is always in singular when following a numeral)
|
|
# numerals are mutated as well
|
|
# e.g. "300"
|
|
# tri chant
|
|
# 3 hundred
|
|
# "200"
|
|
# dau gant
|
|
# 2 hundred
|
|
# "500"
|
|
# pump cant
|
|
# 5 hundreds
|
|
# the numerals for 2, 3 and 4 are different in function of gender (MASC, FEM)
|
|
# 2 cats
|
|
# dwy gath
|
|
|
|
# 2 dogs
|
|
# dau gi
|
|
|
|
# 2000
|
|
# dwy fil
|
|
|
|
# 3000
|
|
# tair mil
|
|
|
|
# to add the counted object in the correct position use
|
|
# num2words(17, lang="cy", counted="ci", gender="masc")
|
|
# num2words(17, lang="cy", counted="cath", gender="fem")
|
|
# if the number is > 99, use plural form of counted object
|
|
# num2words(117, lang="cy", counted="cathod", gender="fem")
|
|
|
|
|
|
# Globals
|
|
# -------
|
|
|
|
OBJ = "__OBJECT__"
|
|
|
|
CARDINAL_WORDS = {
|
|
# masc, fem, triggers mutation
|
|
0: [("dim", None), (OBJ, None)],
|
|
1: [("un", None), (OBJ, None)],
|
|
2: [("dau", "SM"), (OBJ, None)],
|
|
3: [("tri", "AM"), (OBJ, None)],
|
|
4: [("pedwar", None), (OBJ, None)],
|
|
5: [("pump", None), (OBJ, None)],
|
|
6: [("chwech", "AM"), (OBJ, None)],
|
|
7: [("saith", None), (OBJ, None)],
|
|
8: [("wyth", None), (OBJ, None)],
|
|
9: [("naw", None), (OBJ, None)],
|
|
10: [("deg", None), (OBJ, None)],
|
|
11: [("un", None), (OBJ, None), ("ar ddeg", None)],
|
|
12: [("deuddeg", None), (OBJ, None)],
|
|
13: [("tri", "AM"), (OBJ, None), ("ar ddeg", None)],
|
|
14: [("pedwar", None), (OBJ, None), ("ar ddeg", None)],
|
|
15: [("pymtheg", None), (OBJ, None)],
|
|
16: [("un", None), (OBJ, None), ("ar bymtheg", None)],
|
|
17: [("dau", "SM"), (OBJ, None), ("ar bymtheg", None)],
|
|
18: [("deunaw", None), (OBJ, None)],
|
|
19: [("pedwar", None), ("ar bymtheg", None)],
|
|
}
|
|
|
|
CARDINAL_WORDS_FEM = {
|
|
# masc, fem, triggers mutation
|
|
0: [("dim", None), (OBJ, None)],
|
|
1: [("un", None), (OBJ, None)],
|
|
2: [("dwy", "SM"), (OBJ, None)],
|
|
3: [("tair", None), (OBJ, None)],
|
|
4: [("pedair", None), (OBJ, None)],
|
|
5: [("pump", None), (OBJ, None)],
|
|
6: [("chwech", "AM"), (OBJ, None)],
|
|
7: [("saith", None), (OBJ, None)],
|
|
8: [("wyth", None), (OBJ, None)],
|
|
9: [("naw", None), (OBJ, None)],
|
|
10: [("deg", None), (OBJ, None)],
|
|
11: [("un", None), (OBJ, None), ("ar ddeg", None)],
|
|
12: [("deuddeg", None), (OBJ, None)],
|
|
13: [("tair", None), (OBJ, None), ("ar ddeg", None)],
|
|
14: [("pedair", None), (OBJ, None), ("ar ddeg", None)],
|
|
15: [("pymtheg", None), (OBJ, None)],
|
|
16: [("un", None), (OBJ, None), ("ar bymtheg", None)],
|
|
17: [("dwy", "SM"), (OBJ, None), ("ar bymtheg", None)],
|
|
18: [("deunaw", None), (OBJ, None)],
|
|
19: [("pedair", None), ("ar bymtheg", None)],
|
|
}
|
|
|
|
MILLION_WORDS = {
|
|
3: ("mil", None),
|
|
6: ("miliwn", None),
|
|
9: ("biliwn", None),
|
|
12: ("triliwn", None),
|
|
15: ("cwadriliwn", None),
|
|
18: ("cwintiliwn", None),
|
|
21: ("secsttiliwn", None),
|
|
24: ("septiliwn", None),
|
|
27: ("octiliwn", None),
|
|
30: ("noniliwn", None),
|
|
33: ("dengiliwn", None),
|
|
}
|
|
|
|
ORDINAL_WORDS = {
|
|
0: [("dimfed", None), (OBJ, None)],
|
|
1: [(OBJ, None), ("cyntaf", None)],
|
|
2: [("ail", "SM"), (OBJ, None)],
|
|
3: [("trydydd", None), (OBJ, None)],
|
|
4: [("pedwerydd", None), (OBJ, None)],
|
|
5: [("pumed", None), (OBJ, None)],
|
|
6: [("chweched", None), (OBJ, None)],
|
|
7: [("saithfed", None), (OBJ, None)],
|
|
8: [("wythfed", None), (OBJ, None)],
|
|
9: [("nawfed", None), (OBJ, None)],
|
|
10: [("degfed", None), (OBJ, None)],
|
|
11: [("unfed", "SM"), (OBJ, None), ("ar ddeg", None)],
|
|
12: [("deuddegfed", None), (OBJ, None)],
|
|
13: [("trydydd", None), (OBJ, None), ("ar ddeg", None)],
|
|
14: [("pedwerydd", None), (OBJ, None), ("ar ddeg", None)],
|
|
15: [("pymthegfed", None), (OBJ, None)],
|
|
16: [("unfed", None), (OBJ, None), ("ar bymtheg", None)],
|
|
17: [("ail", "SM"), (OBJ, None), ("ar bymtheg", None)],
|
|
18: [("deunawfed", None), (OBJ, None)],
|
|
19: [("pedwerydd", None), (OBJ, None), ("ar bymtheg", None)],
|
|
}
|
|
ORDINAL_WORDS_FEM = {
|
|
0: [("dimfed", None), (OBJ, None)],
|
|
1: [(OBJ, None), ("gyntaf", None)],
|
|
2: [("ail", "SM"), (OBJ, None)],
|
|
3: [("trydedd", "SM"), (OBJ, None)],
|
|
4: [("pedwaredd", "SM"), (OBJ, None)],
|
|
5: [("pumed", None), (OBJ, None)],
|
|
6: [("chweched", None), (OBJ, None)],
|
|
7: [("saithfed", None), (OBJ, None)],
|
|
8: [("wythfed", None), (OBJ, None)],
|
|
9: [("nawfed", None), (OBJ, None)],
|
|
10: [("degfed", None), (OBJ, None)],
|
|
11: [("unfed", "SM"), (OBJ, None), ("ar ddeg", None)],
|
|
12: [("deuddegfed", None), (OBJ, None)],
|
|
13: [("trydedd", "SM"), (OBJ, None), ("ar ddeg", None)],
|
|
14: [("pedwaredd", "SM"), (OBJ, None), ("ar ddeg", None)],
|
|
15: [("pymthegfed", None), (OBJ, None)],
|
|
16: [("unfed", None), (OBJ, None), ("ar bymtheg", None)],
|
|
17: [("ail", "SM"), (OBJ, None), ("ar bymtheg", None)],
|
|
18: [("deunawfed", None), (OBJ, None)],
|
|
19: [("pedwaredd", None), (OBJ, None), ("ar bymtheg", None)],
|
|
}
|
|
|
|
# The script can extrapolate the missing numbers from the base forms.
|
|
STR_TENS = {
|
|
1: [("ugain", None), (OBJ, None)],
|
|
2: [("deugain", None), (OBJ, None)],
|
|
3: [("trigain", None), (OBJ, None)],
|
|
4: [("pedwar ugain", None), (OBJ, None)],
|
|
}
|
|
|
|
ORD_STR_TENS = {
|
|
1: [("ugainfed", None), (OBJ, None)],
|
|
2: [("deugainfed", None), (OBJ, None)],
|
|
3: [("trigainfed", None), (OBJ, None)],
|
|
4: [("pedwar ugainfed", None), (OBJ, None)],
|
|
}
|
|
|
|
STR_TENS_INFORMAL = {
|
|
1: ("undeg", None),
|
|
2: ("dauddeg", None),
|
|
3: ("trideg", None),
|
|
4: ("pedwardeg", None),
|
|
5: ("pumdeg", None),
|
|
6: ("chwedeg", None),
|
|
7: ("saithdeg", None),
|
|
8: ("wythdeg", None),
|
|
9: ("nawdeg", None),
|
|
}
|
|
|
|
|
|
GENERIC_DOLLARS = ("dolar", "dolarau")
|
|
GENERIC_CENTS = ("ceiniog", "ceiniogau")
|
|
|
|
CURRENCIES_FEM = ["GBP"]
|
|
|
|
|
|
class Num2Word_CY(Num2Word_EU):
|
|
CURRENCY_FORMS = {
|
|
# currency code: (sg, pl), (sg, pl)
|
|
# in Welsh a noun after a numeral is ALWAYS in the singular
|
|
"EUR": (("euro", "euros"), GENERIC_CENTS),
|
|
"USD": (GENERIC_DOLLARS, GENERIC_CENTS),
|
|
"GBP": (("punt", "punnoedd"), ("ceiniog", "ceiniogau")),
|
|
"CNY": (("yuan", "yuans"), ("ffen", "ffens")),
|
|
}
|
|
|
|
MINUS_PREFIX_WORD = "meinws "
|
|
FLOAT_INFIX_WORD = " pwynt "
|
|
|
|
# def setup(self):
|
|
# Num2Word_EU.setup(self)
|
|
|
|
def __init__(self):
|
|
pass
|
|
|
|
def float_to_words(self, float_number):
|
|
# if ordinal:
|
|
# prefix = self.to_ordinal(int(float_number))
|
|
# else:
|
|
prefix = self.to_cardinal(int(float_number))
|
|
float_part = str(float_number).split(".")[1]
|
|
postfix = " ".join(
|
|
# Drops the trailing zero and comma
|
|
[self.to_cardinal(int(c)) for c in float_part]
|
|
)
|
|
return prefix + Num2Word_CY.FLOAT_INFIX_WORD + postfix
|
|
|
|
def hundred_group(
|
|
self, number, informal=False, gender="masc", ordinal=False
|
|
):
|
|
hundreds = number // 100
|
|
until100 = number % 100 # 0 - 99
|
|
# list group of number words and mutation info (for the following word)
|
|
result = (
|
|
[]
|
|
)
|
|
if gender == "fem":
|
|
CW = CARDINAL_WORDS_FEM
|
|
else:
|
|
if ordinal:
|
|
CW = ORDINAL_WORDS
|
|
else:
|
|
CW = CARDINAL_WORDS
|
|
|
|
if hundreds > 0:
|
|
if hundreds > 1:
|
|
result.extend((CARDINAL_WORDS[hundreds]))
|
|
result.extend([("cant", None), (OBJ, None)])
|
|
if until100:
|
|
if until100 in [
|
|
1,
|
|
8,
|
|
11,
|
|
16,
|
|
20,
|
|
21,
|
|
31,
|
|
36,
|
|
41,
|
|
48,
|
|
61,
|
|
68,
|
|
71,
|
|
81,
|
|
88,
|
|
91,
|
|
]:
|
|
result.append(("ac", None))
|
|
else:
|
|
result.append(("a", "AM"))
|
|
if until100:
|
|
# if informal:
|
|
# pass
|
|
if not ordinal and until100 >= 50 and until100 <= 59:
|
|
units = number % 10
|
|
if hundreds > 0:
|
|
if units == 0:
|
|
result.append(("hanner", None))
|
|
elif units == 1:
|
|
result.extend([("hanner ac un", None), (OBJ, None)])
|
|
else:
|
|
result.append(("hanner a", "AM"))
|
|
result.extend(CW[units])
|
|
else:
|
|
if units == 0:
|
|
result.extend([("hanner cant", None), (OBJ, None)])
|
|
elif units == 1:
|
|
result.extend(
|
|
[("hanner cant ac un", None), (OBJ, None)]
|
|
)
|
|
else:
|
|
result.append(("hanner cant a", "AM"))
|
|
result.extend(CW[units])
|
|
else:
|
|
if (number < 20 and number > 0) or (
|
|
number == 0 and hundreds == 0
|
|
):
|
|
if gender == "fem":
|
|
result.extend(CARDINAL_WORDS_FEM[int(number)])
|
|
else:
|
|
result.extend(CARDINAL_WORDS[int(number)])
|
|
|
|
else:
|
|
tens = until100 // 20
|
|
units = number % 20
|
|
if ordinal and units == 0:
|
|
degau = ORD_STR_TENS.get(tens)
|
|
else:
|
|
degau = STR_TENS.get(tens)
|
|
|
|
if units != 0:
|
|
if tens > 1:
|
|
result.extend(CW[units])
|
|
if degau:
|
|
result.append(("a", "AM"))
|
|
result.extend(degau)
|
|
else:
|
|
result.extend(CW[units])
|
|
if degau:
|
|
result.append(("ar", "SM"))
|
|
result.extend(degau)
|
|
elif degau:
|
|
result.extend(degau)
|
|
return result
|
|
|
|
def to_ordinal(self, number, informal=False, gender="masc"):
|
|
if number < 20:
|
|
return makestring(ORDINAL_WORDS[number])
|
|
if number == 100:
|
|
return "canfed"
|
|
elif number > 100:
|
|
raise NotImplementedError("The given number is too large.")
|
|
|
|
return self.to_cardinal(
|
|
number, informal=False, gender=gender, ordinal=True
|
|
)
|
|
|
|
def to_cardinal(
|
|
self,
|
|
number,
|
|
informal=False,
|
|
gender="masc",
|
|
ordinal=False,
|
|
counted=None,
|
|
raw=False,
|
|
):
|
|
negative = False
|
|
if number < 0:
|
|
negative = True
|
|
number = -1 * number
|
|
if number == 0:
|
|
if raw:
|
|
return CARDINAL_WORDS[0]
|
|
else:
|
|
return makestring(CARDINAL_WORDS[0])
|
|
elif not number < 999 * 10**33:
|
|
raise NotImplementedError("The given number is too large.")
|
|
|
|
elif isinstance(number, float):
|
|
return self.float_to_words(number)
|
|
|
|
# split in groups of 10**3
|
|
# groups of three digits starting from right (units (1 - 999),
|
|
# thousands, millions, ...)
|
|
groups = (
|
|
[]
|
|
)
|
|
lowestgroup = (
|
|
None # find the lowest group of 3 digits > 0 for the ordinals
|
|
)
|
|
for pot in [3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36]:
|
|
gr = (number % 10**pot) // 10 ** (pot - 3)
|
|
groups.append((gr, pot))
|
|
if gr and not lowestgroup:
|
|
lowestgroup = gr
|
|
# print("groups", groups)
|
|
|
|
result = []
|
|
if negative:
|
|
result.append(("meinws", None))
|
|
|
|
for gr, pot in reversed(groups):
|
|
if gr:
|
|
# print("AAAA", gr, pot, gender)
|
|
if pot == 6:
|
|
g = "fem" # mil (1000) is feminine
|
|
elif pot == 3:
|
|
g = gender # units depend on the following noun
|
|
else:
|
|
g = "masc" # millions etc are masculine
|
|
# "mil" is feminine
|
|
if gr > 1 or pot == 3:
|
|
words = self.hundred_group(
|
|
gr,
|
|
informal=informal,
|
|
gender=g,
|
|
ordinal=ordinal and (lowestgroup == gr),
|
|
)
|
|
result += words
|
|
# print(">>>> ", words)
|
|
if pot > 3:
|
|
result.append(MILLION_WORDS[pot - 3])
|
|
if raw:
|
|
# need to be able trigger correct mutation on currencies
|
|
return result
|
|
else:
|
|
if number < 100:
|
|
return makestring(result, counted=counted)
|
|
else:
|
|
if counted:
|
|
result.extend([("o", "SM"), (counted, None)])
|
|
return makestring(result)
|
|
|
|
def to_currency(
|
|
self, val, currency="EUR", cents=True, separator=",", adjective=False
|
|
):
|
|
"""
|
|
Args:
|
|
val: Numeric value
|
|
currency (str): Currency code
|
|
cents (bool): Verbose cents
|
|
separator (str): Cent separator
|
|
adjective (bool): Prefix currency name with adjective
|
|
Returns:
|
|
str: Formatted string
|
|
|
|
"""
|
|
left, right, is_negative = parse_currency_parts(val)
|
|
try:
|
|
cr1, cr2 = self.CURRENCY_FORMS[currency]
|
|
|
|
except KeyError:
|
|
raise NotImplementedError(
|
|
'Currency code "%s" not implemented for "%s"'
|
|
% (currency, self.__class__.__name__)
|
|
)
|
|
|
|
# if adjective and currency in self.CURRENCY_ADJECTIVES:
|
|
# cr1 = prefix_currency(self.CURRENCY_ADJECTIVES[currency], cr1)
|
|
|
|
minus_str = "%s " % self.negword.strip() if is_negative else ""
|
|
money_str = self._money_verbose(left, currency)
|
|
cents_str = (
|
|
self._cents_verbose(right, currency)
|
|
if cents
|
|
else self._cents_terse(right, currency)
|
|
)
|
|
|
|
if right == 0:
|
|
# no pence
|
|
return "%s%s" % (
|
|
minus_str,
|
|
money_str,
|
|
# self.pluralize(right, cr2)
|
|
)
|
|
elif left == 0:
|
|
# no pounds
|
|
return "%s%s" % (
|
|
minus_str,
|
|
cents_str,
|
|
# self.pluralize(right, cr2)
|
|
)
|
|
|
|
return "%s%s%s %s" % (
|
|
minus_str,
|
|
money_str,
|
|
# self.pluralize(left, cr1),
|
|
separator,
|
|
cents_str,
|
|
# self.pluralize(right, cr2)
|
|
)
|
|
|
|
def _money_verbose(self, number, currency):
|
|
# used in super().to_currency(), we need to add gender
|
|
# here for feminine currencies
|
|
# if currency in CURRENCIES_FEM: # always true in this context
|
|
if number > 100:
|
|
m = self.to_cardinal(number, gender="fem", raw=True)
|
|
# if currency in self.CURRENCY_FORMS:
|
|
c = self.CURRENCY_FORMS[currency][0][1]
|
|
m.append(("o", "SM"))
|
|
m.append((c, None))
|
|
# else:
|
|
# c = currency
|
|
# m.append((c, None))
|
|
return makestring(m)
|
|
else:
|
|
# if number > 1:
|
|
m = self.to_cardinal(number, gender="fem", raw=True)
|
|
# elif number == 0:
|
|
# m = self.to_cardinal(number, gender="fem", raw=True)
|
|
# else:
|
|
# m = [(OBJ, None)]
|
|
# if currency in self.CURRENCY_FORMS:
|
|
c = self.CURRENCY_FORMS[currency][0][0]
|
|
# else:
|
|
# c = currency
|
|
# print("eeeeeeeee", m)
|
|
# m.append((c, None))
|
|
# print("fffffffff", m)
|
|
return makestring(m, counted=c)
|
|
# else:
|
|
# return self.to_cardinal(number, raw=True)
|
|
|
|
def _cents_verbose(self, number, currency):
|
|
if number == 0:
|
|
return ""
|
|
# elif number > 100:
|
|
# m = self.to_cardinal(number, raw=True)
|
|
# # if currency in self.CURRENCY_FORMS:
|
|
# c = self.CURRENCY_FORMS[currency][0][1]
|
|
# m.append(("o", "SM"))
|
|
# m.append((c, None))
|
|
# # else:
|
|
# # c = currency
|
|
# # m.append((c, None))
|
|
# return makestring(m)
|
|
else:
|
|
if number > 1:
|
|
m = self.to_cardinal(number, raw=True)
|
|
else:
|
|
m = [(OBJ, None)]
|
|
# if currency in self.CURRENCY_FORMS:
|
|
c = self.CURRENCY_FORMS[currency][1][0]
|
|
# else:
|
|
# c = currency
|
|
return makestring(m, counted=c)
|
|
|
|
|
|
def makestring(result, counted=None):
|
|
# concatenate numberwords with correct mutation
|
|
out = []
|
|
lastmut = None
|
|
for w, mut in result:
|
|
if w == OBJ:
|
|
if not counted:
|
|
continue
|
|
else:
|
|
w = counted
|
|
counted = None # only first position
|
|
if lastmut:
|
|
out.append(mutate(w, lastmut))
|
|
else:
|
|
out.append(w)
|
|
lastmut = mut
|
|
return " ".join(out)
|
|
|
|
|
|
def mutate(word, mutation):
|
|
# print("uuu", word, mutation)
|
|
if mutation == "SM":
|
|
return softmutation(word)
|
|
elif mutation == "AM":
|
|
return aspiratedmutation(word)
|
|
# return word # does not occur
|
|
|
|
|
|
def softmutation(word):
|
|
# print("SM<<<<%s>" % word)
|
|
if word[0] == "p" and word[1] != "h":
|
|
return "b" + word[1:]
|
|
elif word[0] == "t" and word[1] != "h":
|
|
return "d" + word[1:]
|
|
elif word[0] == "c" and word[1] != "h":
|
|
return "g" + word[1:]
|
|
elif word[0] == "b" or word[0] == "m":
|
|
return "f" + word[1:]
|
|
elif word[0] == "d" and word[1] != "d":
|
|
return "d" + word
|
|
elif word.startswith("ll"):
|
|
return word[1:]
|
|
elif word.startswith("rh"):
|
|
return "r" + word[2:]
|
|
elif word == "ugain":
|
|
return "hugain"
|
|
else:
|
|
return word
|
|
|
|
|
|
def aspiratedmutation(word):
|
|
if word[0] == "p" and word[1] != "h":
|
|
return "ph" + word[1:]
|
|
elif word[0] == "t" and word[1] != "h":
|
|
return "th" + word[1:]
|
|
elif word[0] == "c" and word[1] != "h":
|
|
return "ch" + word[1:]
|
|
else:
|
|
return word
|