Разбор простого текста таким образом, чтобы распознать пользовательский оператор if

У меня есть следующая строка:

$string = "The man has {NUM_DOGS} dogs."

Я разбираю это, запустив его через следующую функцию:

function parse_text($string)
{
    global $num_dogs;

    $string = str_replace('{NUM_DOGS}', $num_dogs, $string);

    return $string;
}

parse_text($string);

Где $num_dogs - заданная переменная. В зависимости от $num_dogs, это может вернуть любую из следующих строк:

  • У человека есть 1 собака.
  • У мужчины две собаки.
  • У человека 500 собак.

Проблема в том, что в случае, если "у человека есть 1 собака", собака плюрализована, что нежелательно. Я знаю, что это можно решить просто, не используя функцию parse_text и вместо этого сделав что-то вроде:

if($num_dogs = 1){
    $string = "The man has 1 dog.";
}else{
    $string = "The man has $num_dogs dogs.";
}

Но в моем приложении я разбираю больше, чем просто {NUM_DOGS}, и для записи всех условий потребуется много строк.

Мне нужен сокращенный путь, который я могу записать в начальный $string, который я могу запустить через синтаксический анализатор, что в идеале не ограничивало бы меня только двумя истинными/ложными возможностями.

Например, пусть

$string = 'The man has {NUM_DOGS} [{NUM_DOGS}|0=>"dogs",1=>"dog called fred",2=>"dogs called fred and harry",3=>"dogs called fred, harry and buster"].';

Ясно ли, что произошло в конце? Я попытался инициировать создание массива, используя часть внутри квадратных скобок, которая после вертикальной полосы, затем сравните ключ нового массива с анализируемым значением {NUM_DOGS} (который теперь будет переменной $num_dogs слева от вертикальной полосы) и вернуть значение записи массива с помощью этого ключа.

Если это не совсем запутывает, возможно ли использовать функции preg_ *?

Ответ 1

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

Кажется идеальным кандидатом для preg_replace_callback

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

По этому вопросу я собираюсь взять на себя ограниченный уровень сложности и решить его с помощью двухэтапного анализа с использованием регулярного выражения.


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

/{([^}]+)}/

Давайте сломаем это.

{        # A literal opening brace
(        # Begin capture
  [^}]+  # Everything that not a closing brace (one or more times)
)        # End capture
}        # Literal closing brace

При применении к строке с preg_match_all результаты выглядят примерно так:

array (
  0 => array (
    0 => 'A string {TOK_ONE}',
    1 => ' with {TOK_TWO|0=>"no", 1=>"one", 2=>"two"}',
  ),
  1 => array (
    0 => 'TOK_ONE',
    1 => 'TOK_TWO|0=>"no", 1=>"one", 2=>"two"',
  ),
)

Выглядит хорошо.

Обратите внимание, что если в ваших строках есть вложенные фигурные скобки, т.е. {TOK_TWO|0=>"hi {x} y"}, это регулярное выражение не будет работать. Если это не проблема, перейдите к следующему разделу.

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

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

Рекурсивное регулярное выражение взято из одного из моих других ответов и немного изменилось.

`/{((?:[^{}]*|(?R))*)}/`

Сломанный.

{                   # literal brace
(                   # begin capture
    (?:             # don't create another capture set
        [^{}]*      # everything not a brace
        |(?R)       # OR recurse
    )*              # none or more times
)                   # end capture
}                   # literal brace

И на этот раз вывод соответствует только скобкам верхнего уровня

array (
  0 => array (
    0 => '{TOK_ONE|0=>"a {nested} brace"}',
  ),
  1 => array (
    0 => 'TOK_ONE|0=>"a {nested} brace"',
  ),
)

Опять же, не используйте рекурсивное регулярное выражение, если вам не нужно. (Ваша система может даже не поддерживать их, если у нее есть старая библиотека PCRE)


С этим нам нужно работать, если у токена есть связанные с ним параметры. Вместо того, чтобы сопоставлять два фрагмента в соответствии с вашим вопросом, я бы рекомендовал сохранить варианты с токеном в соответствии с моими примерами. {TOKEN|0=>"option"}

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

/(\d)+\s*=>\s*"([^"]*)",?/

Сломанный.

(\d)+    # Capture one or more decimal digits
\s*      # Any amount of whitespace (allows you to do 0    =>    "")
=>       # Literal pointy arrow
\s*      # Any amount of whitespace
"        # Literal quote
([^"]*)  # Capture anything that isn't a quote
"        # Literal quote
,?       # Maybe followed by a comma

И пример соответствует

array (
  0 => array (
    0 => '0=>"no",',
    1 => '1 => "one",',
    2 => '2=>"two"',
  ),
  1 => array (
    0 => '0',
    1 => '1',
    2 => '2',
  ),
  2 => array (
    0 => 'no',
    1 => 'one',
    2 => 'two',
  ),
)

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


Завершение, здесь рабочий пример.

Некорректный код инициализации.

$options = array(
    'WERE' => 1,
    'TYPE' => 'cat',
    'PLURAL' => 1,
    'NAME' => 2
);

$string = 'There {WERE|0=>"was a",1=>"were"} ' .
    '{TYPE}{PLURAL|1=>"s"} named bob' . 
    '{NAME|1=>" and bib",2=>" and alice"}';

И все вместе.

$string = preg_replace_callback('/{([^}]+)}/', function($match) use ($options) {
    $match = $match[1];

    if (false !== $pipe = strpos($match, '|')) {
        $tokens = substr($match, $pipe + 1);
        $match = substr($match, 0, $pipe);
    } else {
        $tokens = array();
    }

    if (isset($options[$match])) {
        if ($tokens) {
            preg_match_all('/(\d)+\s*=>\s*"([^"]*)",?/', $tokens, $tokens);

            $tokens = array_combine($tokens[1], $tokens[2]);

            return $tokens[$options[$match]];
        }
        return $options[$match];
    }
    return '';
}, $string);

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

Вероятно, есть намного более простой способ сделать все это, но я просто взял идею и побежал с ней.

Ответ 2

Прежде всего, это немного спорно, но если вы можете легко избежать, просто передать $num_dogs в качестве аргумента функции, как большинство людей считают, глобальные переменные являются злыми!

Далее, для получения "s", я обычно делаю что-то вроде этого:

$dogs_plural = ($num_dogs == 1) ? '' : 's';

Затем просто сделайте что-то вроде этого:

$your_string = "The man has $num_dogs dog$dogs_plural";

Это по сути то же самое, что и для блока if/else, но меньше строк кода, и вам нужно только один раз написать текст.

Что касается другой части, я все равно смущен тем, что вы пытаетесь сделать, но я считаю, что вы ищете какой-то способ конвертировать

{NUM_DOGS}|0=>"dogs",1=>"dog called fred",2=>"dogs called fred and harry",3=>"dogs called fred, harry and buster"]

в

switch $num_dogs {
    case 0:
        return 'dogs';
        break;
    case 1:
        return 'dog called fred';
        break;
    case 2:
        return 'dogs called fred and harry';
        break;
    case 3:
        return 'dogs called fred, harry and buster';
        break;
}

Самый простой способ - попытаться использовать комбинацию explode() и regex, чтобы заставить его сделать что-то вроде выше.

Ответ 3

В крайнем случае, я сделал что-то похожее на то, что вы просите, с реализацией, как код ниже.

Это не так близко, как функция, богатая, как @Mike ответ, но она сделала трюк в прошлом.

/**
 * This function pluralizes words, as appropriate.
 *
 * It is a completely naive, example-only implementation.
 * There are existing "inflector" implementations that do this
 * quite well for many/most *English* words.
 */
function pluralize($count, $word)
{
    if ($count === 1)
    {
        return $word;
    }
    return $word . 's';
}

/**
 * Matches template patterns in the following forms:
 *   {NAME}       - Replaces {NAME} with value from $values['NAME']
 *   {NAME:word}  - Replaces {NAME:word} with 'word', pluralized using the pluralize() function above.
 */
function parse($template, array $values)
{
    $callback = function ($matches) use ($values) {
        $number = $values[$matches['name']];
        if (array_key_exists('word', $matches)) {
            return pluralize($number, $matches['word']);
        }
        return $number;
    };

    $pattern = '/\{(?<name>.+?)(:(?<word>.+?))?\}/i';
    return preg_replace_callback($pattern, $callback, $template);
}

Вот несколько примеров, похожих на ваш исходный вопрос...

echo parse(
    'The man has {NUM_DOGS} {NUM_DOGS:dog}.' . PHP_EOL,
    array('NUM_DOGS' => 2)
);

echo parse(
    'The man has {NUM_DOGS} {NUM_DOGS:dog}.' . PHP_EOL,
    array('NUM_DOGS' => 1)
);

Вывод:

У мужчины две собаки.

У человека есть 1 собака.

Возможно, стоит упомянуть, что в более крупных проектах я неизменно оказывал влияние на любой пользовательский перевернутый перегиб в пользу GNU gettext, который, как представляется, быть самым разумным способом вперед, когда требуется многоязычный язык.

Ответ 4

Это было скопировано из ответа, опубликованного flussence еще в 2009 году в ответ на этот вопрос:

Возможно, вы захотите посмотреть расширение gettext. Более конкретно, похоже, что ngettext() будет делать то, что вы хотите: он правильно умножает слова, если у вас есть число, на которое нужно рассчитывать.

print ngettext('odor', 'odors', 1); // prints "odor"
print ngettext('odor', 'odors', 4); // prints "odors"
print ngettext('%d cat', '%d cats', 4); // prints "4 cats"

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