Распознаем числа в тексте

в 11:21, , рубрики: java, kotlin, Алгоритмы, ненормальное программирование, Программирование, текст в числа, числа из прописи, числа из текста

Кому может быть полезна эта статья?

Извращенцам делающим NLP на 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.

Критика, вопросы, пожелания, принимаются в issues или в комментариях под статьей.

Автор:
Reformat

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js