Skip to content

Commit a8bc7e9

Browse files
committed
Unique string combinations
1 parent ca49620 commit a8bc7e9

9 files changed

+388
-27
lines changed

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
language: php
22

33
php:
4+
- 5.6
45
- 7.0
56
- 7.1
7+
- 7.2
68

79
before_script:
810
- travis_retry composer self-update

README.md

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,86 @@ roar-roar-meow
158158
roar-roar-roar
159159
```
160160

161+
**No duplicates**
162+
163+
You can avoid generating stings having the same letter more than once with the `withoutDuplicates()` method:
164+
165+
```php
166+
$combinations = string_combinations('abc');
167+
var_dump(count($combinations)); // 39
168+
print_r($combinations->asArray());
169+
```
170+
171+
> Array
172+
(
173+
[0] => a
174+
[1] => b
175+
[2] => c
176+
[3] => aa
177+
[4] => ab
178+
[5] => ac
179+
[6] => ba
180+
[7] => bb
181+
[8] => bc
182+
[9] => ca
183+
[10] => cb
184+
[11] => cc
185+
[12] => aaa
186+
[13] => aab
187+
[14] => aac
188+
[15] => aba
189+
[16] => abb
190+
[17] => abc
191+
[18] => aca
192+
[19] => acb
193+
[20] => acc
194+
[21] => baa
195+
[22] => bab
196+
[23] => bac
197+
[24] => bba
198+
[25] => bbb
199+
[26] => bbc
200+
[27] => bca
201+
[28] => bcb
202+
[29] => bcc
203+
[30] => caa
204+
[31] => cab
205+
[32] => cac
206+
[33] => cba
207+
[34] => cbb
208+
[35] => cbc
209+
[36] => cca
210+
[37] => ccb
211+
[38] => ccc
212+
)
213+
214+
215+
```php
216+
$combinations = $combinations->withoutDuplicates();
217+
var_dump(count($combinations)); // 15
218+
print_r($combinations->asArray());
219+
```
220+
221+
> Array
222+
(
223+
[0] => a
224+
[1] => b
225+
[2] => c
226+
[3] => ab
227+
[4] => ac
228+
[5] => ba
229+
[6] => bc
230+
[7] => ca
231+
[8] => cb
232+
[9] => abc
233+
[10] => acb
234+
[11] => bac
235+
[12] => bca
236+
[13] => cab
237+
[14] => cba
238+
)
239+
240+
161241
Performance considerations
162242
--------------------------
163243

@@ -179,13 +259,13 @@ printf(
179259
'Generated %d combinations in %ss - Memory usage: %sMB / Peak usage: %sMB' . PHP_EOL,
180260
++$c,
181261
round($end - $start, 3),
182-
round(memory_get_usage() / 1024 / 1024),
183-
round(memory_get_peak_usage() / 1024 / 1024)
262+
round(memory_get_usage(true) / 1024 / 1024),
263+
round(memory_get_peak_usage(true) / 1024 / 1024)
184264
);
185265
```
186266

187267
Output:
188-
> Generated 19173960 combinations in 5.579s - Memory usage: 0MB / Peak usage: 1MB
268+
> Generated 19173960 combinations in 5.579s - Memory usage: 2MB / Peak usage: 2MB
189269
190270

191271
Tests

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"psr-4": {
88
"BenTools\\StringCombinations\\" : "src"
99
},
10-
"files": ["src/function.php"]
10+
"files": ["src/functions.php"]
1111
},
1212
"autoload-dev": {
1313
"psr-4": {

phpunit.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<testsuites>
1919
<testsuite name="Test Suite">
2020
<file>tests/TestStringCombinations.php</file>
21+
<file>tests/NoDuplicateLettersStringCombinationsTest.php</file>
2122
</testsuite>
2223
</testsuites>
2324
<filter>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
namespace BenTools\StringCombinations;
4+
5+
use Countable;
6+
use IteratorAggregate;
7+
8+
final class NoDuplicateLettersStringCombinations implements IteratorAggregate, Countable
9+
{
10+
/**
11+
* @var StringCombinations
12+
*/
13+
private $stringCombinations;
14+
15+
/**
16+
* UniqueStringCombinations constructor.
17+
*/
18+
public function __construct(StringCombinations $stringCombinations)
19+
{
20+
$this->stringCombinations = $stringCombinations;
21+
}
22+
23+
/**
24+
* @inheritDoc
25+
*/
26+
public function getIterator()
27+
{
28+
for ($i = $this->stringCombinations->min; $i <= $this->stringCombinations->max; $i++) {
29+
foreach ($this->permute($this->stringCombinations->charset, $i) as $combination) {
30+
yield implode($this->stringCombinations->glue, $combination);
31+
}
32+
}
33+
}
34+
35+
/**
36+
* @inheritDoc
37+
*/
38+
public function count()
39+
{
40+
$arr = [];
41+
42+
for ($pos = $this->stringCombinations->max, $i = 0; $pos >= $this->stringCombinations->min; $pos--, $i++) {
43+
if (0 === $i) {
44+
$arr[$i] = [$pos];
45+
} else {
46+
$arr[$i] = array_merge($arr[$i - 1], [$pos]);
47+
}
48+
}
49+
50+
return array_sum(array_map('array_product', $arr));
51+
}
52+
53+
private function permute(array $charset, $length = null)
54+
{
55+
$n = count($charset);
56+
57+
if (null === $length) {
58+
$length = $n;
59+
}
60+
61+
if ($length > $n) {
62+
return;
63+
}
64+
65+
$indices = range(0, $n - 1);
66+
$cycles = range($n, $n - $length + 1, -1);
67+
68+
yield array_slice($charset, 0, $length);
69+
70+
if ($n <= 0) {
71+
return;
72+
}
73+
74+
while (true) {
75+
$exitEarly = false;
76+
for ($i = $length; $i--;) {
77+
$cycles[$i]-= 1;
78+
if ($cycles[$i] == 0) {
79+
if ($i < count($indices)) {
80+
$removed = array_splice($indices, $i, 1);
81+
$indices[] = $removed[0];
82+
}
83+
$cycles[$i] = $n - $i;
84+
} else {
85+
$j = $cycles[$i];
86+
$value = $indices[$i];
87+
$negative = $indices[count($indices) - $j];
88+
$indices[$i] = $negative;
89+
$indices[count($indices) - $j] = $value;
90+
$result = [];
91+
$counter = 0;
92+
foreach ($indices as $index) {
93+
$result[] = $charset[$index];
94+
$counter++;
95+
if ($counter == $length) {
96+
break;
97+
}
98+
}
99+
yield $result;
100+
$exitEarly = true;
101+
break;
102+
}
103+
}
104+
if (!$exitEarly) {
105+
break;
106+
}
107+
}
108+
}
109+
110+
/**
111+
* @inheritDoc
112+
*/
113+
public function getRandomString()
114+
{
115+
$charset = $this->stringCombinations->charset;
116+
$string = [];
117+
118+
$length = random_int($this->stringCombinations->min, $this->stringCombinations->max);
119+
120+
for ($pos = 1; $pos <= $length; $pos++) {
121+
shuffle($charset);
122+
123+
$string[] = array_shift($charset);
124+
}
125+
126+
return implode($this->stringCombinations->glue, $string);
127+
}
128+
129+
/**
130+
* @inheritDoc
131+
*/
132+
public function asArray()
133+
{
134+
return iterator_to_array($this);
135+
}
136+
}

src/StringCombinations.php

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,16 @@
33
namespace BenTools\StringCombinations;
44

55
use function BenTools\CartesianProduct\cartesian_product;
6+
use Countable;
7+
use IteratorAggregate;
68

7-
final class StringCombinations implements \IteratorAggregate, \Countable
9+
/**
10+
* @property $min
11+
* @property $max
12+
* @property $charset
13+
* @property $glue
14+
*/
15+
final class StringCombinations implements IteratorAggregate, Countable
816
{
917
/**
1018
* @var string[]
@@ -41,7 +49,7 @@ final class StringCombinations implements \IteratorAggregate, \Countable
4149
*/
4250
public function __construct($charset, $min = 1, $max = null, $glue = '')
4351
{
44-
if (is_string($charset) || is_integer($charset)) {
52+
if (is_string($charset) || is_int($charset)) {
4553
$this->charset = preg_split('/(?<!^)(?!$)/u', $charset);
4654
$this->validateCharset($this->charset);
4755
} elseif (is_array($charset)) {
@@ -51,10 +59,19 @@ public function __construct($charset, $min = 1, $max = null, $glue = '')
5159
$this->denyCharset();
5260
}
5361
$this->min = (int) $min;
54-
$this->max = is_null($max) ? count($this->charset) : (int) $max;
62+
$length = count($this->charset);
63+
$this->max = null === $max ? $length : min((int) $max, $this->charset);
5564
$this->glue = $glue;
5665
}
5766

67+
/**
68+
* @return NoDuplicateLettersStringCombinations
69+
*/
70+
public function withoutDuplicates()
71+
{
72+
return new NoDuplicateLettersStringCombinations($this);
73+
}
74+
5875
/**
5976
* @inheritDoc
6077
*/
@@ -86,13 +103,13 @@ public function getIterator()
86103
*/
87104
public function getRandomString()
88105
{
89-
$length = mt_rand($this->min, $this->max);
106+
$length = random_int($this->min, $this->max);
90107
$charset = $this->charset;
91108
for ($pos = 0, $str = []; $pos < $length; $pos++) {
92109
shuffle($charset);
93110
$str[] = $charset[0];
94111
}
95-
return implode('', $str);
112+
return implode($this->glue, $str);
96113
}
97114

98115
/**
@@ -116,7 +133,7 @@ private function generateSets()
116133

117134
private function validateCharset($charset)
118135
{
119-
if (is_null($charset)) {
136+
if (null === $charset) {
120137
$this->denyCharset();
121138
}
122139
foreach ($charset as $value) {
@@ -133,4 +150,12 @@ private function denyCharset()
133150
{
134151
throw new \InvalidArgumentException('Charset should be a string or an array of strings.');
135152
}
153+
154+
/**
155+
* @inheritDoc
156+
*/
157+
public function __get($name)
158+
{
159+
return $this->{$name};
160+
}
136161
}

src/function.php

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)