Разбор простого текста таким образом, чтобы распознать пользовательский оператор 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_ *?

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

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

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

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


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

 /{([^}]+)}/ 

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

 { # A literal opening brace ( # Begin capture [^}]+ # Everything that's 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 содержит совпадающий токен, если мы проверим наличие трубы | , и возьмем подстроку всего после нее, мы останемся с вашим списком опций, снова мы сможем использовать regex для их анализа. (Не беспокойтесь, я приведу все вместе в конце)

/(\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); 

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

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

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

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

Это не так близко, как функция, богатая, как ответ Майка, но в прошлом это было трюком.

 /** * 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, который, по-видимому, является самым разумным способом, когда требуется многоязычное.

Это было скопировано из ответа, опубликованного флюссенцией еще в 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" 

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