Вопрос

Как мы все знаем, числа можно записывать как цифрами, так и называть их именами.Хотя можно найти множество примеров преобразования 123 в сто двадцать три, мне не удалось найти хороших примеров того, как преобразовать это число наоборот.

Некоторые предостережения:

  1. кардинальный/номинальный или порядковый:«один» и «первый»
  2. распространенные орфографические ошибки:«сорок» / «сорок»
  3. сотни/тысячи:2100 -> «двадцать одна сотня», а также «две тысячи сто»
  4. разделители:«одиннадцатьсот пятьдесят два», но также «одиннадцатьсот пятьдесят два» или «одиннадцатьсот пятьдесят два» и еще много чего.
  5. разговорные выражения:"тридцать с чем-то"
  6. дроби:«одна треть», «две пятых»
  7. общие имена:«дюжина», «половина»

И, вероятно, есть еще возможные предостережения, которые еще не перечислены.Предположим, что алгоритм должен быть очень надежным и даже понимать орфографические ошибки.

Какие области/статьи/исследования/алгоритмы мне следует прочитать, чтобы научиться все это писать?Где информация?

ПС:Мой окончательный парсер должен понимать три разных языка: английский, русский и иврит.И, возможно, на более позднем этапе будут добавлены другие языки.В иврите также есть мужские и женские числа, например, «один мужчина» и «одна женщина» имеют разные «один» — «ехад» и «ахат».В русском языке тоже есть свои сложности.

Google отлично справляется с этой задачей.Например:

http://www.google.com/search?q=two+thousand+and+one+hundred+plus+five+dozen+and+four+fifths+in+decimal

(возможен и обратный вариант http://www.google.com/search?q=999999999999+in+английский)

Это было полезно?

Решение

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

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

"one" -> 1, "two" -> 2, ... "twenty" -> 20,
"dozen" -> 12, "score" -> 20, ...
"hundred" -> 100, "thousand" -> 1000, "million" -> 1000000

...и так далее

Алгоритм просто:

total = 0
prior = null
for each word w
    v <- value(w) or next if no value defined
    prior <- case
        when prior is null:       v
        when prior > v:     prior+v
        else                prior*v
        else
    if w in {thousand,million,billion,trillion...}
        total <- total + prior
        prior <- null
total = total + prior unless prior is null

Например, это происходит следующим образом:

total    prior      v     unconsumed string
    0      _              four score and seven 
                    4     score and seven 
    0      4              
                   20     and seven 
    0     80      
                    _     seven 
    0     80      
                    7 
    0     87      
   87

total    prior      v     unconsumed string
    0        _            two million four hundred twelve thousand eight hundred seven
                    2     million four hundred twelve thousand eight hundred seven
    0        2
                  1000000 four hundred twelve thousand eight hundred seven
2000000      _
                    4     hundred twelve thousand eight hundred seven
2000000      4
                    100   twelve thousand eight hundred seven
2000000    400
                    12    thousand eight hundred seven
2000000    412
                    1000  eight hundred seven
2000000  412000
                    1000  eight hundred seven
2412000     _
                      8   hundred seven
2412000     8
                     100  seven
2412000   800
                     7
2412000   807
2412807

И так далее.Я не говорю, что он идеален, но для быстрой и грязной работы он вполне подходит.


Обращение к вашему конкретному списку при редактировании:

  1. кардинальный/номинальный или порядковый:«один» и «первый» — просто занеси их в словарь
  2. английский/британский:"сорок"/"сорок" -- то же самое
  3. сотни/тысячи:2100 -> «двадцать одна сотня», а также «две тысячи сто» -- работает как есть
  4. разделители:«одиннадцатьсот пятьдесят два», но также «одиннадцатьсот пятьдесят два» или «одиннадцатьсот пятьдесят два» и еще много чего. для начала просто определите «следующее слово» как самый длинный префикс, соответствующий определенному слову, или до следующего не-слова, если ни один из них не соответствует
  5. разговорные выражения:"тридцать с чем-то" -- работает
  6. фрагменты:«одна треть», «две пятых» — эээ, еще нет...
  7. общие имена:«дюжина», «половина» — работает;вы даже можете делать такие вещи, как «полдюжины»

Число 6 — единственное, на которое у меня нет готового ответа, и это из-за двусмысленности между порядковыми числами и дробями (по крайней мере, в английском языке), а также того факта, что моя последняя чашка кофе была много несколько часов назад.

Другие советы

Это непростая задача, и я не знаю ни одной библиотеки, которая могла бы это сделать.Возможно, когда-нибудь я сяду и попробую написать что-нибудь подобное.Однако я бы сделал это на Прологе, Java или Haskell.Насколько я вижу, есть несколько проблем:

  • Токенизация:иногда пишут числа одиннадцатьсот пятьдесят два, но я видел одиннадцатьсот пятьдесят два или одиннадцатьсот пятьдесят два и еще много чего.Необходимо было бы провести опрос о том, какие формы на самом деле используются.Это может быть особенно сложно для иврита.
  • Орфографические ошибки:это не так уж и сложно.У вас ограниченное количество слов, и немного магии расстояния Левенштейна должно помочь.
  • Альтернативные формы, как вы уже упомянули, существуют.Сюда входят порядковые/количественные числительные, а также сорок/сорок и...
  • ...общие имена или часто используемые фразы и NE (именованные объекты).Хотели бы вы извлечь 30 из Тридцатилетней войны или 2 из Второй мировой войны?
  • Римские цифры тоже?
  • Коллоквиализмы, такие как «тридцать с чем-то» и «три евро и шрапнель», с которыми я не знаю, как обращаться.

Если вам это интересно, я мог бы попробовать на этих выходных.Моя идея, вероятно, заключается в использовании UIMA и токенизации с его помощью, а затем в дальнейшей токенизации/устранении неоднозначности и, наконец, переводе.Могут быть и другие проблемы, посмотрим, смогу ли я придумать что-нибудь более интересное.

Извините, это пока не настоящий ответ, а просто продолжение вашего вопроса.Я дам вам знать, если что-нибудь найду/напишу.

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

У меня есть код, который я написал некоторое время назад: текст2num.Это делает то, что вам нужно, за исключением того, что он не обрабатывает порядковые числа.На самом деле я ни для чего не использовал этот код, поэтому он практически не проверялся!

Используйте Python шаблон-ru библиотека:

>>> from pattern.en import number
>>> number('two thousand fifty and a half') => 2050.5

Следует иметь в виду, что Европа и Америка считают по-разному.

Европейский стандарт:

One Thousand
One Million
One Thousand Millions (British also use Milliard)
One Billion
One Thousand Billions
One Trillion
One Thousand Trillions

Здесь это небольшая ссылка на него.


Простой способ увидеть разницу заключается в следующем:

(American counting Trillion) == (European counting Billion)

Порядковые числа неприменимы, поскольку их нельзя значимым образом соединить с другими числами в языке (... по крайней мере, в английском языке).

напримерсто первый, одиннадцать второй и т. д.

Однако есть еще одно англо-американское предостережение относительно слова «и».

то есть

Сто один (английский) сто один (американский)

Кроме того, использование буквы «а» для обозначения единицы в английском языке

тысяча = одна тысяча

...Кстати, калькулятор Google отлично справляется с этой задачей.

в сто три тысячи раз быстрее скорости света

И даже...

две тысячи сто плюс дюжина

...что за черт?!? счет плюс дюжина римскими цифрами

Вот чрезвычайно надежное решение в Clojure.

AFAIK это уникальный подход к реализации.

;----------------------------------------------------------------------
; numbers.clj
; written by: Mike Mattie codermattie@gmail.com
;----------------------------------------------------------------------
(ns operator.numbers
  (:use compojure.core)

  (:require
    [clojure.string     :as string] ))

(def number-word-table {
  "zero"          0
  "one"           1
  "two"           2
  "three"         3
  "four"          4
  "five"          5
  "six"           6
  "seven"         7
  "eight"         8
  "nine"          9
  "ten"           10
  "eleven"        11
  "twelve"        12
  "thirteen"      13
  "fourteen"      14
  "fifteen"       15
  "sixteen"       16
  "seventeen"     17
  "eighteen"      18
  "nineteen"      19
  "twenty"        20
  "thirty"        30
  "fourty"        40
  "fifty"         50
  "sixty"         60
  "seventy"       70
  "eighty"        80
  "ninety"        90
})

(def multiplier-word-table {
  "hundred"       100
  "thousand"      1000
})

(defn sum-words-to-number [ words ]
  (apply + (map (fn [ word ] (number-word-table word)) words)) )

; are you down with the sickness ?
(defn words-to-number [ words ]
  (let
    [ n           (count words)

      multipliers (filter (fn [x] (not (false? x))) (map-indexed
                                                      (fn [ i word ]
                                                        (if (contains? multiplier-word-table word)
                                                          (vector i (multiplier-word-table word))
                                                          false))
                                                      words) )

      x           (ref 0) ]

    (loop [ indices (reverse (conj (reverse multipliers) (vector n 1)))
            left    0
            combine + ]
      (let
        [ right (first indices) ]

        (dosync (alter x combine (* (if (> (- (first right) left) 0)
                                      (sum-words-to-number (subvec words left (first right)))
                                      1)
                                    (second right)) ))

        (when (> (count (rest indices)) 0)
          (recur (rest indices) (inc (first right))
            (if (= (inc (first right)) (first (second indices)))
              *
              +))) ) )
    @x ))

Вот некоторые примеры

(operator.numbers/words-to-number ["six" "thousand" "five" "hundred" "twenty" "two"])
(operator.numbers/words-to-number ["fifty" "seven" "hundred"])
(operator.numbers/words-to-number ["hundred"])

Моя реализация LPC некоторых ваших требований (только американский английский):

internal mapping inordinal = ([]);
internal mapping number = ([]);

#define Numbers ([\
    "zero"        : 0, \
    "one"         : 1, \
    "two"         : 2, \
    "three"       : 3, \
    "four"        : 4, \
    "five"        : 5, \
    "six"         : 6, \
    "seven"       : 7, \
    "eight"       : 8, \
    "nine"        : 9, \
    "ten"         : 10, \
    "eleven"      : 11, \
    "twelve"      : 12, \
    "thirteen"    : 13, \
    "fourteen"    : 14, \
    "fifteen"     : 15, \
    "sixteen"     : 16, \
    "seventeen"   : 17, \
    "eighteen"    : 18, \
    "nineteen"    : 19, \
    "twenty"      : 20, \
    "thirty"      : 30, \
    "forty"       : 40, \
    "fifty"       : 50, \
    "sixty"       : 60, \
    "seventy"     : 70, \
    "eighty"      : 80, \
    "ninety"      : 90, \
    "hundred"     : 100, \
    "thousand"    : 1000, \
    "million"     : 1000000, \
    "billion"     : 1000000000, \
])

#define Ordinals ([\
    "zeroth"        : 0, \
    "first"         : 1, \
    "second"        : 2, \
    "third"         : 3, \
    "fourth"        : 4, \
    "fifth"         : 5, \
    "sixth"         : 6, \
    "seventh"       : 7, \
    "eighth"        : 8, \
    "ninth"         : 9, \
    "tenth"         : 10, \
    "eleventh"      : 11, \
    "twelfth"       : 12, \
    "thirteenth"    : 13, \
    "fourteenth"    : 14, \
    "fifteenth"     : 15, \
    "sixteenth"     : 16, \
    "seventeenth"   : 17, \
    "eighteenth"    : 18, \
    "nineteenth"    : 19, \
    "twentieth"     : 20, \
    "thirtieth"     : 30, \
    "fortieth"      : 40, \
    "fiftieth"      : 50, \
    "sixtieth"      : 60, \
    "seventieth"    : 70, \
    "eightieth"     : 80, \
    "ninetieth"     : 90, \
    "hundredth"     : 100, \
    "thousandth"    : 1000, \
    "millionth"     : 1000000, \
    "billionth"     : 1000000000, \
])

varargs int denumerical(string num, status ordinal) {
    if(ordinal) {
        if(member(inordinal, num))
            return inordinal[num];
    } else {
        if(member(number, num))
            return number[num];
    }
    int sign = 1;
    int total = 0;
    int sub = 0;
    int value;
    string array parts = regexplode(num, " |-");
    if(sizeof(parts) >= 2 && parts[0] == "" && parts[1] == "-")
        sign = -1;
    for(int ix = 0, int iix = sizeof(parts); ix < iix; ix++) {
        string part = parts[ix];
        switch(part) {
        case "negative" :
        case "minus"    :
            sign = -1;
            continue;
        case ""         :
            continue;
        }
        if(ordinal && ix == iix - 1) {
            if(part[0] >= '0' && part[0] <= '9' && ends_with(part, "th"))
                value = to_int(part[..<3]);
            else if(member(Ordinals, part))
                value = Ordinals[part];
            else
                continue;
        } else {
            if(part[0] >= '0' && part[0] <= '9')
                value = to_int(part);
            else if(member(Numbers, part))
                value = Numbers[part];
            else
                continue;
        }
        if(value < 0) {
            sign = -1;
            value = - value;
        }
        if(value < 10) {
            if(sub >= 1000) {
                total += sub;
                sub = value;
            } else {
                sub += value;
            }
        } else if(value < 100) {
            if(sub < 10) {
                sub = 100 * sub + value;
            } else if(sub >= 1000) {
                total += sub;
                sub = value;
            } else {
                sub *= value;
            }
        } else if(value < sub) {
            total += sub;
            sub = value;
        } else if(sub == 0) {
            sub = value;
        } else {
            sub *= value;
        }
    }
    total += sub;
    return sign * total;
}

Что ж, я опоздал с ответом на этот вопрос, но я работал над небольшим тестовым сценарием, который, кажется, сработал для меня очень хорошо.Я использовал (простое, но уродливое и большое) регулярное выражение, чтобы найти все слова.Выражение выглядит следующим образом:

(?<Value>(?:zero)|(?:one|first)|(?:two|second)|(?:three|third)|(?:four|fourth)|
(?:five|fifth)|(?:six|sixth)|(?:seven|seventh)|(?:eight|eighth)|(?:nine|ninth)|
(?:ten|tenth)|(?:eleven|eleventh)|(?:twelve|twelfth)|(?:thirteen|thirteenth)|
(?:fourteen|fourteenth)|(?:fifteen|fifteenth)|(?:sixteen|sixteenth)|
(?:seventeen|seventeenth)|(?:eighteen|eighteenth)|(?:nineteen|nineteenth)|
(?:twenty|twentieth)|(?:thirty|thirtieth)|(?:forty|fortieth)|(?:fifty|fiftieth)|
(?:sixty|sixtieth)|(?:seventy|seventieth)|(?:eighty|eightieth)|(?:ninety|ninetieth)|
(?<Magnitude>(?:hundred|hundredth)|(?:thousand|thousandth)|(?:million|millionth)|
(?:billion|billionth)))

Здесь показано с разрывами строк для целей форматирования.

В любом случае, мой метод заключался в том, чтобы выполнить этот RegEx с помощью такой библиотеки, как PCRE, а затем прочитать названные совпадения.И это работало на всех различных примерах, перечисленных в этом вопросе, за исключением типов «Одна половина», поскольку я их не добавлял, но, как вы можете видеть, это было бы нетрудно сделать.Это решает множество проблем.Например, в нем рассматриваются следующие элементы исходного вопроса и других ответов:

  1. кардинальный/номинальный или порядковый:«один» и «первый»
  2. распространенные орфографические ошибки:«сорок»/«сорок» (Обратите внимание, что это не решается ЯВНО, это то, что вы хотели бы сделать, прежде чем передать строку этому синтаксическому анализатору.Этот парсер рассматривает этот пример как «ЧЕТЫРЕ»...)
  3. сотни/тысячи:2100 -> «двадцать одна сотня», а также «две тысячи сто»
  4. разделители:«одиннадцатьсот пятьдесят два», но также «одиннадцатьсот пятьдесят два» или «одиннадцатьсот пятьдесят два» и еще много чего.
  5. разговорные выражения:«тридцать с чем-то» (Это тоже не ПОЛНОСТЬЮ, поскольку что ТАКОЕ «что-то»?Что ж, этот код находит это число просто как «30»).**

Теперь, вместо того, чтобы хранить это чудовище регулярного выражения в исходном коде, я рассматривал возможность создания этого RegEx во время выполнения, используя что-то вроде следующего:

char *ones[] = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve",
  "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"};
char *tens[] = {"", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"};
char *ordinalones[] = { "", "first", "second", "third", "fourth", "fifth", "", "", "", "", "", "", "twelfth" };
char *ordinaltens[] = { "", "", "twentieth", "thirtieth", "fortieth", "fiftieth", "sixtieth", "seventieth", "eightieth", "ninetieth" };
and so on...

Самое простое здесь: мы сохраняем только те слова, которые имеют значение.В случае с SIXTH вы заметите, что для него нет записи, потому что это обычный номер с прикрепленным TH...Но такие, как TWELVE, требуют другого внимания.

Хорошо, теперь у нас есть код для создания нашего (уродливого) RegEx, теперь мы просто выполняем его на наших числовых строках.

Я бы порекомендовал отфильтровать или съесть слово «И».Это не обязательно и приводит только к другим проблемам.

Итак, вам нужно настроить функцию, которая передает именованные совпадения для «Величины» в функцию, которая просматривает все возможные значения величины и умножает ваш текущий результат на это значение величины.Затем вы создаете функцию, которая просматривает совпадения с именем «Значение» и возвращает целое число (или что-то еще, что вы используете) на основе обнаруженного там значения.

Все совпадения ЗНАЧЕНИЯ ДОБАВЛЯЮТСЯ к вашему результату, а совпадения по величине умножают результат на значение магнита.Итак, двести пятьдесят тысяч становятся "2", затем "2 * 100", затем "200 + 50", затем "250 * 1000", в итоге получается 250000...

Просто ради интереса я написал версию vbScript, и она отлично работала со всеми предоставленными примерами.Теперь он не поддерживает именованные совпадения, поэтому мне пришлось немного потрудиться, чтобы получить правильный результат, но я его получил.Суть в том, что если это совпадение «ЗНАЧЕНИЕ», добавьте его в свой аккумулятор.Если величина совпадает, умножьте аккумулятор на 100, 1000, 1000000, 1000000000 и т. д.Это даст вам довольно потрясающие результаты, и все, что вам нужно сделать, чтобы настроить такие вещи, как «половина», — это добавить их в свой RegEx, поместить для них маркер кода и обработать их.

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

Если бы я мог..На каком окончательном языке это будет написано?C++ или что-то вроде скриптового языка?Источник Грега Хьюгилла во многом поможет понять, как все это происходит.

Дайте мне знать, если я могу быть чем-то еще полезен.Извините, я знаю только английский/американский язык, поэтому не могу помочь вам с другими языками.

Я конвертировал порядковые номера из книг раннего Нового времени (например,«2nd edition», «Editio Quarta») к целым числам и требовалась поддержка порядковых номеров 1–100 в английском языке и порядковых номеров 1–10 в нескольких романских языках.Вот что я придумал на Python:

def get_data_mapping():
  data_mapping = {
    "1st": 1,
    "2nd": 2,
    "3rd": 3,

    "tenth": 10,
    "eleventh": 11,
    "twelfth": 12,
    "thirteenth": 13,
    "fourteenth": 14,
    "fifteenth": 15,
    "sixteenth": 16,
    "seventeenth": 17,
    "eighteenth": 18,
    "nineteenth": 19,
    "twentieth": 20,

    "new": 2,
    "newly": 2,
    "nova": 2,
    "nouvelle": 2,
    "altera": 2,
    "andere": 2,

    # latin
    "primus": 1,
    "secunda": 2,
    "tertia": 3,
    "quarta": 4,
    "quinta": 5,
    "sexta": 6,
    "septima": 7,
    "octava": 8,
    "nona": 9,
    "decima": 10,

    # italian
    "primo": 1,
    "secondo": 2,
    "terzo": 3,
    "quarto": 4,
    "quinto": 5,
    "sesto": 6,
    "settimo": 7,
    "ottavo": 8,
    "nono": 9,
    "decimo": 10,

    # french
    "premier": 1,
    "deuxième": 2,
    "troisième": 3,
    "quatrième": 4,
    "cinquième": 5,
    "sixième": 6,
    "septième": 7,
    "huitième": 8,
    "neuvième": 9,
    "dixième": 10,

    # spanish
    "primero": 1,
    "segundo": 2,
    "tercero": 3,
    "cuarto": 4,
    "quinto": 5,
    "sexto": 6,
    "septimo": 7,
    "octavo": 8,
    "noveno": 9,
    "decimo": 10
  }

  # create 4th, 5th, ... 20th
  for i in xrange(16):
    data_mapping[str(4+i) + "th"] = 4+i

  # create 21st, 22nd, ... 99th
  for i in xrange(79):
    last_char = str(i)[-1]

    if last_char == "0":
      data_mapping[str(20+i) + "th"] = 20+i

    elif last_char == "1":
      data_mapping[str(20+i) + "st"] = 20+i

    elif last_char == "2":
      data_mapping[str(20+i) + "nd"] = 20+i

    elif last_char == "3":
      data_mapping[str(20+i) + "rd"] = 20+i

    else:
      data_mapping[str(20+i) + "th"] = 20+i

  ordinals = [
    "first", "second", "third", 
    "fourth", "fifth", "sixth", 
    "seventh", "eighth", "ninth"
  ]

  # create first, second ... ninth
  for c, i in enumerate(ordinals):
    data_mapping[i] = c+1

  # create twenty-first, twenty-second ... ninty-ninth
  for ci, i in enumerate([
    "twenty", "thirty", "forty", 
    "fifty", "sixty", "seventy", 
    "eighty", "ninety"
  ]):
    for cj, j in enumerate(ordinals):
      data_mapping[i + "-" + j] = 20 + (ci*10) + (cj+1)
    data_mapping[i.replace("y", "ieth")] = 20 + (ci*10)

  return data_mapping

Пытаться

  1. Откройте HTTP-запрос для "http://www.google.com/search?q=" + число + "+в+десятичном формате".

  2. Проанализируйте результат для вашего номера.

  3. Кэшируйте пары число/результат, чтобы анализировать запросы с течением времени.

Одним из мест, с которого стоит начать поиск, является GNU get_date библиотека, который может анализировать почти любой Английская текстовая дата в метку времени.Хотя это и не совсем то, что вы ищете, их решение аналогичной проблемы может дать много полезных подсказок.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top