Spaces:
Sleeping
Sleeping
Download rules/currency.py from frznkshn/TextNormalizer: direct link, hf CLI and curl.
- Browser
- Download file 5.5 kB
-
https://huggingface.co/spaces/frznkshn/TextNormalizer/resolve/main/rules/currency.py
- Command line
-
hf download hf://spaces/frznkshn/TextNormalizer/rules/currency.py
-
curl -L -o currency.py https://huggingface.co/spaces/frznkshn/TextNormalizer/resolve/main/rules/currency.py
5.5 kB
| import random | |
| import re | |
| from dataclasses import dataclass | |
| from decimal import Decimal | |
| from num2fawords import words | |
| from num2words import num2words | |
| from persiantools import digits | |
| from rules.base import BaseProcessor | |
| from utils import NO_LETTERS, detect_digit_language | |
| class Currency: | |
| symbol: str | |
| fa_unit: str | |
| fa_subunit: str | |
| en_unit: str | |
| en_units: str | |
| en_subunit: str | |
| en_subunits: str | |
| CURRENCIES = { | |
| "$": Currency("$", "دلار", "سنت", "dollar", "dollars", "cent", "cents"), | |
| "€": Currency("€", "یورو", "سنت", "euro", "euros", "cent", "cents"), | |
| "£": Currency("£", "پوند", "پنس", "pound", "pounds", "penny", "pence"), | |
| } | |
| SYMBOLS = "".join(re.escape(symbol) for symbol in CURRENCIES) | |
| AMOUNT = r"\d+(?:[.,٫٬]\d+)*" | |
| # Matches a single token such as `3$`, `$3`, `۳€`, `£12.50`, `1,000$`, | |
| # optionally wrapped in punctuation, e.g. `(3$)` or `£12.50.`. | |
| CURRENCY_PATTERN = re.compile( | |
| rf""" | |
| ^ | |
| (?P<lead>[^\w{SYMBOLS}]*) | |
| (?: | |
| (?P<prefix_symbol>[{SYMBOLS}])(?P<prefixed>{AMOUNT}) | |
| | | |
| (?P<suffixed>{AMOUNT})(?P<suffix_symbol>[{SYMBOLS}]) | |
| ) | |
| (?P<trail>[^\w{SYMBOLS}]*) | |
| $ | |
| """, | |
| re.VERBOSE, | |
| ) | |
| class CurrencyProcessor(BaseProcessor): | |
| """Converts amounts such as `3$`, `€3` or `£12.50` into spoken text. | |
| Supported symbols: `$` (dollar), `€` (euro), `£` (pound). | |
| Persian: `۳$` -> `سه دلار` | |
| `€12.50` -> `دوازده یورو و پنجاه سنت` | |
| `£12.50` -> `دوازده پوند و پنجاه پنس` | |
| English: `3$` -> `three dollars` | |
| `$12.50` -> `twelve dollars and fifty cents` | |
| `£12.50` -> `twelve pounds and fifty pence` | |
| Amounts with more than two decimal places are read as a plain decimal | |
| number (e.g. `3.141$` -> `three point one four one dollars`). | |
| If the text has no letters at all, the digits pick the language: | |
| `۱۰۰۰$` -> `یک هزار دلار` | |
| `1000$` -> `one thousand dollars` | |
| """ | |
| def __init__(self, epsilon: float = 1.0): | |
| super(CurrencyProcessor, self).__init__(epsilon=epsilon) | |
| def process(self, token: str, language: str = "en", *args, **kwargs) -> str: | |
| if random.random() > self.epsilon: | |
| return token | |
| match = CURRENCY_PATTERN.fullmatch(token) | |
| if match is None: | |
| return token | |
| # A text with no letters at all (e.g. just `۱۰۰۰$`) has no language, so | |
| # the digits decide: Persian digits -> Persian, ASCII digits -> English. | |
| if language == NO_LETTERS: | |
| language = detect_digit_language(token) | |
| amount = digits.fa_to_en(match.group("prefixed") or match.group("suffixed")) | |
| symbol = match.group("prefix_symbol") or match.group("suffix_symbol") | |
| currency = CURRENCIES[symbol] | |
| if language == "fa": | |
| text = self._amount_to_persian(amount, currency) | |
| elif language == "en": | |
| text = self._amount_to_english(amount, currency) | |
| else: | |
| return token | |
| return f"{match.group('lead')}{text}{match.group('trail')}" | |
| def is_currency(token: str) -> bool: | |
| return CURRENCY_PATTERN.fullmatch(token) is not None | |
| def _amount_to_persian(amount: str, currency: Currency) -> str: | |
| clean = CurrencyProcessor._clean_amount(amount) | |
| parts = CurrencyProcessor._split_major_and_minor(clean) | |
| if parts is None: | |
| return f"{words(float(clean))} {currency.fa_unit}" | |
| major, minor = parts | |
| return CurrencyProcessor._compose( | |
| major=major, | |
| minor=minor, | |
| spell=words, | |
| major_unit=currency.fa_unit, | |
| minor_unit=currency.fa_subunit, | |
| joiner=" و ", | |
| ) | |
| def _amount_to_english(amount: str, currency: Currency) -> str: | |
| clean = CurrencyProcessor._clean_amount(amount) | |
| parts = CurrencyProcessor._split_major_and_minor(clean) | |
| if parts is None: | |
| return f"{num2words(Decimal(clean))} {currency.en_units}" | |
| major, minor = parts | |
| return CurrencyProcessor._compose( | |
| major=major, | |
| minor=minor, | |
| spell=num2words, | |
| major_unit=currency.en_unit if major == 1 else currency.en_units, | |
| minor_unit=currency.en_subunit if minor == 1 else currency.en_subunits, | |
| joiner=" and ", | |
| ) | |
| def _compose(major, minor, spell, major_unit, minor_unit, joiner) -> str: | |
| pieces = [] | |
| # Skip "zero dollars" when there are cents (`0.50$` -> "fifty cents"). | |
| if major or not minor: | |
| pieces.append(f"{spell(major)} {major_unit}") | |
| if minor: | |
| pieces.append(f"{spell(minor)} {minor_unit}") | |
| return joiner.join(pieces) | |
| def _split_major_and_minor(clean: str): | |
| """Returns (major, minor), or None if there are more than 2 decimals.""" | |
| if "." not in clean: | |
| return int(clean), 0 | |
| whole, fraction = clean.split(".") | |
| if len(fraction) > 2: | |
| return None | |
| return int(whole), int(fraction.ljust(2, "0")) | |
| def _clean_amount(amount: str) -> str: | |
| # Thousands separators are dropped; ٫ is the Persian decimal mark. | |
| return amount.replace(",", "").replace("٬", "").replace("٫", ".") |