Распознаем числа из прописи на Kotlin

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Кому может быть полезна эта статья?
Извращенцам делающим ML на Java? Или может быть для обучения?
Хотя зачем эти оправдания? Весь код был написан because we can.
Под катом мы рассмотрим как превращать числа вида "Двенадцать тысяч шестьсот пятьдесят девять целых четыре миллионных" в форму вроде 12 659, 000 004.

Русский язык обладает встроенными алиасами для некоторых чисел. Их мы будем с переводить в последовательность обычных чисел. Для этого составим словарь псевдонимов:

0 ноль нуль
1 один
2 два
3 три
4 четыре
5 пять
6 шесть
7 семь
8 восемь
9 девять
11 одиннадцать
12 двенадцать дюжина
13 тринадцать
14 четырнадцать
15 пятнадцать
16 шестнадцать
17 семнадцать
18 восемнадцать
19 девятнадцать
20 двадцать
30 тридцать
40 сорок
50 пятьдесят
60 шестьдесят
70 семьдесят
80 восемьдесят
90 девяносто
200 двести
300 триста
400 четыреста
500 пятьсот
600 шестьсот
700 семьсот
800 восемьсот
900 девятьсот
0.00000000001 стомиллиардный
0.0000000001 десятимиллиардный
0.000000001 миллиардный
0.00000001 стомиллионный
0.0000001 десятимиллионный
0.000001 миллионный
0.00001 стотысячный
0.0001 десятитысячный
0.001 тысячный
0.01 сотый
0.1 десятый
10 десять
100 сто
1000 тысяча
1000000 миллион
1000000000 миллиард
1000000000000 триллион
1000000000000000 квадриллион
1000000000000000000 квинтиллион
1000000000000000000000 секстиллион
1000000000000000000000000 септиллион
1000000000000000000000000000 октиллион

Чтобы прочитать словарь из ресурсов в память, нам потребуется такой код на Kotlin:

{}.javaClass.getResourceAsStream("/dictionary")!!
  .bufferedReader()
  .readLines()
  .flatMap { line ->
    val aliases = line.split(' ')
    val number = aliases.first().toDouble()
    aliases.drop(1).map { Pair(it, number) }
  }.toMap()

Некоторая сложность этого кода обсуловлена теоретической возможностью наличия двух и более псеводонимов для одного числа.

Теперь настало время выхода на сцену токенайзера и морфологического словаря.
Подключив их, мы можем вытащить из любой строки последовательность наших чисел в любых разрешенных русским языком склонениях:

val integerPart = mutableListOf<Double>()
val fractionalPart = mutableListOf<Double>()
var currentPart = integerPart
for (token in words) {
  if (integerPart.isNotEmpty() && token.lowercase() in separators) {
    currentPart = fractionalPart
    continue
  }
  val number =
    lookupForMeanings(token)
      .run {
        firstOrNull { it.partOfSpeech == Numeral || it.partOfSpeech == OrdinalNumber }
          ?: getOrNull(0)
      }
      ?.lemma
      ?.toString()
      ?.let(numbers::get)
  if (number != null) {
    currentPart += number
    continue
  }
  if (currentPart.isNotEmpty()) {
    break
  }
}

Код ужасно мутабельный, но как сделать лучше пока не придумал. После этого нам остается только склеить последовательность обычных чисел в одно. Это самое простое, пока число в последовательности меньше следующего, то умножаем, а когда следующее становится меньше предыдущего, то складываем островки умножений.

private fun List<Double>.join(): Double {
  var tokensSum = 0.0
  var previousToken = first()
  for (currToken in drop(1)) {
    if (currToken > previousToken) {
      previousToken *= currToken
    } else {
      tokensSum += previousToken
      previousToken = currToken
    }
  }
  return tokensSum + previousToken
}

Пришло время тестов нашей чудо-библиотеки!

@Test
fun parseRussianDouble() {
  assertThat("Двенадцать тысяч шестьсот пятьдесят девять целых четыре миллионных".parseRussianDouble())
    .isEqualTo(12659.000004)

  assertThat("Десять тысяч четыреста тридцать четыре".parseRussianDouble())
    .isEqualTo(10434.0)

  assertThat("Двенадцать целых шестьсот пятьдесят девять тысячных".parseRussianDouble())
    .isEqualTo(12.659)

  assertThat("Ноль целых пятьдесят восемь сотых".parseRussianDouble())
    .isEqualTo(0.58)

  assertThat("Сто тридцать пять".parseRussianDouble())
    .isEqualTo(135.0)
}

Если вам интересно, как сделать, чтобы метод .parseToRussianDouble появился для всех строк в вашем Kotlin (или Java) проекте, то вам нужно просто подключить пару строчек в вашей системе сборки:
https://jitpack.io/#demidko/chisla/2021.10.30

В качестве демонстрации еще одной возможности библиотеки приведу кусочек кода:

"Я хотел передать ему сто тридцать пять яблок".parseRussianDouble()
// 135

Исходный код библиотеки доступен на GitHub: https://github.com/demidko/chisla
Критика, вопросы, пожелания, принимаются в issues или в комментариях под статьей.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Полезно?
100% Да, может заиспользую в своем проекте 2
0% Нет, буду пользоваться ICU / другим велосипедом 0
Проголосовали 2 пользователя. Воздержавшихся нет.
Источник: https://habr.com/ru/post/586466/


Интересные статьи

Интересные статьи

Я работаю Java/Kotlin-разработчиком в компании EPAM. В этой статье хочу поделиться опытом настройки плагина ktlint для Kotlin проекта. Данный плагин помогает об...
Когда вы использовали какой-либо API-интерфейс, хотелось ли вам добавить в него новые функции или свойства?Для решения этой задачи вы можете использовать наследование (создать новый класс...
Приветствую вас (лично вас, а не всех кто это читает)! Сегодня мы: Создадим приложение (навык) Алисы с использованием нового (октябрь 2019) сервиса Yandex Cloud Functions. Настроим н...
Здравствуйте. Я уже давно не пишу на php, но то и дело натыкаюсь на интернет-магазины на системе управления сайтами Битрикс. И я вспоминаю о своих исследованиях. Битрикс не любят примерно так,...
Некоторое время назад мне довелось пройти больше десятка собеседований на позицию php-программиста (битрикс). К удивлению, требования в различных организациях отличаются совсем незначительно и...