Completed
Push — master ( 68eef5...7fee1f )
by Josh
01:38
created

ShortestCommonSuperstring::resetKeys()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2
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
	* Match and merge a string by key
77
	*
78
	* @param  integer $leftKey Left string's key
79
	* @return bool             Whether a match was found and strings merged
80
	*/
81 7
	protected function mergeString(int $leftKey): bool
82
	{
83
		// Temporarily blank this string's prefix from the array to avoid matches
84 7
		$prefix = $this->prefixes[$leftKey];
85 7
		$this->prefixes[$leftKey] = '';
86
87
		// Search for a prefix that matches this string's suffix before restoring its prefix
88 7
		$rightKey = array_search($this->suffixes[$leftKey], $this->prefixes, true);
89 7
		$this->prefixes[$leftKey] = $prefix;
90
91 7
		if ($rightKey === false)
92
		{
93 7
			return false;
94
		}
95
96 7
		$this->mergeStringPair($leftKey, $rightKey);
97
98 7
		return true;
99
	}
100
101
	/**
102
	* Merge two stored strings together at current affix length
103
	*/
104 7
	protected function mergeStringPair(int $leftKey, int $rightKey): void
105
	{
106 7
		$this->strings[$leftKey] .= substr($this->strings[$rightKey], $this->len);
107 7
		$this->suffixes[$leftKey] = $this->suffixes[$rightKey];
108 7
		unset($this->prefixes[$rightKey], $this->strings[$rightKey], $this->suffixes[$rightKey]);
109
	}
110
111
	/**
112
	* Merge all stored strings using current affix length
113
	*/
114 10
	protected function mergeStrings(): void
115
	{
116 10
		$this->storeAffixes();
117
118
		// Merge strings whose prefix is identical to their suffix
119 10
		$keys = array_keys(array_intersect_assoc($this->prefixes, $this->suffixes));
120 10
		$this->mergeStringsGroup($keys);
121
122
		// Merge strings that have a suffix that matches a prefix
123 10
		$keys = array_keys(array_intersect($this->suffixes, $this->prefixes));
124 10
		$this->mergeStringsGroup($keys);
125
126 10
		$this->resetKeys();
127
	}
128
129
	/**
130
	* Match and merge strings from given group
131
	*
132
	* @param  integer[] $keys List of keys
133
	* @return void
134
	*/
135 10
	protected function mergeStringsGroup(array $keys): void
136
	{
137 10
		foreach ($keys as $leftKey)
138
		{
139 7
			if (isset($this->strings[$leftKey]))
140
			{
141 7
				while ($this->mergeString($leftKey))
142
				{
143
					// Keep going
144
				}
145
			}
146
		}
147
	}
148
149
	/**
150
	* Remove empty strings from the list
151
	*/
152 12
	protected function removeEmptyStrings(): void
153
	{
154 12
		if (end($this->strings) === '')
155
		{
156 1
			array_pop($this->strings);
157
		}
158
	}
159
160
	/**
161
	* Remove fully-overlapping strings from the list
162
	*/
163 12
	protected function removeFullyOverlappingStrings(): void
164
	{
165
		// Copy the list of strings ordered by ascending length and create a master string by
166
		// concatenating them all
167 12
		$strings = array_reverse($this->strings, true);
168 12
		$all     = implode('', $strings);
169 12
		$pos     = 0;
170 12
		foreach ($strings as $i => $str)
171
		{
172
			// Test whether current string potentially appears in any subsequent, bigger strings
173 10
			$pos += strlen($str);
174 10
			if (strpos($all, $str, $pos) === false)
175
			{
176 10
				continue;
177
			}
178
179
			// Iterate over strings from the longest to the one before current string
180 2
			$j = -1;
181 2
			while (++$j < $i)
182
			{
183 2
				if (strpos($strings[$j], $str) !== false)
184
				{
185 2
					unset($this->strings[$i]);
186 2
					break;
187
				}
188
			}
189
		}
190
191 12
		$this->resetKeys();
192
	}
193
194
	/**
195
	* Reset the keys in the string list to remove the gaps
196
	*/
197 12
	protected function resetKeys(): void
198
	{
199 12
		end($this->strings);
200 12
		if (key($this->strings) !== count($this->strings) - 1)
201
		{
202 6
			$this->strings = array_values($this->strings);
203
		}
204
	}
205
206
	/**
207
	* Sort the stored strings
208
	*/
209 12
	protected function sortStrings(): void
210
	{
211 12
		usort($this->strings, [__CLASS__, 'compareStrings']);
212
	}
213
214
	/**
215
	* Capture and stored affixes of current length
216
	*
217
	* Will only store affixes from strings that are longer than current affix length
218
	*/
219 10
	protected function storeAffixes(): void
220
	{
221 10
		$this->prefixes = [];
222 10
		$this->suffixes = [];
223 10
		foreach ($this->strings as $str)
224
		{
225 10
			if (strlen($str) <= $this->len)
226
			{
227 1
				break;
228
			}
229 10
			$this->prefixes[] = substr($str, 0, $this->len);
230 10
			$this->suffixes[] = substr($str, -$this->len);
231
		}
232
	}
233
}