Генератор строк из шаблона

30 марта 2011 г.

Устраиваясь на должность младшего-программиста, мне дали выполнить несколько тестовых заданий. Часть из них я решил на месте, а часть была в виде домашнего задания. Меня заинтересовало одно из них, вот его текст:

На PHP написать класс StringGenerator — генератор строк из шаблона. В конструкторе задается шаблон. Затем вызывается метод, генерирующий заданное количество строк(строки не должны повторяться). В шаблоне, в фигурных скобках записаны варианты слов/словосочетаний. Разделителем служит “|”.

$strPattern=”{Данная|Эта}” строка {должна быть|будет} сгенерирована {методом{класса StringGenerator|gerateString} автоматически|заданное число раз}.”;
$objStringGenerator = new StringGenerator($strPattern);
$objStringGenerator -> generateString(3);
Данный код должен вывести на экран 3 различных строки.

Следующий день оказался суматошным и продумывать работу пришлось в метро с ручкой и ежедневником. На бумаге всё было хорошо, но перевести всё в работающий код не получилось. Шестое чувство подсказало, что задачка типовая и не зачем придумывать велосипеды, когда уже должны быть такие. И это было моей первой ошибкой. Google в паре с Yandex, находили, не то чего я ожидал. Но спустя некоторое время я попал сюда. Не буду долго описывать мою первую радость, а потом разочарование. Углубившись в более гремучие дебри, велосипедов я не находил. Ушел в «изучение» шаблонизаторов. Один из форумов(или хабр) меня познакомил с рекурсивно-нисходящими парсерами, БНФ. Начал уже искать велосипеды с подобными ключевыми словами. Увидя то количество кода, что предлагали в виде «простого примера», понял, что всё это стрельба из пушки по воробьям. И решил вернуться к своему велосипеду.

Отоспавшись два дня, я создал новый файл, достал ежедневник и ручку. Я работаю медленно, но верно. Мой алгоритм был прост: блок — массивы, несколько блоков, несколько массивов. В каждом своё количество элементов. И подписав каждый блок, решил что генерировать строки будет проще, если будут известны варианты комбинаций заранее. В итоге была написана функция, которая зная максимальное количество элементов в каждом блоке генерировала значения.
Например, если шаблон: “{Слово0|Слово1}{Слово2|Слово3|Слово4|Слово5}{Слово6|Слово7|Слово8}”, то массив в итоге хранил:
132
131
130
122
121

001
000

Потом была написана рекурсивная функция, которая перебирала строку. «Потом», потому что в первый раз сгенерировать варианты комбинаций я не смог и думал, что снова не получиться. Функция перебирала строку-шаблон с помощью регулярного выражения и находила самый низкий блок(внутри которого нет других), передавала найденный блок в функцию, где он резался, сохранялся его номер и вычислялось количество элементов. Блок в строке удалялся и снова обрабатывался, пока поиски шаблонов были возможны. Этим самым я избавлялся от проблемы вложенных блоков. Написав, ещё парочку вспомогательных функций, всё это было собрано в класс, вот его код:

<?php
class StrinGenerator{

var $str; //исходная строка
var $max_arr; //массив, в котором храним максимальное количество вариантов для блока
var $result=array(); //массив выходных строк
var $variant_arr; //массив в котором храним возможные варианты
var $count_i=0; //глобальный счетчик строк, поможет при переборе вариантов
var $count_str; //количество нужных строк
var $step; //переменная является порядковым номером блока
var $key_stop=false; //глобальный ключ для прекращения работы

function StrinGenerator($str){
 $this->str=$str;
 $this->pre_pars($str); //рекурсивный перебор строки, формирует массив, в котором храним максимальное значение ключа для каждого блока
 $this->func_variant_arr(); //генерируем массив с вариантами комбинаций(строк) 
}

function generateString($count){
$this->count_str = $count;

while(true){
 $this->step=0;
 $this->count_i++; //увеличиваем счетчик => смотрим следующий вариант строки 
 $res_tmp=$this->pars($this->str); //вызываем рекурсивную функцию для перебора и генерирования строки
 if(!in_array($res_tmp, $this->result)) { //если нет повторений
 $this->result[] = $res_tmp; //сохраняем новый варант строки
 if(count($this->result) == $this->count_str OR $this->key_stop) { //если нужное количество или глобальный ключ=true
 $this->echo_result(); //вызов функции вывода
 break;
 }
 }
 }
}

function pre_pars($str){
 if (preg_match('|{([^{]*?)}|i', $str)){ //если есть блоки
 $text=preg_replace('|{([^{]*?)}|sie', "\$this->arr_count('\\0')",$str); //захватили и передали
 return $this->pre_pars($text); 
 }
}

function arr_count($text){
 $arr=explode("|",$text);
 $this->max_arr[]=count($arr)-1; //сохраняем максимальное значение ключа для данного блока
}

function pars($str){
 if (preg_match('|{(?=[^\{]*\}).*?}|i', $str)){ //если есть блоки
 $text=preg_replace('|{([^{]*?)}|sie', "\$this->return_res('\\1')",$str); //захватили, передали, заменили на один из варинатов
 return $this->pars($text); //снова перебираем, помогает справиться с вложенными блоками
 } else {
 return $str; //возвратили строку
 }
}

function return_res($text){
 $res=explode("|",$text);
 $variant_str=$this->variant_arr[$this->count_i]; //выбираем комбинацию
 $key=substr($variant_str,$this->step++,1); //выбираем значение для обрабатываемого блока, вариант из слов
 if($variant_str==0) $this->key_stop=true; //дошли до последней возможной комбинации, прекращаем работу
 return $res[$key]; 
}

function func_variant_arr(){
 $max_arr=$this->max_arr;
 
 foreach($max_arr as $value){ //фомируем из массива число, легче для подсчетов комбинаций
 $max.= $value;
 }

while($max>=0){
 $max=str_pad($max,4,0,STR_PAD_LEFT); //дополняем число,чтобы вариант комбинации был "целым"
 $this->variant_arr[]=$max--;
 for($n=0;$n<count($max_arr);$n++){
 if (substr($max,$n,1)>$max_arr[$n]) {$max=substr_replace($max,$max_arr[$n],$n, 1);}; //проверка если разряд комбинации(номер блока) больше максимального значения, то приравниваем этому самому значению
 }
 }
}

function echo_result(){ //вывод строк
 foreach($this->result as $value){
 echo $value."
";
 }
}

}

$obj=new StrinGenerator("{Данная|Эта} строка, {должна быть|будет} сгенерирована {методом {класса StringGenerator|generateString} автоматически|заданное число раз}.");
$obj->generateString(5);

?>

Код прост, не используется никаких сложных структур. В этом его плюс, хотя есть люди «свободно читающие» на PHP, которые пробегают по коду, даже не задумывавшись и всё прекрасно поняв. Код занял меньше 100 строк, более чем уверен, что его ещё можно упростить процентов на 20-30.

Теги: рубрика Программирование
  • Похожие статьи
  • Предыдущие из рубрики