Вопрос по php, numbers, word – Преобразование слов в числа в PHP

19

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

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

Ваш Ответ

6   ответов
2

Эль ЙобоОтвет, теперь можно запускать функцию wordsToNumber над (почти) любой строкой, содержащей цифры. Смотрите тест ниже.

<?php

class Converter {

    /**
     * Convert numerals to digits
     * @param string $input
     *
     * @return string
     */
    public static function wordsToNumber(string $input)
    {
        static $delims = " \-,.!?:;\\/&\(\)\[\]";
        static $tokens = [
            'zero'        => ['val' => '0', 'power' => 1],
            'a'           => ['val' => '1', 'power' => 1],
            'first'       => ['val' => '1', 'suffix' => 'st', 'power' => 1],
            'one'         => ['val' => '1', 'power' => 1],
            'second'      => ['val' => '2', 'suffix' => 'nd', 'power' => 1],
            'two'         => ['val' => '2', 'power' => 1],
            'third'       => ['val' => '3', 'suffix' => 'rd', 'power' => 1],
            'three'       => ['val' => '3', 'power' => 1],
            'fourth'      => ['val' => '4', 'suffix' => 'th', 'power' => 1],
            'four'        => ['val' => '4', 'power' => 1],
            'fifth'       => ['val' => '5', 'suffix' => 'th', 'power' => 1],
            'five'        => ['val' => '5', 'power' => 1],
            'sixth'       => ['val' => '6', 'suffix' => 'th', 'power' => 1],
            'six'         => ['val' => '6', 'power' => 1],
            'seventh'     => ['val' => '7', 'suffix' => 'th', 'power' => 1],
            'seven'       => ['val' => '7', 'power' => 1],
            'eighth'      => ['val' => '8', 'suffix' => 'th', 'power' => 1],
            'eight'       => ['val' => '8', 'power' => 1],
            'ninth'       => ['val' => '9', 'suffix' => 'th', 'power' => 1],
            'nine'        => ['val' => '9', 'power' => 1],
            'tenth'       => ['val' => '10', 'suffix' => 'th', 'power' => 1],
            'ten'         => ['val' => '10', 'power' => 10],
            'eleventh'    => ['val' => '11', 'suffix' => 'th', 'power' => 10],
            'eleven'      => ['val' => '11', 'power' => 10],
            'twelveth'    => ['val' => '12', 'suffix' => 'th', 'power' => 10],
            'twelfth'    => ['val' => '12', 'suffix' => 'th', 'power' => 10],
            'twelve'      => ['val' => '12', 'power' => 10],
            'thirteenth'  => ['val' => '13', 'suffix' => 'th', 'power' => 10],
            'thirteen'    => ['val' => '13', 'power' => 10],
            'fourteenth'  => ['val' => '14', 'suffix' => 'th', 'power' => 10],
            'fourteen'    => ['val' => '14', 'power' => 10],
            'fifteenth'   => ['val' => '15', 'suffix' => 'th', 'power' => 10],
            'fifteen'     => ['val' => '15', 'power' => 10],
            'sixteenth'   => ['val' => '16', 'suffix' => 'th', 'power' => 10],
            'sixteen'     => ['val' => '16', 'power' => 10],
            'seventeenth' => ['val' => '17', 'suffix' => 'th', 'power' => 10],
            'seventeen'   => ['val' => '17', 'power' => 10],
            'eighteenth'  => ['val' => '18', 'suffix' => 'th', 'power' => 10],
            'eighteen'    => ['val' => '18', 'power' => 10],
            'nineteenth'  => ['val' => '19', 'suffix' => 'th', 'power' => 10],
            'nineteen'    => ['val' => '19', 'power' => 10],
            'twentieth'   => ['val' => '20', 'suffix' => 'th', 'power' => 10],
            'twenty'      => ['val' => '20', 'power' => 10],
            'thirty'      => ['val' => '30', 'power' => 10],
            'forty'       => ['val' => '40', 'power' => 10],
            'fourty'      => ['val' => '40', 'power' => 10], // common misspelling
            'fifty'       => ['val' => '50', 'power' => 10],
            'sixty'       => ['val' => '60', 'power' => 10],
            'seventy'     => ['val' => '70', 'power' => 10],
            'eighty'      => ['val' => '80', 'power' => 10],
            'ninety'      => ['val' => '90', 'power' => 10],
            'hundred'     => ['val' => '100', 'power' => 100],
            'thousand'    => ['val' => '1000', 'power' => 1000],
            'million'     => ['val' => '1000000', 'power' => 1000000],
            'billion'     => ['val' => '1000000000', 'power' => 1000000000],
            'and'         => ['val' => '', 'power' => null],
            '-'           => ['val' => '', 'power' => null],
        ];
        $powers = array_column($tokens, 'power', 'val');

        $mutate = function ($parts) use (&$mutate, $powers){
            $stack = new \SplStack;
            $sum   = 0;
            $last  = null;

            foreach ($parts as $idx => $arr) {
                $part = $arr['val'];

                if (!$stack->isEmpty()) {
                    $check = $last ?? $part;

                    if ((float)$stack->top() < 20 && (float)$part < 20 ?? (float)$part < $stack->top() ) { //пропускаем спец числительные
                        return $stack->top().(isset($parts[$idx - $stack->count()]['suffix']) ? $parts[$idx - $stack->count()]['suffix'] : '')." ".$mutate(array_slice($parts, $idx));
                    }
                    if (isset($powers[$check]) && $powers[$check] <= $arr['power'] && $arr['power'] <= 10) { //но добавляем степени (сотни, тысячи, миллионы итп)
                        return $stack->top().(isset($parts[$idx - $stack->count()]['suffix']) ? $parts[$idx - $stack->count()]['suffix'] : '')." ".$mutate(array_slice($parts, $idx));
                    }
                    if ($stack->top() > $part) {
                        if ($last >= 1000) {
                            $sum += $stack->pop();
                            $stack->push($part);
                        } else {
                            // twenty one -> "20 1" -> "20 + 1"
                            $stack->push($stack->pop() + (float) $part);
                        }
                    } else {
                        $stack->push($stack->pop() * (float) $part);
                    }
                } else {
                    $stack->push($part);
                }

                $last = $part;
            }

            return $sum + $stack->pop();
        };

        $prepared = preg_split('/(['.$delims.'])/', $input, -1, PREG_SPLIT_DELIM_CAPTURE);

        //Замена на токены
        foreach ($prepared as $idx => $word) {
            if (is_array($word)) {continue;}
            $maybeNumPart = trim(strtolower($word));
            if (isset($tokens[$maybeNumPart])) {
                $item = $tokens[$maybeNumPart];
                if (isset($prepared[$idx+1])) {
                    $maybeDelim = $prepared[$idx+1];
                    if ($maybeDelim === " ") {
                        $item['delim'] = $maybeDelim;
                        unset($prepared[$idx + 1]);
                    } elseif ($item['power'] == null && !isset($tokens[$maybeDelim])) {
                        continue;
                    }
                }
                $prepared[$idx] = $item;
            }
        }

        $result      = [];
        $accumulator = [];

        $getNumeral = function () use ($mutate, &$accumulator, &$result) {
            $last        = end($accumulator);
            $result[]    = $mutate($accumulator).(isset($last['suffix']) ? $last['suffix'] : '').(isset($last['delim']) ? $last['delim'] : '');
            $accumulator = [];
        };

        foreach ($prepared as $part) {
            if (is_array($part)) {
                $accumulator[] = $part;
            } else {
                if (!empty($accumulator)) {
                    $getNumeral();
                }
                $result[] = $part;
            }
        }
        if (!empty($accumulator)) {
            $getNumeral();
        }

        return implode('', array_filter($result));
    }
}

$testStrings = [
    'thirty thirty eighty one one eighty' => '30 30 81 1 80',
    'twenty twenty' => '20 20',
    'twelfth eleventh tenth' => '12th 11th 10th',
    'ten eleven twelve' => '10 11 12',
    'one two five zero' => '1 2 5 0',
    'One First Two' => '1 1st 2',
    'One First Two Second Three Third Four Fourth Five Fifth Six Sixth Seven' => '1 1st 2 2nd 3 3rd 4 4th 5 5th 6 6th 7',
    'Bus number fifteen from bus stop number Eighty three thousand one hundred thirty nine' => 'Bus number 15 from bus stop number 83139',
    'get the fifteenth cookie from fifth jar on second left shelf' => 'get the 15th cookie from 5th jar on 2nd left shelf',
    'One hundred million monkeys could not write second Macbeth' => '100000000 monkeys could not write 2nd Macbeth',
    'Taganskaya str. thirty two, three hundred fifty six' => 'Taganskaya str. 32, 356',
    'Lenina str 56/17 b. one hundred seven' => 'Lenina str 56/17 b. 107',
    'Paris & Hilton road, twenty two, house 356' => 'Paris & Hilton road, 22, house 356',
    'Wien, Wilhelmstraße zwei hundert sieben und dreißig' => 'Wien, Wilhelmstraße zwei hundert sieben und dreißig',
    'Vienna, Wilhelmstrasse two hundred and thirty seven' => 'Vienna, Wilhelmstrasse 237',
];

$converter = new Converter();
foreach ($testStrings as $input => $expected) {
    $output = $converter::wordsToNumber($input);
    echo $input."\t=>\t".$output."\n";
    if ($output != $expected) { die("words to number conversion failed!");}
}
Это работает намного лучше, чем другие для номеров адресов, «одна-две-три-главная улица». или "одна двадцать три главных улицы".
-2

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

передайте строку, упомянутую ниже, и проверьте все:

"автобус номер пятнадцать от автобусной остановки номер восемьдесят три тысячи сто тридцать девять"

Это не ответ на вопрос. Это было бы лучше опубликовано в разделе комментариев.
4

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

<?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";

?>
Просто понял проблему. При доступе к первому ключу происходит что-то странное, то есть «один». Вместо этого положите «квадриллион»; = & GT; & APOS; 1000000000000000 & APOS; до "одного" и это работает со 100% точностью. user132513
Это также будет связываться с определенными формулировками для дат. "Я родился в тысяча девятьсот восемьдесят первом году"
Также включите «lakh»; = & GT; & APOS; 100000 & APOS; и "crore"; = & GT; & APOS; 10000000 & APOS; в ключах $. Это более распространенные термины, чем миллион в странах Южной Азии user132513
Спасибо большое Джоберу за код! Я постараюсь улучшить его. Я установил тестовый набор из 10000 слов случайных чисел (используя Numbers_Words), и в настоящее время точность декодирования слов в числа составляет 75%. Правильно: сорок пять тысяч пятьсот пятьдесят четыре становится 45554 Неверно: пятьдесят одна тысяча пятьсот восемьдесят шесть становится 586 user132513
0

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

Спасибо Яни. Этот пакет выглядит интересным, хотя он выполняет мою цель, то есть от цифр к словам. Будет полезно в будущих проектах. user132513
21

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

Well, overall you are doing two things: Finding tokens (words that translates to numbers) and applying grammar. In short, you are building a parser for a very limited language.

The tokens you would need are:

POWER: thousand, million, billion
HUNDRED: hundred
TEN: twenty, thirty... ninety
UNIT: one, two, three, ... nine,
SPECIAL: ten, eleven, twelve, ... nineteen

(drop any "and"s as they are meaningless. Break hyphens into two tokens. That is sixty-five should be processed as "sixty" "five")

Once you've tokenized your string, move from RIGHT TO LEFT.

Grab all the tokens from the RIGHT until you hit a POWER or the whole string.

Parse the tokens after the stop point for these patterns:

SPECIAL
TEN
UNIT
TEN UNIT
UNIT HUNDRED
UNIT HUNDRED SPECIAL
UNIT HUNDRED TEN
UNIT HUNDRED UNIT
UNIT HUNDRED TEN UNIT

(This assumes that "seventeen hundred" is not allowed in this grammar)

This gives you the last three digits of your number.

If you stopped at the whole string you are done.

If you stopped at a power, start again at step 1 until you reach a higher POWER or the whole string.

Спасибо, Джон! Этот алгоритм именно то, что я искал. Я пытался разобрать его слева направо, но это выглядит лучше. Ценю твою помощь! user132513
+1 Джон - Ваши ответы всегда великолепны.
Почему мы обрабатываем токены справа?
@joebert, потому что это легче кодировать :)
Я добавил ответ ниже, который реализует неопределенно похожий алгоритм.
19

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

После комментария пользователя 1323513 к ответу Джоберта я использовал Number_Words Pear для генерации серии тестов. Следующий код набрал 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();
}
Я обнаружил, что это очень надежное и емкое решение. Прекрасная работа! Моим единственным редактором было добавить'lakh' => '100000' а также'crore' => '10000000' как упомянуто user132513 в ответе joeberts.
@ElYobo Это хорошо работает, за исключением того, что если$data значениеTen вместоten потом возвращается0 вместо10 , Пожалуйста, помогите за этоcase sensitivity приятель.
«Недопустимые биты (например, числа с ошибками) будут игнорироваться, поэтому вывод неверных строк считается неопределенным»; к сожалению, это предназначено только для преобразования строки, содержащей одно число. Вы можете попытаться разбить вашу строку на фрагменты, используя$data список выше (так как это единственные подстроки, которые нас интересуют), а затем запустите его для каждого фрагмента, а затем объедините результаты, используя разделенные слова.
Я бы порекомендовал сделать это первой строкой:$data = strtolower(trim($data));, Это решает вопрос, высказанный @RajaGopal
Один случай использования, где это не работает, например,$data= 'five or ten', Это возвращает 50. Ответ выше работает хорошо для OP. Тем не менее, необходимо учитывать, что строка имеет «правильное» значение. форматирование. В моем случае я пытался вырезать число из непроверенной строки, не контролируя (или не зная), какой может быть строка. Пользователи иногда помещают довольно странные ответы в формы!

Похожие вопросы