Completed
Push — master ( c83c49...70979f )
by Josh
01:36
created

ShortestCommonSuperstring::storeAffixes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 8
cts 8
cp 1
rs 9.7998
c 0
b 0
f 0
cc 3
nc 3
nop 0
crap 3
1
<?php declare(strict_types=1);
2
3
/**
4
* @package   s9e\ShortestCommonSuperstring
5
* @copyright Copyright (c) 2016-2019 The s9e authors
6
* @license   http://www.opensource.org/licenses/mit-license.php The MIT License
7
*/
8
namespace s9e\ShortestCommonSuperstring;
9
10
class ShortestCommonSuperstring
11
{
12
	/**
13
	* @var integer Affix length for current iteration
14
	*/
15
	protected $len;
16
17
	/**
18
	* @var string[] Prefixes of current length
19
	*/
20
	protected $prefixes;
21
22
	/**
23
	* @var string[] Strings being merged
24
	*/
25
	protected $strings;
26
27
	/**
28
	* @var string[] Suffixes of current length
29
	*/
30
	protected $suffixes;
31
32
	/**
33
	* Get the shortest string that contains all given strings
34
	*
35
	* @param  string[] $strings
36
	* @return string
37
	*/
38 12
	public function getShortest(array $strings): string
39
	{
40 12
		$this->strings = array_unique($strings);
41 12
		$this->sortStrings();
42 12
		$this->removeEmptyStrings();
43 12
		$this->removeFullyOverlappingStrings();
44 12
		if (isset($this->strings[1]))
45
		{
46
			// Start with the longest partial match possible, which is equal to the length of the
47
			// second longest string minus 1
48 10
			$this->len = strlen($this->strings[1]);
49 10
			while (--$this->len > 0)
50
			{
51 10
				$this->mergeStrings();
52
			}
53
		}
54
55 12
		return implode('', $this->strings);
56
	}
57
58
	/**
59
	* Compare given strings
60
	*/
61 10
	protected static function compareStrings(string $a, string $b): int
62
	{
63 10
		$aLen = strlen($a);
64 10
		$bLen = strlen($b);
65 10
		if ($aLen !== $bLen)
66
		{
67
			// Longest first
68 4
			return $bLen - $aLen;
69
		}
70
71
		// Lexical order
72 10
		return ($a > $b) ? 1 : -1;
73
	}
74
75
	/**
76
	* Return the list of keys pointing to strings whose prefix is identical to their suffix
77
	*
78
	* @return integer[]
79
	*/
80 10
	protected function getIdenticalAffixKeys(): array
81
	{
82 10
		$identicalAffixKeys = [];
83 10
		foreach ($this->prefixes as $k => $prefix)
84
		{
85 10
			if ($this->suffixes[$k] === $prefix)
86
			{
87 6
				$identicalAffixKeys[] = $k;
88
			}
89
		}
90
91 10
		return $identicalAffixKeys;
92
	}
93
94
	/**
95
	* Match and merge a string by key
96
	*
97
	* @param  integer $leftKey Left string's key
98
	* @return bool             Whether a match was found and strings merged
99
	*/
100 10
	protected function mergeString(int $leftKey): bool
101
	{
102 10
		$suffix = $this->suffixes[$leftKey];
103 10
		foreach ($this->prefixes as $rightKey => $prefix)
104
		{
105 10
			if ($prefix === $suffix && $leftKey !== $rightKey)
106
			{
107 7
				$this->mergeStringPair($leftKey, $rightKey);
108
109 7
				return true;
110
			}
111
		}
112
113 10
		return false;
114
	}
115
116
	/**
117
	* Merge two stored strings together at current affix length
118
	*/
119 7
	protected function mergeStringPair(int $leftKey, int $rightKey): void
120
	{
121 7
		$this->strings[$leftKey] .= substr($this->strings[$rightKey], $this->len);
122 7
		$this->suffixes[$leftKey] = $this->suffixes[$rightKey];
123 7
		unset($this->prefixes[$rightKey], $this->strings[$rightKey]);
124
	}
125
126
	/**
127
	* Merge all stored strings using current affix length
128
	*/
129 10
	protected function mergeStrings(): void
130
	{
131 10
		$this->storeAffixes();
132
133
		// Merge strings whose prefix is identical to their suffix
134 10
		$keys = $this->getIdenticalAffixKeys();
135 10
		$this->mergeStringsGroup($keys);
136
137
		// Merge the remaining strings that have a prefix stored
138 10
		$keys = array_diff(array_keys($this->prefixes), $keys);
139 10
		$this->mergeStringsGroup($keys);
140
141 10
		$this->resetKeys();
142
	}
143
144
	/**
145
	* Match and merge strings from given group
146
	*
147
	* @param  integer[] $keys List of keys
148
	* @return void
149
	*/
150 10
	protected function mergeStringsGroup(array $keys): void
151
	{
152 10
		foreach ($keys as $leftKey)
153
		{
154 10
			if (isset($this->strings[$leftKey]))
155
			{
156 10
				while ($this->mergeString($leftKey))
157
				{
158
					// Keep going
159
				}
160
			}
161
		}
162
	}
163
164
	/**
165
	* Remove empty strings from the list
166
	*/
167 12
	protected function removeEmptyStrings(): void
168
	{
169 12
		if (end($this->strings) === '')
170
		{
171 1
			array_pop($this->strings);
172
		}
173
	}
174
175
	/**
176
	* Remove fully-overlapping strings from the list
177
	*/
178 12
	protected function removeFullyOverlappingStrings(): void
179
	{
180 12
		$strlen = array_map('strlen', $this->strings);
181 12
		$i      = count($this->strings);
182 12
		while (--$i > 0)
183
		{
184 10
			$str = $this->strings[$i];
185 10
			$len = $strlen[$i];
186
187
			// Iterate over strings starting with the longest. Stop when we reach strings the size
188
			// of the current string
189 10
			$j = -1;
190 10
			while ($strlen[++$j] > $len)
191
			{
192 4
				if (strpos($this->strings[$j], $str) !== false)
193
				{
194 2
					unset($this->strings[$i]);
195 2
					break;
196
				}
197
			}
198
		}
199
200 12
		$this->resetKeys();
201
	}
202
203
	/**
204
	* Reset the keys in the string list to remove the gaps
205
	*/
206 12
	protected function resetKeys(): void
207
	{
208 12
		end($this->strings);
209 12
		if (key($this->strings) !== count($this->strings) - 1)
210
		{
211 6
			$this->strings = array_values($this->strings);
212
		}
213
	}
214
215
	/**
216
	* Sort the stored strings
217
	*/
218 12
	protected function sortStrings(): void
219
	{
220 12
		usort($this->strings, [__CLASS__, 'compareStrings']);
221
	}
222
223
	/**
224
	* Capture and stored affixes of current length
225
	*
226
	* Will only store affixes from strings that are longer than current affix length
227
	*/
228 10
	protected function storeAffixes(): void
229
	{
230 10
		$this->prefixes = [];
231 10
		$this->suffixes = [];
232 10
		foreach ($this->strings as $str)
233
		{
234 10
			if (strlen($str) <= $this->len)
235
			{
236 1
				break;
237
			}
238 10
			$this->prefixes[] = substr($str, 0, $this->len);
239 10
			$this->suffixes[] = substr($str, -$this->len);
240
		}
241
	}
242
}