Преобразование слов в числа в PHP

Я пытаюсь преобразовать числовые значения, записанные в виде слов, в целые числа. Например, «iPhone имеет двести тридцать тысяч семьсот восемьдесят три приложения» станет «iPhone как 230783 приложения».

Прежде чем я начну кодировать, я хотел бы знать, существует ли какая-либо функция / код для этого преобразования.


person user132513    schedule 03.07.2009    source источник


Ответы (6)


Есть много страниц, на которых обсуждается преобразование чисел в слова. Не так много для обратного направления. Лучшее, что я смог найти, это какой-то псевдокод на Ask Yahoo. См. http://answers.yahoo.com/question/index?qid=20090216103754AAONnDz для красивого алгоритма:

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

Вам понадобятся следующие токены:

МОЩНОСТЬ: тысяча, миллион, миллиард
СОТНИ: сто
ДЕСЯТЬ: двадцать, тридцать ... девяносто
ЕДИНИЦА: один, два, три, ... девять,
СПЕЦИАЛЬНЫЕ: десять, одиннадцать, двенадцать, ... девятнадцать

(отбросьте любые символы "и", поскольку они бессмысленны. Разбейте дефисы на две лексемы. То есть шестьдесят пять должны обрабатываться как шестьдесят пять)

После того, как вы разметили свою строку, переходите от ВПРАВО НАЛЕВО.

  1. Возьмите все жетоны ПРАВО, пока не нажмете СИЛУ или всю строку.

  2. Разберите токены после точки остановки на предмет следующих шаблонов:

    СПЕЦИАЛЬНЫЕ
    ДЕСЯТЬ
    БЛОК
    ДЕСЯТЬ БЛОК
    БЛОК СТО
    БЛОК СТО СПЕЦИАЛЬНЫЕ
    БЛОК СТО ДЕСЯТЬ
    АГРЕГАТ СТО БЛОК
    БЛОК СТО ДЕСЯТЬ БЛОК

    (Предполагается, что в этой грамматике недопустимо "семнадцать сотен")

    Это дает вам последние три цифры вашего номера.

  3. Если вы остановились на всей строке, все готово.

  4. Если вы остановились на мощности, начните снова с шага 1, пока не достигнете более высокой мощности или всей струны.

person John Kugelman    schedule 03.07.2009
comment
Спасибо, Джон! Этот алгоритм - именно то, что я искал. Я пытался разобрать его слева направо, но это выглядит лучше. Ценю вашу помощь! - person user132513; 03.07.2009
comment
Я добавил ответ ниже, который реализует отдаленно похожий алгоритм. - person El Yobo; 27.06.2012

Старый вопрос, но для всех, кто сталкивался с этим, мне пришлось написать решение этого сегодня. В нижеследующем используется подход, отдаленно похожий на алгоритм, описанный Джоном Кугельманом, но он не применяется как строгая грамматика; как таковой он допускает некоторые странные порядки, например «сто тысяч один миллион» по-прежнему будет производить то же самое, что и «один миллион сто тысяч» (1 100 000). Недействительные биты (например, числа с ошибками) будут проигнорированы, поэтому вывод с недопустимыми строками будет считаться неопределенным.

Следуя комментарию user132513 к ответу Джоберта, я использовал Pear Number_Words для создания серии тестов. Следующий код набрал 100% на числах от 0 до 5 000 000, затем 100% на случайной выборке из 100 000 чисел от 0 до 10 000 000 (требуется слишком много времени, чтобы перебрать все 10 миллиардов серий).

/**
 * Convert a string such as "one hundred thousand" to 100000.00.
 *
 * @param string $data The numeric string.
 *
 * @return float or false on error
 */
function wordsToNumber($data) {
    // Replace all number words with an equivalent numeric value
    $data = strtr(
        $data,
        array(
            'zero'      => '0',
            'a'         => '1',
            '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',
            'fourty'    => '40', // common misspelling
            'fifty'     => '50',
            'sixty'     => '60',
            'seventy'   => '70',
            'eighty'    => '80',
            'ninety'    => '90',
            'hundred'   => '100',
            'thousand'  => '1000',
            'million'   => '1000000',
            'billion'   => '1000000000',
            'and'       => '',
        )
    );

    // Coerce all tokens to numbers
    $parts = array_map(
        function ($val) {
            return floatval($val);
        },
        preg_split('/[\s-]+/', $data)
    );

    $stack = new SplStack; // Current work stack
    $sum   = 0; // Running total
    $last  = null;

    foreach ($parts as $part) {
        if (!$stack->isEmpty()) {
            // We're part way through a phrase
            if ($stack->top() > $part) {
                // Decreasing step, e.g. from hundreds to ones
                if ($last >= 1000) {
                    // If we drop from more than 1000 then we've finished the phrase
                    $sum += $stack->pop();
                    // This is the first element of a new phrase
                    $stack->push($part);
                } else {
                    // Drop down from less than 1000, just addition
                    // e.g. "seventy one" -> "70 1" -> "70 + 1"
                    $stack->push($stack->pop() + $part);
                }
            } else {
                // Increasing step, e.g ones to hundreds
                $stack->push($stack->pop() * $part);
            }
        } else {
            // This is the first element of a new phrase
            $stack->push($part);
        }

        // Store the last processed part
        $last = $part;
    }

    return $sum + $stack->pop();
}
person El Yobo    schedule 27.06.2012
comment
Я обнаружил, что это очень надежное и емкое решение. Прекрасная работа! Мое единственное изменение заключалось в том, чтобы добавить 'lakh' => '100000' и 'crore' => '10000000', как указано пользователем132513 в ответе Джобертса. - person Khior; 13.02.2013
comment
Один вариант использования, когда это не работает, - это, например, $data= 'five or ten'. Это возвращает 50. Приведенный выше ответ хорошо подходит для OP. Однако нужно учитывать, что строка имеет правильное форматирование. В моем случае я пытался вырезать число из непроверенной строки, не контролируя (или не зная), какой может быть строка. Пользователи иногда помещают в формы довольно странные ответы! - person Sablefoste; 18.12.2014
comment
Недействительные биты (например, числа с ошибками) будут проигнорированы, поэтому вывод с недопустимыми строками будет считаться неопределенным; к сожалению, это предназначено только для преобразования строки, содержащей одно число. Вы можете попытаться разделить свою строку на фрагменты, используя приведенный выше список $data (поскольку это единственные подстроки, которые нас волнуют), а затем запустить его для каждого фрагмента, а затем объединить результаты, используя разделенные слова. - person El Yobo; 19.12.2014
comment
@ElYobo Работает хорошо, за исключением того, что если значение $ data равно десять вместо десять, тогда возвращается 0. вместо 10. Пожалуйста, помогите этому чувствительному к регистру товарищу. - person Raja Gopal; 25.09.2016
comment
Вызов wordsToNumber со строкой в ​​нижнем регистре, например wordsToNumber(strtolower($my_string)). - person El Yobo; 27.09.2016
comment
Я бы рекомендовал сделать это первой строкой: $data = strtolower(trim($data));. Это касается вопроса, высказанного @RajaGopal - person sean.boyer; 15.03.2017

Я не тестировал это слишком тщательно, я более или менее просто работал над этим, пока не увидел то, что ожидал в выводе, но, похоже, он работает и анализирует слева направо.

<?php

$str = 'twelve billion people know iPhone has two hundred and thirty thousand, seven hundred and eighty-three apps as well as over one million units sold';

function strlen_sort($a, $b)
{
    if(strlen($a) > strlen($b))
    {
        return -1;
    }
    else if(strlen($a) < strlen($b))
    {
        return 1;
    }
    return 0;
}

$keys = array(
    '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'
);


preg_match_all('#((?:^|and|,| |-)*(\b' . implode('\b|\b', array_keys($keys)) . '\b))+#i', $str, $tokens);
//print_r($tokens); exit;
$tokens = $tokens[0];
usort($tokens, 'strlen_sort');

foreach($tokens as $token)
{
    $token = trim(strtolower($token));
    preg_match_all('#(?:(?:and|,| |-)*\b' . implode('\b|\b', array_keys($keys)) . '\b)+#', $token, $words);
    $words = $words[0];
    //print_r($words);
    $num = '0'; $total = 0;
    foreach($words as $word)
    {
        $word = trim($word);
        $val = $keys[$word];
        //echo "$val\n";
        if(bccomp($val, 100) == -1)
        {
            $num = bcadd($num, $val);
            continue;
        }
        else if(bccomp($val, 100) == 0)
        {
            $num = bcmul($num, $val);
            continue;
        }
        $num = bcmul($num, $val);
        $total = bcadd($total, $num);
        $num = '0';
    }
    $total = bcadd($total, $num);
    echo "$total:$token\n";
    $str = preg_replace("#\b$token\b#i", number_format($total), $str);
}
echo "\n$str\n";

?>
person joebert    schedule 03.07.2009
comment
Обнаружен один недостаток, в нем отсутствуют распространенные сочетания чисел и слов, например 2 миллиона. - person joebert; 03.07.2009
comment
Это также испортит некоторые формулировки дат. Я родился в тысяча девятьсот восемьдесят первом - person joebert; 03.07.2009
comment
Большое спасибо, Джоберт, за код! Я постараюсь улучшить его. Я установил тестовый набор из 10000 слов со случайными числами (используя Numbers_Words), и в настоящее время точность декодирования слов в числа составляет 75%. Правильно: сорок пять тысяч пятьсот пятьдесят четыре становится 45554 Неправильно: пятьдесят одна тысяча пятьсот восемьдесят шесть становится 586 - person user132513; 09.07.2009
comment
Только что осознал проблему. При доступе к первому ключу происходит что-то забавное, например, «один». Вместо этого поместите «квадриллион» = ›« 1000000000000000 »перед« один », и это сработает со 100% точностью. - person user132513; 10.07.2009
comment
Кроме того, включите 'lakh' = ›'100000' и 'crore' =› '10000000' в ключи $. Это более распространенные термины, чем миллионы в странах Южной Азии. - person user132513; 10.07.2009
comment
В этом есть смысл. У меня есть форматировщик файлов, который работает аналогично. Я, должно быть, очень спешил и забыл поставить на чеке самые большие числа. - person joebert; 14.07.2009

Несколько обновленный ответ El Yobo, теперь можно запускать функцию wordsToNumber для (почти) любой строки, содержащей цифры.

https://github.com/thefish/words-to-number-converter

Converter.php - сам конвертер

test.php - тест с разными строками

UPD 22.10.2020: Ответ стал слишком большим, чтобы поддерживать его. код перенесен на github.

person thefish    schedule 10.04.2018
comment
Это работает намного лучше, чем другие для адресных номеров: one two three main street или one 23 main street. - person Jay A. Little; 07.10.2018
comment
@thefish любит поддержку смешанного ввода, но некоторые строки вроде «тридцать три тысячи пятьдесят девять долларов» не работают. То и вызывает проблемы. - person Enlai; 11.10.2020

Самый простой способ, который я нашел, - использовать numfmt_parse:

$fmt = numfmt_create('en_US', NumberFormatter::SPELLOUT);
echo numfmt_parse($fmt, 'one million two hundred thirty-four thousand five hundred sixty-seven');

(источник; сообщение Дориана на странице https://stackoverflow.com/a/31588055/11827985):

person will    schedule 19.04.2020

Пакет PEAR Numbers_Words, вероятно, станет хорошим началом: http://pear.php.net/package-info.php?package=Numbers_Words

person Jani Hartikainen    schedule 03.07.2009
comment
Спасибо, Яни. Этот пакет выглядит интересно, хотя он выполняет обратную задачу, то есть от чисел к словам. Будет полезно в будущих проектах. - person user132513; 03.07.2009