Completed
Push — master ( 9f9f55...c83c49 )
by Josh
01:49
created

ShortestCommonSuperstring   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 233
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 98.63%

Importance

Changes 0
Metric Value
wmc 31
lcom 1
cbo 0
dl 0
loc 233
ccs 72
cts 73
cp 0.9863
rs 9.92
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getShortest() 0 19 3
A compareStrings() 0 13 3
A getIdenticalAffixKeys() 0 13 3
A mergeString() 0 15 4
A mergeStringPair() 0 6 1
A mergeStrings() 0 14 1
A mergeStringsGroup() 0 13 4
A removeEmptyStrings() 0 7 2
A removeFullyOverlappingStrings() 0 24 4
A resetKeys() 0 8 2
A sortStrings() 0 4 1
A storeAffixes() 0 14 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 11
	public function getShortest(array $strings): string
39
	{
40 11
		$this->strings = array_unique($strings);
41 11
		$this->sortStrings();
42 11
		$this->removeEmptyStrings();
43 11
		$this->removeFullyOverlappingStrings();
44 11
		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 9
			$this->len = strlen($this->strings[1]);
49 9
			while (--$this->len > 0)
50
			{
51 9
				$this->mergeStrings();
52
			}
53
		}
54
55 11
		return implode('', $this->strings);
56
	}
57
58
	/**
59
	* Compare given strings
60
	*/
61 9
	protected static function compareStrings(string $a, string $b): int
62
	{
63 9
		$aLen = strlen($a);
64 9
		$bLen = strlen($b);
65 9
		if ($aLen !== $bLen)
66
		{
67
			// Longest first
68 3
			return $bLen - $aLen;
69
		}
70
71
		// Lexical order
72 9
		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 9
	protected function getIdenticalAffixKeys(): array
81
	{
82 9
		$identicalAffixKeys = [];
83 9
		foreach ($this->prefixes as $k => $prefix)
84
		{
85 9
			if ($this->suffixes[$k] === $prefix)
86
			{
87 6
				$identicalAffixKeys[] = $k;
88
			}
89
		}
90
91 9
		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 9
	protected function mergeString(int $leftKey): bool
101
	{
102 9
		$suffix = $this->suffixes[$leftKey];
103 9
		foreach ($this->prefixes as $rightKey => $prefix)
104
		{
105 9
			if ($prefix === $suffix && $leftKey !== $rightKey)
106
			{
107 7
				$this->mergeStringPair($leftKey, $rightKey);
108
109 7
				return true;
110
			}
111
		}
112
113 9
		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 9
	protected function mergeStrings(): void
130
	{
131 9
		$this->storeAffixes();
132
133
		// Merge strings whose prefix is identical to their suffix
134 9
		$keys = $this->getIdenticalAffixKeys();
135 9
		$this->mergeStringsGroup($keys);
136
137
		// Merge the remaining strings that have a prefix stored
138 9
		$keys = array_diff(array_keys($this->prefixes), $keys);
139 9
		$this->mergeStringsGroup($keys);
140
141 9
		$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 9
	protected function mergeStringsGroup(array $keys): void
151
	{
152 9
		foreach ($keys as $leftKey)
153
		{
154 9
			if (isset($this->strings[$leftKey]))
155
			{
156 9
				while ($this->mergeString($leftKey))
157
				{
158
					// Keep going
159
				}
160
			}
161
		}
162
	}
163
164
	/**
165
	* Remove empty strings from the list
166
	*/
167 11
	protected function removeEmptyStrings(): void
168
	{
169 11
		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 11
	protected function removeFullyOverlappingStrings(): void
179
	{
180 11
		$strlen = array_map('strlen', $this->strings);
181 11
		$i      = count($this->strings);
182 11
		while (--$i > 0)
183
		{
184 9
			$str = $this->strings[$i];
185 9
			$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 9
			$j = -1;
190 9
			while ($strlen[++$j] > $len)
191
			{
192 3
				if (strpos($this->strings[$j], $str) !== false)
193
				{
194 2
					unset($this->strings[$i]);
195 2
					break;
196
				}
197
			}
198
		}
199
200 11
		$this->resetKeys();
201
	}
202
203
	/**
204
	* Reset the keys in the string list to remove the gaps
205
	*/
206 11
	protected function resetKeys(): void
207
	{
208 11
		end($this->strings);
209 11
		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 11
	protected function sortStrings(): void
219
	{
220 11
		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 9
	protected function storeAffixes(): void
229
	{
230 9
		$this->prefixes = [];
231 9
		$this->suffixes = [];
232 9
		foreach ($this->strings as $str)
233
		{
234 9
			if (strlen($str) <= $this->len)
235
			{
236
				break;
237
			}
238 9
			$this->prefixes[] = substr($str, 0, $this->len);
239 9
			$this->suffixes[] = substr($str, -$this->len);
240
		}
241
	}
242
}