Как читать значения из чисел, написанных словами? - PullRequest
49 голосов
/ 16 сентября 2008

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

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

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

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

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

PS: Мой последний парсер должен понимать 3 разных языка: английский, русский и иврит. И, возможно, на более позднем этапе будет добавлено больше языков. У иврита также есть мужские / женские номера, например, «один мужчина» и «одна женщина» имеют разные «один» - «эхад» и «ахат». У русских тоже есть свои сложности.

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+english)

Ответы [ 12 ]

46 голосов
/ 17 марта 2009

Я играл с 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 - единственное, на которое у меня нет готового ответа, и это из-за неоднозначности между ординалами и дробями (по крайней мере на английском языке), добавленной к тому факту, что моя последняя чашка кофе была много часов назад.

11 голосов
/ 18 сентября 2008

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

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

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

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

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

11 голосов
/ 16 сентября 2008

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

7 голосов
/ 03 августа 2011

Использование библиотеки Python pattern-en :

>>> from pattern.en import number
>>> number('two thousand fifty and a half') => 2050.5
5 голосов
/ 17 марта 2009

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

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

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)
4 голосов
/ 14 марта 2009

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

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

Тем не менее, есть еще одна англо-американская оговорка со словами 'и'

т.е.

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

Кроме того, использование «а» в английском означает «1011»

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

... С другой стороны, калькулятор Google проделывает потрясающую работу.

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

И даже ...

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

... WTF?!? Оценка плюс дюжина римских цифр

3 голосов
/ 13 ноября 2011

Вот чрезвычайно надежное решение в 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"])
2 голосов
/ 21 марта 2009

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

(?<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...

Легкая часть здесь - мы храним только те слова, которые имеют значение. В случае ШЕСТОГО вы заметите, что для него нет записи, потому что это просто обычный номер с прикрепленным TH ... Но такие, как TWELVE, требуют другого внимания.

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

Одна вещь, которую я бы порекомендовал, это фильтровать или есть слово «И». Это не обязательно, и только приводит к другим проблемам.

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

Все совпадения VALUE ДОБАВЛЯЮТСЯ в ваш результат, а совпадения Magnitutde умножают результат на значение mag. Таким образом, двести пятьдесят тысяч становится «2», затем «2 * 100», затем «200 + 50», затем «250 * 1000», заканчивая 250000 ...

Просто для забавы я написал эту версию на vbScript, и она прекрасно работала со всеми представленными примерами. Теперь он не поддерживает именованные совпадения, поэтому мне пришлось потрудиться, чтобы получить правильный результат, но я его получил. Суть в том, что если это совпадение VALUE, добавьте его в свой аккумулятор. Если это совпадение по величине, умножьте свой аккумулятор на 100, 1000, 1000000, 1000000000 и т. Д. ... Это даст вам довольно удивительные результаты, и все, что вам нужно сделать, чтобы приспособиться к таким вещам, как "половина", это добавить их в свой RegEx, введите для них маркер кода и обработайте их.

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

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

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

2 голосов
/ 17 марта 2009

Моя 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;
}
0 голосов
/ 30 декабря 2016

Я конвертировал порядковые операторы из ранних современных книг (например, «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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...