Skip to content

Commit dead2a4

Browse files
authored
Add Factory::getRangeFromAddresses() (#109)
1 parent 08d5c66 commit dead2a4

3 files changed

Lines changed: 151 additions & 1 deletion

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,21 @@ $ranges = \IPLib\Factory::getRangesFromBoundaries('192.168.0.0', '192.168.0.5');
235235
echo implode(' ', $ranges);
236236
```
237237

238+
### Retrieve a range that contains a set of IP addresses
239+
240+
You can use `IPLib\Factory::getRangeFromAddresses()` to retrieve the minimal IP range that contains all the provided IP addresses:
241+
242+
```php
243+
$range = \IPLib\Factory::getRangeFromAddresses(array(
244+
'1.2.2.225',
245+
'1.2.1.124',
246+
'1.2.3.237',
247+
));
248+
249+
// This will print 1.2.0.0/22
250+
echo (string) $range;
251+
```
252+
238253
### Retrieve the boundaries of a range
239254

240255
```php

src/Factory.php

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,53 @@ public static function getRangeFromBoundaries($from, $to, $flags = 0)
158158
return $from === false || $to === false ? null : static::rangeFromBoundaryAddresses($from, $to);
159159
}
160160

161+
/**
162+
* Calculate the minimal range that contains all the specified addresses.
163+
*
164+
* @param array<non-empty-string|\IPLib\Address\AddressInterface|mixed> $addresses
165+
* @param int $flags
166+
*
167+
* @return \IPLib\Range\RangeInterface|null Returns NULL if $addresses is empty, if it contains invalid addresses, or if the addresses aren't compatible (for example, both IPv4 and IPv6 addresses)
168+
*
169+
* @since 1.22.0
170+
*/
171+
public static function getRangeFromAddresses(array $addresses, $flags = 0)
172+
{
173+
$min = null;
174+
$max = null;
175+
$numberOfBits = null;
176+
foreach ($addresses as $address) {
177+
if (!$address instanceof AddressInterface) {
178+
$address = Factory::parseAddressString($address, $flags);
179+
if ($address === null) {
180+
return null;
181+
}
182+
}
183+
if ($numberOfBits === null) {
184+
$min = $address;
185+
$max = $address;
186+
$numberOfBits = $address->getNumberOfBits();
187+
} elseif ($numberOfBits !== $address->getNumberOfBits()) {
188+
return null;
189+
} else {
190+
/** @var AddressInterface $min */
191+
/** @var AddressInterface $max */
192+
$comparable = $address->getComparableString();
193+
if ($min->getComparableString() > $comparable) {
194+
$min = $address;
195+
}
196+
if ($max->getComparableString() < $comparable) {
197+
$max = $address;
198+
}
199+
}
200+
}
201+
if ($numberOfBits === null) {
202+
return null;
203+
}
204+
205+
return static::rangeFromBoundaryAddresses($min, $max);
206+
}
207+
161208
/**
162209
* @deprecated since 1.17.0: use the getRangesFromBoundaries() method instead.
163210
* For upgrading:
@@ -265,7 +312,7 @@ protected static function rangeFromBoundaryAddresses($from = null, $to = null)
265312
* @param string|\IPLib\Address\AddressInterface|mixed $to
266313
* @param int $flags
267314
*
268-
* @return \IPLib\Address\AddressInterface[]|null[]|false[]
315+
* @return array{\IPLib\Address\AddressInterface|false|null, \IPLib\Address\AddressInterface|false|null}
269316
*/
270317
private static function parseBoundaries($from, $to, $flags = 0)
271318
{
@@ -285,6 +332,7 @@ private static function parseBoundaries($from, $to, $flags = 0)
285332
}
286333
$result[] = $value;
287334
}
335+
/** @var array{\IPLib\Address\AddressInterface|false|null, \IPLib\Address\AddressInterface|false|null} $result */
288336
if ($result[0] && $result[1] && strcmp($result[0]->getComparableString(), $result[1]->getComparableString()) > 0) {
289337
$result = array($result[1], $result[0]);
290338
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace IPLib\Test\Ranges;
4+
5+
use IPLib\Factory;
6+
use IPLib\ParseStringFlag;
7+
use IPLib\Test\TestCase;
8+
9+
class RangeFromAddressesTest extends TestCase
10+
{
11+
/**
12+
* @return array{mixed}[]
13+
*/
14+
public function invalidProvider()
15+
{
16+
return array(
17+
array(array()),
18+
array(array(null)),
19+
array(array(false)),
20+
array(array('')),
21+
array(array('1.2.3.4.5')),
22+
array(array('127.0.0.1', '::1')),
23+
array(array(Factory::parseAddressString('127.0.0.1'), '::1')),
24+
);
25+
}
26+
27+
/**
28+
* @dataProvider invalidProvider
29+
*
30+
* @param array{mixed} $addresses
31+
*
32+
* @return void
33+
*/
34+
public function testInvalid(array $addresses)
35+
{
36+
$this->assertNull(Factory::getRangeFromAddresses($addresses));
37+
}
38+
39+
/**
40+
* @return array{array{mixed}, string, 2?: int}[]
41+
*/
42+
public function validProvider()
43+
{
44+
return array(
45+
array(array('1.2.3.4'), '1.2.3.4'),
46+
array(array('1.2.3.4', Factory::parseAddressString('1.2.3.4')), '1.2.3.4'),
47+
array(array('0.0.0.0'), '0.0.0.0'),
48+
array(array('255.255.255.255'), '255.255.255.255'),
49+
array(array('0.0.0.0', '255.255.255.255'), '0.0.0.0/0'),
50+
array(array('0.0.0.0', '127.255.255.255'), '0.0.0.0/1'),
51+
array(array('128.0.0.0', '255.255.255.255'), '128.0.0.0/1'),
52+
array(array('255.255.255.255', '128.0.0.0', '128.0.0.0'), '128.0.0.0/1'),
53+
array(array(Factory::parseAddressString('::')), '::'),
54+
array(array(Factory::parseAddressString('::')), '::'),
55+
array(
56+
array(
57+
'131.100.75.124',
58+
'131.100.72.225',
59+
'131.100.72.237',
60+
'131.100.72.96',
61+
'131.100.73.125',
62+
'131.100.73.139',
63+
'131.100.74.24',
64+
'131.100.74.243:80',
65+
),
66+
'131.100.72.0/22',
67+
ParseStringFlag::MAY_INCLUDE_PORT,
68+
),
69+
);
70+
}
71+
72+
/**
73+
* @dataProvider validProvider
74+
*
75+
* @param non-empty-array{non-empty-string|\IPLib\Address\AddressInterface} $addresses
76+
* @param non-empty-string $expectedSubnetStringRepresentation
77+
* @param int $flags
78+
*
79+
* @return void
80+
*/
81+
public function testValid(array $addresses, $expectedSubnetStringRepresentation, $flags = 0)
82+
{
83+
$subnet = Factory::getRangeFromAddresses($addresses, $flags);
84+
$this->assertNotNull($subnet);
85+
$this->assertSame($expectedSubnetStringRepresentation, (string) $subnet);
86+
}
87+
}

0 commit comments

Comments
 (0)