diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 6bece3b..969d72c 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -4,7 +4,7 @@ on: pull_request: push: paths: - - 'cunumbers/**' + - 'omninumeric/**' - 'tests/**' jobs: diff --git a/.github/workflows/test-python-3.5.yml b/.github/workflows/test-python-3.5.yml index cfeae83..269cf54 100644 --- a/.github/workflows/test-python-3.5.yml +++ b/.github/workflows/test-python-3.5.yml @@ -4,12 +4,12 @@ on: push: branches: [ dev ] paths: - - 'cunumbers/**' + - 'omninumeric/**' - 'tests/**' pull_request: branches: [ dev ] paths: - - 'cunumbers/**' + - 'omninumeric/**' - 'tests/**' jobs: diff --git a/.github/workflows/test-python-3.7.yml b/.github/workflows/test-python-3.7.yml index 957ff55..a9f4e7b 100644 --- a/.github/workflows/test-python-3.7.yml +++ b/.github/workflows/test-python-3.7.yml @@ -4,12 +4,12 @@ on: push: branches: [ dev ] paths: - - 'cunumbers/**' + - 'omninumeric/**' - 'tests/**' pull_request: branches: [ dev ] paths: - - 'cunumbers/**' + - 'omninumeric/**' - 'tests/**' jobs: diff --git a/cunumbers/__init__.py b/cunumbers/__init__.py deleted file mode 100644 index 4aaf871..0000000 --- a/cunumbers/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -__all__ = [ - "to_cu", - "to_arab", - "CU_PLAIN", - "CU_NOTITLO", - "CU_ENDDOT", - "CU_DELIMDOT", - "CU_WRAPDOT", - "CU_ALLDOT", -] diff --git a/cunumbers/cunumbers.py b/cunumbers/cunumbers.py deleted file mode 100644 index dc4bbba..0000000 --- a/cunumbers/cunumbers.py +++ /dev/null @@ -1,302 +0,0 @@ -# -*- coding: UTF-8 -*- -# For licensing information see LICENSE file included in the project's root directory. -# To learn about Cyrillic numeral system (CU), see INTRODUCTION.md -"Module for number conversion between Arabic and Cyrillic numeral systems." - -import re - -CU_DELIM = 0x1 # Write in delim style -CU_PLAIN = 0x10 # Read/write in plain style -CU_NOTITLO = 0x100 # DO NOT append titlo -CU_ENDDOT = 0x1000 # Append dot -CU_PREDOT = 0x10000 # Prepend dot -CU_DELIMDOT = 0x100000 | CU_DELIM # Delimeter dots (delim mode only) -CU_WRAPDOT = CU_ENDDOT | CU_PREDOT # Wrap in dots -CU_ALLDOT = CU_ENDDOT | CU_PREDOT | CU_DELIMDOT # Wrapper and delimeter dots - -cu_digits = "авгдєѕзиѳ" # CU digit numerals -cu_tens = "іклмнѯѻпч" # CU tens numerals -cu_hundreds = "рстуфхѱѿц" # CU hundreds numerals -cu_thousand = "҂" # "Thousand" mark -cu_titlo = "҃" # "Titlo" decorator -cu_dot = "." # Dot decorator - -cu_null = "\uE000" # Placeholder character to represent zero in CU numbers -cu_dict = "{0}{1}{0}{2}{0}{3}".format( # CU numerals dictionary - cu_null, cu_digits, cu_tens, cu_hundreds -) - -cu_group_regex = ( # Regex for a basic CU number x < 1000 - "[{0}]?(?:[{2}]?{3}|[{1}]?[{2}]?)".format( - cu_hundreds, cu_tens[1:], cu_digits, cu_tens[0] - ) -) -cu_delim_regex = "({0}*{1})".format( # Regex for a digit group in "delim" style - cu_thousand, cu_group_regex -) -cu_plain_regex = ( # Regex for a single digit in "plain" style - "({0}+[{1}]{2}|(?:{3})$)".format( - cu_thousand, - cu_dict.replace(cu_null, ""), - "{1}", - cu_group_regex, - ) -) - - -class CUNumber: - def __init__(self, input, flags=0): - self.cu = "" - self.arabic = input - self.flags = flags - self.prepare() - - def get(self): - return self.cu - - def prepare(self): - if self.arabic <= 0: - raise ValueError("Non-zero integer required") - - def hasFlag(self, flag): - """Check a flag.""" - - return False if self.flags & flag == 0 else True - - def stripDelimDots(self): - "Strip delimeter dots unless CU_DELIMDOT is set." - - if not self.hasFlag(CU_DELIMDOT): - self.cu = re.sub( - "(\{0}(?!{1}$)|(?", self.cu) - return self - - def prependDot(self): - "Prepend dot if CU_PREDOT is set." - - if self.hasFlag(CU_PREDOT): - self.cu = re.sub("^[^\.][\S]*", ".\g<0>", self.cu) - return self - else: - return self.stripAheadDot() - - def appendDot(self): - "Append dot if CU_ENDDOT is set." - - if self.hasFlag(CU_ENDDOT): - self.cu = re.sub("[\S]*[^\.]$", "\g<0>.", self.cu) - return self - - def appendTitlo(self): - """Append "titlo" unless CU_NOTITLO is set.""" - - if not self.hasFlag(CU_NOTITLO): - result = re.subn( - "([\S]+)(?{0}\g<2>".format(cu_titlo), - self.cu, - ) - self.cu = result[0] if result[1] > 0 else self.cu + cu_titlo - return self - - def swapDigits(input): - """Swap digits in 11-19 unless "і" is "thousand"-marked.""" - - result = re.sub( - "(?\g<1>", - input, - ) - return result - - def processDigit(input, registry=0, multiplier=0): - "Convert the Arabic digit to a Cyrillic numeral." - - return ( - cu_thousand * multiplier + cu_dict[10 * registry + input] if input else "" - ) - - def _processNumberPlain(input, registry=0, result=""): - "Process the Arabic number per digit." - # @registry is current registry - - if input: - result = ( - CUNumber.processDigit(input % 10, registry % 3, registry // 3) + result - ) - return CUNumber._processNumberPlain(input // 10, registry + 1, result) - else: - return CUNumber.swapDigits(result) - - def processGroup(input, group=0): - "Process the group of 3 Arabic digits." - - input = input % 1000 - return ( - (cu_dot + cu_thousand * group + CUNumber._processNumberPlain(input)) - if input - else "" - ) - - def _processNumberDelim(input, group=0, result=""): - "Process the Arabic number per groups of 3 digits." - # @index is current group of digits number (i.e. amount of multiplications of thousand) - - if input: - result = CUNumber.processGroup(input, group) + result - return CUNumber._processNumberDelim(input // 1000, group + 1, result) - else: - return result - - def processNumberPlain(self): - - self.cu = CUNumber._processNumberPlain(self.arabic) - return self - - def processNumberDelim(self): - - self.cu = CUNumber._processNumberDelim(self.arabic) - return self - - def convert(self): - "Convert the Arabic number to Cyrillic." - - if self.arabic < 1001 or self.hasFlag(CU_PLAIN): - self.processNumberPlain() - else: - self.processNumberDelim() - return self.stripDelimDots().prependDot().appendDot().appendTitlo() - - -class ArabicNumber: - def __init__(self, input): - self.cu = input - self.arabic = 0 - self.prepare() - - def get(self): - return self.arabic - - def prepare(self): - "Prepare the Cyrillic number for conversion." - - if self.cu: - self.cu = re.sub( - "[{0}\.]".format(cu_titlo), "", self.cu - ) # Strip ҃"҃ " and dots - self.cu = str.strip(self.cu) - self.cu = str.lower(self.cu) - else: - raise ValueError("Non-empty string required") - - def processDigit(input, registry=0): - "Convert the Cyrillic numeral to an arabic digit." - - index = cu_dict.index(input) # Find current digit in dictionary - number = index % 10 # Digit - registry = index // 10 # Digit registry - return number * pow(10, registry) # Resulting number - - def processGroup(input, group=0): - "Process the group of Cyrillic numerals." - - subtotal = multiplier = 0 - for k in input: - if k == cu_thousand: - multiplier += 1 - continue - subtotal += ArabicNumber.processDigit(k) - - # Multiply result by 1000 - times "҂" marks or current group - return subtotal * pow(1000, max(multiplier, group)) - - def prepareGroups(input, regex): - "Prepare Cyrillic numeral groups for conversion." - - groups = re.split(regex, input) - - while groups.count(""): # Purge empty strs from collection - groups.remove("") - groups.reverse() - return groups - - def _processNumberPlain(input): - "Process the Cyrillic number per digit." - - groups = ArabicNumber.prepareGroups(input, cu_plain_regex) - - result = 0 - for k in groups: - result += ArabicNumber.processGroup(k) - return result - - def _processNumberDelim(input): - "Process the Cyrillic number per groups of 1-3 digits." - - groups = ArabicNumber.prepareGroups(input, cu_delim_regex) - - result = 0 - for i, k in enumerate(groups): - result += ArabicNumber.processGroup(k, i) - return result - - def processNumberPlain(self): - self.arabic = ArabicNumber._processNumberPlain(self.cu) - return self - - def processNumberDelim(self): - self.arabic = ArabicNumber._processNumberDelim(self.cu) - return self - - def convert(self): - "Choose the Cyrillic number to Arabic." - - if re.fullmatch("{0}+".format(cu_plain_regex), self.cu): - return self.processNumberPlain() - elif re.fullmatch("{0}+".format(cu_delim_regex), self.cu): - return self.processNumberDelim() - else: - raise ValueError( - "String does not match any pattern for Cyrillic numeral system number" - ) - - -def isinstance(input, condition, msg): - t = type(input) - if t == condition: - return True - else: - raise TypeError(msg.format(t)) - - -def to_cu(input, flags=0): - """ - Convert a number into Cyrillic numeral system. - - Requires a non-zero integer. - """ - - if isinstance(input, int, "Non-zero integer required, got {0}"): - return CUNumber(input, flags).convert().get() - - -def to_arab(input, flags=0): - """ - Convert a number into Arabic numeral system . - - Requires a non-empty string. - """ - - if isinstance(input, str, "Non-empty string required, got {0}"): - return ArabicNumber(input).convert().get() diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7b0270a..9d8d7a3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,35 +2,6 @@ 🌏 English [Русский](./CHANGELOG.ru.md) -## 1.3.2 - -- Added: Exceptions on unexpected input - -## 1.3.1 - -- Optimization - -## 1.3.0 - -- Added: Flags to specify dotting style: end with dot, wrap in dots, use delimeter-dots, or any combination -- Added: Obligatory dotting in ambiguous "delimeter" style cases - -## 1.2.0 - -- Added: Now supports reading/writing in "plain" style -- Added: Flags to specify Arabic to CU conversion mode (default to "delimeter" style) -- Added: Flag to omit "titlo" - -## 1.1.1 - -- Cleanup - -## 1.1.0 - -- Added: Now supports Python >= 3.5 -- Fixed: Wrong "titlo" positioning -- Fixed: Wrong "800" digit - ## 1.0.0 diff --git a/docs/CHANGELOG.ru.md b/docs/CHANGELOG.ru.md index 6566786..2703993 100644 --- a/docs/CHANGELOG.ru.md +++ b/docs/CHANGELOG.ru.md @@ -2,35 +2,6 @@ 🌏 [English](./CHANGELOG.md) Русский -## 1.3.2 - -- Добавлено: Исключения при некорректных входных данных - -## 1.3.1 - -- Оптимизация - -## 1.3.0 - -- Добавлено: Флаги декорирования точками: конечная, с обеих сторон, точки-разделители разрядов, или любая их комбинация -- Добавлено: Обязательное проставление точек-разделителей в неоднозначных случаях - -## 1.2.0 - -- Добавлено: Поддержка чтения/записи в "сплошном" записи -- Добавлено: Флаг выбора варианта записи при преобразовании в ЦСЯ ("по группам" по умолчанию) -- Добавлено: Флаг для опускания вывода "титла" - -## 1.1.1 - -- Полировка - -## 1.1.0 - -- Добавлено: Поддержка Python >= 3.5 -- Исправлено: Неверная позиция "титла" -- Исправлено: Неверная цифра "800" - ## 1.0.0 diff --git a/docs/README.md b/docs/README.md index e246f5e..460ee1a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,46 +1,54 @@ -# cu-numbers +# Omninumeric -![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cu-numbers) ![PyPI - Wheel](https://img.shields.io/pypi/wheel/cu-numbers) [![Codecov](https://img.shields.io/codecov/c/github/endrain/cu-numbers)](https://app.codecov.io/gh/endrain/cu-numbers) +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/omninumeric) ![PyPI - Wheel](https://img.shields.io/pypi/wheel/omninumeric) [![Codecov](https://img.shields.io/codecov/c/github/endrain/omninumeric)](https://app.codecov.io/gh/endrain/omninumeric) -[![PyPI - License](https://img.shields.io/pypi/l/cu-numbers)](./LICENSE) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![PyPI - License](https://img.shields.io/pypi/l/omninumeric)](./LICENSE) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 🌏 English [Русский](./README.ru.md) -A program for numbers conversion between Arabic and Cyrillic (*further CU*) numeral systems. +Omninumeric provides support for number reading and writing in alphabetic numeral systems. + +## Supported numeral systems + +- [x] Cyrillic +- [ ] Roman - WIP +- [ ] Byzantian Greek - WIP +- [ ] Modern Greek - planned +- [ ] Hebrew - planned ## Background -See [Introduction](./INTRODUCTION.md) to learn about CU numeral system. +See [Introduction](./INTRODUCTION.md) to learn about Cyrillic numeral system. ## Installation - pip install cu-numbers + pip install omninumeric ## Usage - import cunumbers.cunumbers as cu + import omninumeric.cyrillic as CU - # Convert an Arabic number to CU + # Convert a number into Cyrillic # Requires non-zero int, returns str - a = cu.to_cu(1) + a = CU.ArabicNumber(1).convert() - # Convert a CU number to Arabic + # Convert a Cyrillic number to Arabic # Requires non-empty str, returns int - b = cu.to_arab("а҃") + b = CU.CyrillicNumber("а҃").convert() -"Delimiter" and "plain" style numbers are supported in both directions. "Delimeter" style is default for CU-wise conversion. +"Delimiter" and "plain" style numbers are supported both for reading and writing, "plain" style is used by default for writing. -Several falgs can be used with `to_cu()` method: +When writing into Cyrillic, several falgs can be used: - # CU_PLAIN flag sets conversion to "plain" style + # CU_DELIM flag sets conversion to "delimeter" style - c = cu.to_cu(111111, CU_PLAIN) + c = cu.to_alphabetic(111111, CU_DELIM) - # CU_NOTITLO flag omits "titlo" output + # CU_NOTITLO flag omits "titlo" decorator - d = cu.to_cu(11000, CU_PLAIN | CU_NOTITLO) + d = cu.to_alphabetic(11000, CU_DELIM | CU_NOTITLO) # Following flags control dot styling: # diff --git a/docs/README.ru.md b/docs/README.ru.md index 38ab3e4..de443d0 100644 --- a/docs/README.ru.md +++ b/docs/README.ru.md @@ -1,12 +1,20 @@ -# cu-numbers +# Omninumeric -![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cu-numbers) ![PyPI - Wheel](https://img.shields.io/pypi/wheel/cu-numbers) [![Codecov](https://img.shields.io/codecov/c/github/endrain/cu-numbers)](https://app.codecov.io/gh/endrain/cu-numbers) +![PyPI - Python Version](https://img.shields.io/pypi/pyversions/omninumeric) ![PyPI - Wheel](https://img.shields.io/pypi/wheel/omninumeric) [![Codecov](https://img.shields.io/codecov/c/github/endrain/omninumeric)](https://app.codecov.io/gh/endrain/omninumeric) -[![PyPI - License](https://img.shields.io/pypi/l/cu-numbers)](./LICENSE.ru) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![PyPI - License](https://img.shields.io/pypi/l/omninumeric)](./LICENSE.ru) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 🌏 [English](./README.md) Русский -Программа для преобразования чисел между арабской и церковнославянской системами записи. +Программа для чтения и записи чисел в алфавитных системах записи. + +## Поддерживаемые системы записи + +- [x] Церковно-славянская +- [ ] Римская - в работе +- [ ] Византийская греческая - в работе +- [ ] Современная греческая - в проекте +- [ ] Еврейская - в проекте ## Историческая справка @@ -14,33 +22,33 @@ ## Установка - pip install cu-numbers + pip install omninumeric ## Использование - import cunumbers.cunumbers as cu + import omninumeric.cyrillic as cu # Преобразовать арабское число в церковнославянское # Принимает ненулевой int, возвращает str - a = cu.to_cu(1) + a = CU.ArabicNumber(1).convert() # Преобразовать церковнославянское число в арабское # Принимает непустой str, возвращает int - b = cu.to_arab("а҃") + b = CU.CyrillicNumber("а҃").convert() -В обоих направлениях поддерживаются варианты записи "сплошной" и "по группам". Запись "по группам" используется по умолчанию для преобразования в ЦСЯ. +В обоих направлениях поддерживаются варианты записи "сплошной" и "по группам", "сплошная" запись используется по умолчанию. -Метод `to_cu()` принимает несколько флагов: +При записи в ЦСЯ возможно использование слеедующих флагов: - # CU_PLAIN устанавливает "сплошной" вариант записи для преобразования в ЦСЯ + # CU_DELIM устанавливает "сплошной" вариант записи в ЦСЯ - c = cu.to_cu(111111, CU_PLAIN) + c = cu.to_alphabetic(111111, CU_DELIM) # CU_NOTITLO опускает вывод знака "титло" - d = cu.to_cu(11000, CU_PLAIN | CU_NOTITLO) + d = cu.to_alphabetic(11000, CU_DELIM | CU_NOTITLO) # Следующие флаги управляют декорированием точками: # diff --git a/omninumeric/__init__.py b/omninumeric/__init__.py new file mode 100644 index 0000000..00b16dd --- /dev/null +++ b/omninumeric/__init__.py @@ -0,0 +1,7 @@ +from .omninumeric import ( + Dictionary, + IntNumberConverter, + StrNumberConverter, +) + +__all__ = ["Dictionary", "IntNumberConverter", "StrNumberConverter"] diff --git a/omninumeric/cyrillic/__init__.py b/omninumeric/cyrillic/__init__.py new file mode 100644 index 0000000..183c143 --- /dev/null +++ b/omninumeric/cyrillic/__init__.py @@ -0,0 +1,27 @@ +from .cyrillic import ( + CU_PLAIN, + CU_DELIM, + CU_NOTITLO, + CU_ENDDOT, + CU_DELIMDOT, + CU_WRAPDOT, + CU_ALLDOT, + ArabicNumber, + CyrillicNumber, + to_cu, + to_arab, +) + +__all__ = [ + "ArabicNumber", + "CyrillicNumber", + "to_cu", + "to_arab", + "CU_PLAIN", + "CU_DELIM", + "CU_NOTITLO", + "CU_ENDDOT", + "CU_DELIMDOT", + "CU_WRAPDOT", + "CU_ALLDOT", +] diff --git a/omninumeric/cyrillic/cyrillic.py b/omninumeric/cyrillic/cyrillic.py new file mode 100644 index 0000000..7f83149 --- /dev/null +++ b/omninumeric/cyrillic/cyrillic.py @@ -0,0 +1,211 @@ +# -*- coding: UTF-8 -*- +# For licensing information see LICENSE file included with the project. +# To learn about Cyrillic numeral system (CU), see INTRODUCTION.md +"This module provides tools for reading and writing numbers in Cyrillic numeral system." + +import re +from omninumeric.greek import * + + +CU_PLAIN = PLAIN # Write in plain style flag +CU_DELIM = DELIM # Read/write in delim style flag +CU_NOTITLO = 0b10 # DO NOT append titlo flag +CU_ENDDOT = 0b100 # Append dot flag +CU_PREDOT = 0b1000 # Prepend dot flag +CU_DOT = 0b10000 # Delimeter dots flag +CU_DELIMDOT = CU_DOT | CU_DELIM # Delimeter dots flag (forces delim style) +CU_WRAPDOT = CU_ENDDOT | CU_PREDOT # Wrap in dots flag +CU_ALLDOT = CU_ENDDOT | CU_PREDOT | CU_DELIMDOT # Wrapper and delimeter dots flag + + +class _CyrillicDictionary(DictionaryGreek): + "Cyrillic numerals ditcionary." + + а = 1 + в = 2 + г = 3 + д = 4 + є = 5 + ѕ = 6 + з = 7 + и = 8 + ѳ = 9 + і = 10 + к = 20 + л = 30 + м = 40 + н = 50 + ѯ = 60 + ѻ = 70 + п = 80 + ч = 90 + р = 100 + с = 200 + т = 300 + у = 400 + ф = 500 + х = 600 + ѱ = 700 + ѿ = 800 + ц = 900 + THOUSAND = "҂" # "Thousand" mark + TITLO = "҃" # "Titlo" decorator + DOT = "." # Dot decorator + + +class ArabicNumber(IntNumberConverterGreek): + "Number converter into Cyrillic numeral system." + + _dict = _CyrillicDictionary + + def _ambiguityCheck(self, cond, flag): + if cond: + try: + if (self._groups[0] // 10 % 10 == 1) and ( + self._groups[1] // 10 % 10 == 0 + ): + self._flags = self._flags | flag + finally: + return self + else: + return self + + def _swapDigits(self): + "Swap digits for values 11-19 (unless separated)." + + for i, k in enumerate(self._groups): + + self._groups[i] = re.sub( + "({0})([{1}])".format(self._dict.get(10), self._dict.digits()), + "\g<2>\g<1>", + self._groups[i], + ) + + return self + + def _appendTitlo(self, cond): + 'Apply "titlo" decorator unless appropriate flag is set.' + + if not cond: + result = re.subn( + "([\S]+)(?{0}\g<2>".format(self._dict.get("TITLO")), + self._target, + ) + self._target = ( + result[0] + if result[1] > 0 + else "{0}{1}".format(self._target, self._dict.get("TITLO")) + ) + + return self + + def _delimDots(self, cond): + "Insert dots between numeral groups if appropriate flag is set." + + if cond: + for i, k in enumerate(self._groups[1:]): + self._groups[i + 1] = "{0}{1}".format(k, self._dict.get("DOT")) + + return self + + def _wrapDot(self, cond_a, cond_b): + "Prepend and/or append a dot if appropriate flags are set." + + self._target = "{0}{1}{2}".format( + self._dict.get("DOT") if cond_a else "", + self._target, + self._dict.get("DOT") if cond_b else "", + ) + + return self + + def convert(self): + """ + Convert into Cyrillic numeral system. Uses plain style by default. + + Requires a non-zero integer. + """ + + return ( + self._validate() + ._breakIntoGroups() + ._ambiguityCheck(self._hasFlag(CU_DELIM), CU_DOT) + ._translateGroups() + ._appendThousandMarks(self._hasFlag(CU_DELIM)) + ._purgeEmptyGroups() + ._swapDigits() + ._delimDots(self._hasFlag(CU_DOT)) + ._build() + ._appendTitlo(self._hasFlag(CU_NOTITLO)) + ._wrapDot(self._hasFlag(CU_PREDOT), self._hasFlag(CU_ENDDOT)) + ._get() + ) + + +class CyrillicNumber(StrNumberConverterGreek): + "Number converter from Cyrillic numeral system." + + _dict = _CyrillicDictionary + + _regex = "({0}*[{1}]?(?:(?:{0}*[{3}])?{4}|(?:{0}*[{2}])?(?:{0}*[{3}])?))".format( + _dict.get("THOUSAND"), + _dict.hundreds(), + _dict.tens(2), + _dict.digits(), + _dict.get(10), + ) # Regular expression for typical Cyrillic numeral system number + + def _prepare(self): + "Prepare source number for conversion." + + super()._prepare() + self._source = re.sub( + "[{0}\{1}]".format(self._dict.get("TITLO"), self._dict.get("DOT")), + "", + self._source, + ) # Strip ҃decorators + + return self + + def _validate(self): + "Validate that source number is a non-empty string and matches the pattern for Cyrillic numeral system numbers." + + super()._validate() + if not re.fullmatch("{0}+".format(self._regex), self._source): + raise ValueError( + "String does not match any pattern for Cyrillic numeral system numbers" + ) + + return self + + def convert(self): + """ + Convert from Cyrillic numeral system. + + Requires a non-empty string. + """ + + return ( + self._prepare() + ._validate() + ._breakIntoGroups(self._regex) + ._purgeEmptyGroups() + ._translateGroups() + ._build() + ._get() + ) + + +def to_cu(integer, flags=0): + "Deprecated. Use ArabicNumber().convert() instead." + + return ArabicNumber(integer, flags).convert() + + +def to_arab(alphabetic, flags=0): + "Deprecated. Use CyrillicNumber().convert() instead." + + return CyrillicNumber(alphabetic).convert() diff --git a/omninumeric/greek/__init__.py b/omninumeric/greek/__init__.py new file mode 100644 index 0000000..57c6f1b --- /dev/null +++ b/omninumeric/greek/__init__.py @@ -0,0 +1,15 @@ +from .greek import ( + PLAIN, + DELIM, + DictionaryGreek, + IntNumberConverterGreek, + StrNumberConverterGreek, +) + +__all__ = [ + "PLAIN", + "DELIM", + "DictionaryGreek", + "IntNumberConverterGreek", + "StrNumberConverterGreek", +] diff --git a/omninumeric/greek/greek.py b/omninumeric/greek/greek.py new file mode 100644 index 0000000..d60f09f --- /dev/null +++ b/omninumeric/greek/greek.py @@ -0,0 +1,167 @@ +# -*- coding: UTF-8 -*- +# For licensing information see LICENSE file included with the project. +"This module provides basic tools for reading and writing numbers in Greek-type alphabetic numeral systems." + +import re + +from omninumeric import ( + Dictionary, + IntNumberConverter, + StrNumberConverter, +) + + +PLAIN = 0 # Write in plain style flag +DELIM = 0b1 # Read/write in delim style flag + + +class DictionaryGreek(Dictionary): + """ + ABC for Greek-type alphabetic numeral systems ditcionaries. + + Derive from this class to define numeral dictionaries for Greek type alphabetic numeral systems. + """ + + @classmethod + def _getmany(cls, start=1, end=10, step=1): + """ + Look a range of numerals up in dictionary. + + @start - starting numeral value (i.e. 5 for range of 5, 6, 7...) + @end - ending numeral value (i.e. 5 for range of ...3, 4, 5) + step - numeral value increment (i.e. 1 for range of 1, 2, 3...; 10 for range of 10, 20, 30...) + """ + + r = "" + for i in range(start * step, (end + 1) * step, step): + r += cls(i).name + return r + + @classmethod + def digits(cls, start=1, end=9): + """ + Get a range of numerals in digits registry. + + @start - starting numeral value (i.e. 5 for range of 5, 6, 7...) + @end - ending numeral value (i.e. 5 for range of ...3, 4, 5) + """ + return cls._getmany(start, end, 1) + + @classmethod + def tens(cls, start=1, end=9): + """ + Get a range of numerals in tens registry. + + @start - starting numeral value (i.e. 5 for range of 50, 60, 70...) + @end - ending numeral value (i.e. 5 for range of ...30, 40, 50) + """ + return cls._getmany(start, end, 10) + + @classmethod + def hundreds(cls, start=1, end=9): + """ + Get a range of numerals in hundreds registry. + + @start - starting numeral value (i.e. 5 for range of 500, 600, 700...) + @end - ending numeral value (i.e. 5 for range of ...300, 400, 500) + """ + return cls._getmany(start, end, 100) + + +class IntNumberConverterGreek(IntNumberConverter): + """ + ABC for number conversion into Greek-type alphabetic numeral systems. + + Derive from this class to define converters into Greek-type alphabetic numeral systems. + """ + + def _appendThousandMarks(self, cond): + "Append thousand marks according to chosen style (plain or delimeter)." + + for i, k in enumerate(self._groups): + + if k: + if cond: + result = "{0}{1}".format(self._dict.get("THOUSAND") * i, k) + + else: + result = "" + + for l in k: + result = "{0}{1}{2}".format( + result, self._dict.get("THOUSAND") * i, l + ) + + self._groups[i] = result + + return self + + def _translateGroups(self): + "Translate groups of numerals one by one." + + for i, k in enumerate(self._groups): + + result = "" + index = 0 + + while k > 0: + result = self._getNumeral(k % 10 * pow(10, index)) + result + index = index + 1 + k = k // 10 + + self._groups[i] = result + + return self + + def _breakIntoGroups(self): + "Break source number into groups of 3 numerals." + + while self._source > 0: + self._groups.append(self._source % 1000) + self._source = self._source // 1000 + + return self + + +class StrNumberConverterGreek(StrNumberConverter): + """ + ABC for number conversion from Greek-type alphabetic numeral systems. + + Derive from this class to define converters from Greek-type alphabetic numeral systems. + """ + + @classmethod + def _calculateMultiplier(cls, index, group): + 'Calculate multiplier for a numerals group, according to group index or "thousand" marks present in the group.' + + multiplier = ( + re.match("({0}*)".format(cls._dict.get("THOUSAND")), group) + .groups()[0] + .count(cls._dict.get("THOUSAND")) + ) # Count trailing thousand marks in the group + multiplier = pow(1000, multiplier if multiplier else index) + # Use thousand marks if present, otherwise use group index + return multiplier + + def _translateGroups(self): + "Translate groups of numerals one by one." + + for i, k in enumerate(self._groups): + total = 0 # Current group total value + multiplier = self._calculateMultiplier(i, k) + k = re.sub(self._dict.get("THOUSAND"), "", k) # Strip thousand marks + + for l in k: + total += self._getNumeral(l) + + self._groups[i] = total * multiplier + + return self + + def _breakIntoGroups(self, regex=""): + "Break source number in groups of 1-3 numerals." + + self._groups = re.split(regex, self._source) # Break into groups + self._groups.reverse() # Reverse groups (to ascending order) + + return self diff --git a/omninumeric/greek/old.py b/omninumeric/greek/old.py new file mode 100644 index 0000000..b14c7ed --- /dev/null +++ b/omninumeric/greek/old.py @@ -0,0 +1,94 @@ +# -*- coding: UTF-8 -*- +# For licensing information see LICENSE file included with the project. +""" +This module provides tools for reading and writing numbers in Old Greek numeral system. + +WIP +""" + +__all__ = ["ArabicNumber", "OldGreekNumber"] + + +from omninumeric import ( + StrNumberConverter, + IntNumberConverter, +) +from omninumeric.greek import * + + +class _OldGreekDictionary(DictionaryGreek): + "Old Greek numerals dictionary" + + α = 1 + β = 2 + γ = 3 + δ = 4 + є = 5 + ϛ = 6 + ζ = 7 + η = 8 + θ = 9 + ι = 10 + κ = 20 + λ = 30 + μ = 40 + ν = 50 + ξ = 60 + ο = 70 + π = 80 + ϟ = 90 # ϙ + ρ = 100 + σ = 200 + τ = 300 + υ = 400 + φ = 500 + χ = 600 + ψ = 700 + ω = 800 + ϡ = 900 + THOUSAND = "͵" # "Thousand" mark + KERAIA = "ʹ" # "Keraia" decorator + OVERLINE = "̅" # Overline decorator + DOT = "." # Dot decorator + + +class ArabicNumber(IntNumberConverterGreek): + "Number converter into Old Greek numeral system." + + _dict = _OldGreekDictionary + + def convert(self): + """ + Convert into Old Greek numeral system. Uses plain style by default. + + Requires a non-zero integer. + """ + return ( + self._breakIntoGroups() + ._translateGroups() + ._appendThousandMarks(self._hasFlag(DELIM)) + ._purgeEmptyGroups() + ._build() + ._get() + ) + + +class OldGreekNumber(StrNumberConverterGreek): + "Number converter from Old Greek numeral system." + + _dict = _OldGreekDictionary + + def convert(self): + """ + Convert from Old Greek numeral system. + + Requires a non-empty string. + """ + + return ( + self._breakIntoGroups() + ._purgeEmptyGroups() + ._translateGroups() + ._build() + ._get() + ) diff --git a/omninumeric/omninumeric.py b/omninumeric/omninumeric.py new file mode 100644 index 0000000..48ce585 --- /dev/null +++ b/omninumeric/omninumeric.py @@ -0,0 +1,162 @@ +# -*- coding: UTF-8 -*- +# For licensing information see LICENSE file included with the project. +"This module provides basic tools for reading and writing numbers in alphabetic numeral systems." + +import re +from enum import Enum, unique + + +def isinstanceEx(value, cond, msg=""): + """ + Test if value is of a speciic type, raise error with specified message if not. + + @value - value to test + @cond - type to test against + @msg - error message to print. Supports format() to print @value type + """ + + t = type(value) + if not t == cond: + raise TypeError(msg.format(t)) + + +@unique +class Dictionary(Enum): + """ + ABC for alphabetic numeral systems dictionaries. + + Derive from this class to define numeral dictionaries for alphabetic numeral systems. + """ + + @classmethod + def get(cls, numeral): + """ + Look a numeral up in dictionary. + + @numeral - str or int to look up. Returns int if str is found and vice versa, returns None if nothing found + """ + + try: + return cls[numeral].value + except: + try: + return cls(numeral).name + except: + return None + + +class NumberConverter: + """ + ABC for number conversion. + + Derive from this class to define converters into and from alphabetic numeral systems. + """ + + _dict = NotImplemented + + def __init__(self, source, target, flags=0): + self._source = source + self._target = target + self._flags = flags + self._groups = [] + + def _hasFlag(self, flag): + "Check if a flag is set." + + return self._flags & flag + # return False if self._flags & flag == 0 else True + + def _get(self): + "Return the converted number." + + return self._target + + def _build(self): + "Build the converted number from groups of numerals." + + for k in self._groups: + self._target = k + self._target + return self + + @classmethod + def _getNumeral(cls, numeral, fallback): + """ + Look a numeral up in dictionary. + + @numeral - numeral to look up + @fallback - value to return if @numeral is not found + """ + + return cls._dict.get(numeral) or fallback + + def _purgeEmptyGroups(self): + "Remove empty groups from numeral groups collection." + + print(self._groups) + while self._groups.count(""): + self._groups.remove("") # Purge empty groups + print(self._groups) + return self + + def convert(self): + raise NotImplementedError + + +class IntNumberConverter(NumberConverter): + """ + ABC for number conversion into alphabetic numeral systems. + + Derive from this class to define converters into alphabetic numeral systems. + """ + + def __init__(self, value, flags=0): + super().__init__(value, "", flags) + + def _validate(self): + "Validate that source number is a natural number." + + isinstanceEx(self._source, int, "Integer required, got {0}") + + if self._source <= 0: + raise ValueError("Natural number required") + + return self + + @classmethod + def _getNumeral(cls, numeral): + "Get alphabetic digit for given value." + + return super()._getNumeral(numeral, "") + + +class StrNumberConverter(NumberConverter): + """ + ABC for number conversion from alphabetic numeral systems. + + Derive from this class to define converters from ABS. + """ + + def __init__(self, alphabetic, flags=0): + super().__init__(alphabetic, 0, flags) + + def _validate(self): + "Validate that source number is a non-empty string." + + isinstanceEx(self._source, str, "String required, got {0}") + + if not self._source: + raise ValueError("Non-empty string required") + + return self + + def _prepare(self): + "Prepare source number for further operations." + + self._source = str.lower(str.strip(self._source)) + return self + + @classmethod + def _getNumeral(cls, numeral): + "Get value for given alphabetic digit." + + return super()._getNumeral(numeral, 0) diff --git a/setup.cfg b/setup.cfg index e58a4b8..f9ab3b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,29 +1,5 @@ [metadata] -name = cu-numbers -version = 1.3.2 -author = Andrei Shur -author_email = amshoor@gmail.com -description = Cyrillic numeral system numbers conversion -long_description = file: README.md -long_description_content_type = text/markdown -url = https://github.com/endrain/cu-numbers -keywords = church slavonic, conversion -license = MIT -classifiers = - Development Status :: 5 - Production/Stable - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.4 - Programming Language :: Python :: 3.5 - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - License :: OSI Approved :: MIT License - Operating System :: OS Independent - Intended Audience :: Religion - Intended Audience :: Science/Research +long_description = file: docs\README.md [options] -include_package_data = True -packages = cunumbers -python_requires = >=3.4 \ No newline at end of file +include_package_data = True \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ec58b24 --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +from setuptools import setup, find_packages + +setup( + name="omninumeric", + version="1.0.0", + author="Andrei Shur", + author_email="amshoor@gmail.com", + description="Read and write numbers in alphabetic numeral systems", + long_description_content_type="text/markdown", + url="https://github.com/endrain/omninumeric", + keywords=[ + "church slavonic", + "conversion", + ], + license="MIT", + classifiers=[ + "Development Status :: 4 - Beta", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Localization", + "Intended Audience :: Developers", + "Intended Audience :: Religion", + "Intended Audience :: Science/Research", + ], + packages=find_packages(), + python_requires=">=3.4", +) diff --git a/tests/test_cunumbers.py b/tests/test_cyrillic.py similarity index 77% rename from tests/test_cunumbers.py rename to tests/test_cyrillic.py index 1acd952..a59a863 100644 --- a/tests/test_cunumbers.py +++ b/tests/test_cyrillic.py @@ -1,6 +1,6 @@ # -*- coding: UTF-8 -*- import unittest -from cunumbers.cunumbers import * +from omninumeric.cyrillic import * class ToCUPlainTestCase(unittest.TestCase): @@ -20,35 +20,41 @@ def testToCUHundreds(self): def testToCUThousand(self): self.assertEqual(to_cu(1000), "҂а҃") - self.assertEqual(to_cu(1006, CU_PLAIN), "҂а҃ѕ") - self.assertEqual(to_cu(1010, CU_PLAIN), "҂а҃і") + self.assertEqual(to_cu(1006), "҂а҃ѕ") + self.assertEqual(to_cu(1010), "҂а҃і") self.assertEqual(to_cu(1015), "҂ає҃і") self.assertEqual(to_cu(1444), "҂аум҃д") - self.assertEqual(to_cu(11000, CU_PLAIN), "҂і҂а҃") + self.assertEqual(to_cu(11000), "҂і҂а҃") def testToCUBig(self): - self.assertEqual(to_cu(10001010001, CU_PLAIN), "҂҂҂і҂҂а҂і҃а") - self.assertEqual(to_cu(50000000000, CU_PLAIN), "҂҂҂н҃") - self.assertEqual(to_cu(60000070000, CU_PLAIN), "҂҂҂ѯ҂ѻ҃") - self.assertEqual(to_cu(111111111, CU_PLAIN), "҂҂р҂҂і҂҂а҂р҂і҂ара҃і") + self.assertEqual(to_cu(10001010001), "҂҂҂і҂҂а҂і҃а") + self.assertEqual(to_cu(50000000000), "҂҂҂н҃") + self.assertEqual(to_cu(60000070000), "҂҂҂ѯ҂ѻ҃") + self.assertEqual(to_cu(111111111), "҂҂р҂҂і҂҂а҂р҂і҂ара҃і") class ToCUDelimTestCase(unittest.TestCase): - def testToCUDelimThousand(self): - self.assertEqual(to_cu(1010), "҂а.і҃") - self.assertEqual(to_cu(11000), "҂а҃і") + def testToCUDelimAmbiguity(self): + self.assertEqual(to_cu(1010, CU_DELIM), "҂а.і҃") + self.assertEqual(to_cu(11000, CU_DELIM), "҂а҃і") + self.assertEqual(to_cu(10010, CU_DELIM), "҂і҃і") + self.assertEqual(to_cu(110010, CU_DELIM), "҂рі҃і") + self.assertEqual(to_cu(100010, CU_DELIM), "҂р.і҃") + self.assertEqual(to_cu(110000, CU_DELIM), "҂р҃і") + self.assertEqual(to_cu(100011, CU_DELIM), "҂р.а҃і") + self.assertEqual(to_cu(111000, CU_DELIM), "҂ра҃і") def testToCUDelimBig(self): - self.assertEqual(to_cu(10001010001), "҂҂҂і҂҂а҂і҃а") - self.assertEqual(to_cu(50000000000), "҂҂҂н҃") - self.assertEqual(to_cu(60000070000), "҂҂҂ѯ҂ѻ҃") - self.assertEqual(to_cu(111111111), "҂҂раі҂раіра҃і") + self.assertEqual(to_cu(10001010001, CU_DELIM), "҂҂҂і҂҂а҂і҃а") + self.assertEqual(to_cu(50000000000, CU_DELIM), "҂҂҂н҃") + self.assertEqual(to_cu(60000070000, CU_DELIM), "҂҂҂ѯ҂ѻ҃") + self.assertEqual(to_cu(111111111, CU_DELIM), "҂҂раі҂раіра҃і") class ToCUFlagsTestCase(unittest.TestCase): def testToCUNotitlo(self): self.assertEqual(to_cu(1, CU_NOTITLO), "а") - self.assertEqual(to_cu(11000, CU_PLAIN + CU_NOTITLO), "҂і҂а") + self.assertEqual(to_cu(11000, CU_NOTITLO), "҂і҂а") def testToCUEnddot(self): self.assertEqual(to_cu(1, CU_ENDDOT), "а҃.")