Как читать значения из чисел, записанных в виде слов?
-
09-06-2019 - |
Вопрос
Как мы все знаем, числа можно записывать как цифрами, так и называть их именами.Хотя можно найти множество примеров преобразования 123 в сто двадцать три, мне не удалось найти хороших примеров того, как преобразовать это число наоборот.
Некоторые предостережения:
- кардинальный/номинальный или порядковый:«один» и «первый»
- распространенные орфографические ошибки:«сорок» / «сорок»
- сотни/тысячи:2100 -> «двадцать одна сотня», а также «две тысячи сто»
- разделители:«одиннадцатьсот пятьдесят два», но также «одиннадцатьсот пятьдесят два» или «одиннадцатьсот пятьдесят два» и еще много чего.
- разговорные выражения:"тридцать с чем-то"
- дроби:«одна треть», «две пятых»
- общие имена:«дюжина», «половина»
И, вероятно, есть еще возможные предостережения, которые еще не перечислены.Предположим, что алгоритм должен быть очень надежным и даже понимать орфографические ошибки.
Какие области/статьи/исследования/алгоритмы мне следует прочитать, чтобы научиться все это писать?Где информация?
ПС:Мой окончательный парсер должен понимать три разных языка: английский, русский и иврит.И, возможно, на более позднем этапе будут добавлены другие языки.В иврите также есть мужские и женские числа, например, «один мужчина» и «одна женщина» имеют разные «один» — «ехад» и «ахат».В русском языке тоже есть свои сложности.
Google отлично справляется с этой задачей.Например:
(возможен и обратный вариант 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
И так далее.Я не говорю, что он идеален, но для быстрой и грязной работы он вполне подходит.
Обращение к вашему конкретному списку при редактировании:
- кардинальный/номинальный или порядковый:«один» и «первый» — просто занеси их в словарь
- английский/британский:"сорок"/"сорок" -- то же самое
- сотни/тысячи:2100 -> «двадцать одна сотня», а также «две тысячи сто» -- работает как есть
- разделители:«одиннадцатьсот пятьдесят два», но также «одиннадцатьсот пятьдесят два» или «одиннадцатьсот пятьдесят два» и еще много чего. для начала просто определите «следующее слово» как самый длинный префикс, соответствующий определенному слову, или до следующего не-слова, если ни один из них не соответствует
- разговорные выражения:"тридцать с чем-то" -- работает
- фрагменты:«одна треть», «две пятых» — эээ, еще нет...
- общие имена:«дюжина», «половина» — работает;вы даже можете делать такие вещи, как «полдюжины»
Число 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, а затем прочитать названные совпадения.И это работало на всех различных примерах, перечисленных в этом вопросе, за исключением типов «Одна половина», поскольку я их не добавлял, но, как вы можете видеть, это было бы нетрудно сделать.Это решает множество проблем.Например, в нем рассматриваются следующие элементы исходного вопроса и других ответов:
- кардинальный/номинальный или порядковый:«один» и «первый»
- распространенные орфографические ошибки:«сорок»/«сорок» (Обратите внимание, что это не решается ЯВНО, это то, что вы хотели бы сделать, прежде чем передать строку этому синтаксическому анализатору.Этот парсер рассматривает этот пример как «ЧЕТЫРЕ»...)
- сотни/тысячи:2100 -> «двадцать одна сотня», а также «две тысячи сто»
- разделители:«одиннадцатьсот пятьдесят два», но также «одиннадцатьсот пятьдесят два» или «одиннадцатьсот пятьдесят два» и еще много чего.
- разговорные выражения:«тридцать с чем-то» (Это тоже не ПОЛНОСТЬЮ, поскольку что ТАКОЕ «что-то»?Что ж, этот код находит это число просто как «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
Пытаться
Откройте HTTP-запрос для "http://www.google.com/search?q=" + число + "+в+десятичном формате".
Проанализируйте результат для вашего номера.
Кэшируйте пары число/результат, чтобы анализировать запросы с течением времени.
Одним из мест, с которого стоит начать поиск, является GNU get_date библиотека, который может анализировать почти любой Английская текстовая дата в метку времени.Хотя это и не совсем то, что вы ищете, их решение аналогичной проблемы может дать много полезных подсказок.