Completed
Branch MetaCharacters (822c09)
by Josh
02:24
created

Serializer::serializeCharacterClassUnit()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
/**
4
* @package   s9e\RegexpBuilder
5
* @copyright Copyright (c) 2016-2018 The s9e Authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\RegexpBuilder;
9
10
use s9e\RegexpBuilder\MetaCharacters;
11
use s9e\RegexpBuilder\Output\OutputInterface;
12
13
class Serializer
14
{
15
	/**
16
	* @var Escaper
17
	*/
18
	protected $escaper;
19
20
	/**
21
	* @var MetaCharacters
22
	*/
23
	protected $meta;
24
25
	/**
26
	* @var OutputInterface
27
	*/
28
	protected $output;
29
30
	/**
31
	* @param OutputInterface $output
32
	* @parm  MetaCharacters  $meta
33
	* @param Escaper         $escaper
34
	*/
35 13
	public function __construct(OutputInterface $output, MetaCharacters $meta, Escaper $escaper)
36
	{
37 13
		$this->escaper = $escaper;
38 13
		$this->meta    = $meta;
39 13
		$this->output  = $output;
40 13
	}
41
42
	/**
43
	* Serialize given strings into a regular expression
44
	*
45
	* @param  array[] $strings
46
	* @return string
47
	*/
48 13
	public function serializeStrings(array $strings)
49
	{
50 13
		$info = $this->analyzeStrings($strings);
51 13
		$alternations = $this->buildAlternations($info);
52 13
		$expr = implode('|', $alternations);
53
54 13
		if (count($alternations) > 1 || $this->isOneOptionalString($info))
55
		{
56 3
			$expr = '(?:' . $expr . ')';
57
		}
58
59 13
		return $expr . $info['quantifier'];
60
	}
61
62
	/**
63
	* Analyze given strings to determine how to serialize them
64
	*
65
	* The returned array may contains any of the following elements:
66
	*
67
	*  - (string) quantifier Either '' or '?'
68
	*  - (array)  chars      List of values from single-char strings
69
	*  - (array)  strings    List of multi-char strings
70
	*
71
	* @param  array[] $strings
72
	* @return array
73
	*/
74 13
	protected function analyzeStrings(array $strings)
75
	{
76 13
		$info = ['quantifier' => ''];
77 13
		if ($strings[0] === [])
78
		{
79 4
			$info['quantifier'] = '?';
80 4
			unset($strings[0]);
81
		}
82
83 13
		$chars = $this->getChars($strings);
84 13
		if (count($chars) > 1)
85
		{
86 10
			$info['chars'] = array_values($chars);
87 10
			$strings = array_diff_key($strings, $chars);
88
		}
89
90 13
		$info['strings'] = array_values($strings);
91
92 13
		return $info;
93
	}
94
95
	/**
96
	* Build the list of alternations based on given info
97
	*
98
	* @param  array    $info
99
	* @return string[]
100
	*/
101 13
	protected function buildAlternations(array $info)
102
	{
103 13
		$alternations = [];
104 13
		if (!empty($info['chars']))
105
		{
106 10
			$alternations[] = $this->serializeCharacterClass($info['chars']);
107
		}
108 13
		foreach ($info['strings'] as $string)
109
		{
110 5
			$alternations[] = $this->serializeString($string);
111
		}
112
113 13
		return $alternations;
114
	}
115
116
	/**
117
	* Return the portion of strings that are composed of a single character
118
	*
119
	* @param  array[]
120
	* @return array   String key => value
121
	*/
122 13
	protected function getChars(array $strings)
123
	{
124 13
		$chars = [];
125 13
		foreach ($strings as $k => $string)
126
		{
127 13
			if ($this->isChar($string))
128
			{
129 13
				$chars[$k] = $string[0];
130
			}
131
		}
132
133 13
		return $chars;
134
	}
135
136
	/**
137
	* 
138
	*
139
	* @return void
140
	*/
141 13
	protected function isChar(array $string)
142
	{
143 13
		if (count($string) !== 1 || is_array($string[0]))
144
		{
145 5
			return false;
146
		}
147
148 12
		return $this->meta->isChar($string[0]);
149
	}
150
151
	/**
152
	* Get the list of ranges that cover all given values
153
	*
154
	* @param  integer[] $values Ordered list of values
155
	* @return array[]           List of ranges in the form [start, end]
156
	*/
157 10
	protected function getRanges(array $values)
158
	{
159 10
		$i      = 0;
160 10
		$cnt    = count($values);
161 10
		$start  = $values[0];
162 10
		$end    = $start;
163 10
		$ranges = [];
164 10
		while (++$i < $cnt)
165
		{
166 10
			if ($values[$i] === $end + 1)
167
			{
168 8
				++$end;
169
			}
170
			else
171
			{
172 7
				$ranges[] = [$start, $end];
173 7
				$start = $end = $values[$i];
174
			}
175
		}
176 10
		$ranges[] = [$start, $end];
177
178 10
		return $ranges;
179
	}
180
181
	/**
182
	* Test whether a string is optional and has more than one character
183
	*
184
	* @param  array $info
185
	* @return bool
186
	*/
187 13
	protected function isOneOptionalString(array $info)
188
	{
189
		// Test whether the first string has a quantifier and more than one element
190 13
		return (!empty($info['quantifier']) && isset($info['strings'][0][1]));
191
	}
192
193
	/**
194
	* Serialize a given list of values into a character class
195
	*
196
	* @param  integer[] $values
197
	* @return string
198
	*/
199 10
	protected function serializeCharacterClass(array $values)
200
	{
201 10
		$expr = '[';
202 10
		foreach ($this->getRanges($values) as list($start, $end))
203
		{
204 10
			$expr .= $this->serializeCharacterClassUnit($start);
205 10
			if ($end > $start)
206
			{
207 8
				if ($end > $start + 1)
208
				{
209 1
					$expr .= '-';
210
				}
211 10
				$expr .= $this->serializeCharacterClassUnit($end);
212
			}
213
		}
214 10
		$expr .= ']';
215
216 10
		return $expr;
217
	}
218
219
	/**
220
	* 
221
	*
222
	* @return void
223
	*/
224 10
	protected function serializeCharacterClassUnit($value)
225
	{
226 10
		return $this->serializeValue($value, 'escapeCharacterClass');
227
	}
228
229
	/**
230
	* 
231
	*
232
	* @return void
233
	*/
234 5
	protected function serializeLiteral($value)
235
	{
236 5
		return $this->serializeValue($value, 'escapeLiteral');
237
	}
238
239
	/**
240
	* 
241
	*
242
	* @return void
243
	*/
244 13
	protected function serializeValue($value, $escapeMethod)
245
	{
246 13
		return ($value < 0) ? $this->meta->getExpression($value) : $this->escaper->$escapeMethod($this->output->output($value));
247
	}
248
249
	/**
250
	* Serialize an element from a string
251
	*
252
	* @param  array|integer $element
253
	* @return string
254
	*/
255 5
	protected function serializeElement($element)
256
	{
257 5
		return (is_array($element)) ? $this->serializeStrings($element) : $this->serializeLiteral($element);
258
	}
259
260
	/**
261
	* Serialize a given string into a regular expression
262
	*
263
	* @param  array  $string
264
	* @return string
265
	*/
266 5
	protected function serializeString(array $string)
267
	{
268 5
		return implode('', array_map([$this, 'serializeElement'], $string));
269
	}
270
}