Skip to content

Commit

Permalink
Add AddressInterface::add() (#95)
Browse files Browse the repository at this point in the history
  • Loading branch information
mlocati authored Feb 4, 2025
1 parent c99d8ad commit ed5e3c4
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 1 deletion.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ require_once 'path/to/iplib/ip-lib.php';

### Installation with Composer

Simply run
Simply run

```sh
composer require mlocati/ip-lib
Expand Down Expand Up @@ -101,6 +101,17 @@ echo (string) $address->shift(-1);
echo (string) $address->shift(-16);
```

### Adding two IP addresses

You can calculate the sum of 2 IP addresses using the `add` method:

```php
$a = \IPLib\Factory::parseAddressString('1.2.3.4');
$b = \IPLib\Factory::parseAddressString('10.0.0.0');
// This will print 11.2.3.4
echo (string) $a->add($b);
```

### Get the addresses at a specified offset

For addresses:
Expand Down
12 changes: 12 additions & 0 deletions src/Address/AddressInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,16 @@ public function getReverseDNSLookupName();
* @example shifting by -1 127.0.0.1 you'll have 254.0.0.2
*/
public function shift($bits);

/**
* Create a new IP address by adding to this address another address.
*
* @return self|null returns NULL if $other is not compatible with this address, or if it generates an invalid address
*
* @since 1.20.0
*
* @example adding 0.0.0.10 to 127.0.0.1 generates the IP 127.0.0.11
* @example adding 255.0.0.10 to 127.0.0.1 generates NULL
*/
public function add(AddressInterface $other);
}
31 changes: 31 additions & 0 deletions src/Address/IPv4.php
Original file line number Diff line number Diff line change
Expand Up @@ -539,4 +539,35 @@ public function shift($bits)

return new static(implode('.', $bytes));
}

/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::add()
*/
public function add(AddressInterface $other)
{
if (!$other instanceof self) {
return null;
}
$myBytes = $this->getBytes();
$otherBytes = $other->getBytes();
$sum = array_fill(0, 4, 0);
$carry = 0;
for ($index = 3; $index >= 0; $index--) {
$byte = $myBytes[$index] + $otherBytes[$index] + $carry;
if ($byte > 0xFF) {
$carry = $byte >> 8;
$byte &= 0xFF;
} else {
$carry = 0;
}
$sum[$index] = $byte;
}
if ($carry !== 0) {
return null;
}

return new static(implode('.', $sum));
}
}
31 changes: 31 additions & 0 deletions src/Address/IPv6.php
Original file line number Diff line number Diff line change
Expand Up @@ -632,4 +632,35 @@ public function shift($bits)

return static::fromWords($bytes);
}

/**
* {@inheritdoc}
*
* @see \IPLib\Address\AddressInterface::add()
*/
public function add(AddressInterface $other)
{
if (!$other instanceof self) {
return null;
}
$myWords = $this->getWords();
$otherWords = $other->getWords();
$sum = array_fill(0, 7, 0);
$carry = 0;
for ($index = 7; $index >= 0; $index--) {
$word = $myWords[$index] + $otherWords[$index] + $carry;
if ($word > 0xFFFF) {
$carry = $word >> 16;
$word &= 0xFFFF;
} else {
$carry = 0;
}
$sum[$index] = $word;
}
if ($carry !== 0) {
return null;
}

return static::fromWords($sum);
}
}
64 changes: 64 additions & 0 deletions test/tests/Addresses/AddTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

namespace IPLib\Test\Addresses;

use IPLib\Factory;
use IPLib\Test\TestCase;

class AddTest extends TestCase
{
public function provideCases()
{
return array(
array('0.0.0.0', '::', null),
array('0.0.0.0', '0.0.0.0', '0.0.0.0'),
array('0.0.0.1', '0.0.0.1', '0.0.0.2'),
array('0.0.1.0', '0.0.1.0', '0.0.2.0'),
array('0.0.0.255', '0.0.0.1', '0.0.1.0'),
array('0.0.0.255', '0.0.0.255', '0.0.1.254'),
array('1.2.3.4', '10.0.0.0', '11.2.3.4'),
array('127.2.3.4', '127.255.0.0', '255.1.3.4'),
array('127.3.3.4', '127.255.0.0', '255.2.3.4'),
array('127.127.3.4', '128.255.0.0', null),
array('255.255.255.254', '0.0.0.1', '255.255.255.255'),
array('255.255.255.255', '0.0.0.1', null),
array('255.255.255.255', '255.255.255.255', null),

array('::', '127.0.0.1', null),
array('::', '::', '::'),
array('::1', '::', '::1'),
array('::1', '::1', '::2'),
array('::1', '::1:0', '::1:1'),
array('::1', '::10', '::11'),
array('::a', '::a', '::14'),
array('::fffe', '::1', '::ffff'),
array('::ffff', '::1', '::1:0'),
array('::ffff', '::ffff', '::1:fffe'),
array('ffff::', '::1', 'ffff::1'),
array('fffe::', '1::', 'ffff::'),
array('ffff::', '1::', null),
);
}

/**
* @dataProvider provideCases
*
* @param string $addressA
* @param string $addressB
* @param string|null $expectedSum
*/
public function testAdd($addressA, $addressB, $expectedSum)
{
$ipA = Factory::parseAddressString($addressA);
$this->assertNotNull($ipA, "'{$addressA}' has been detected as an invalid IP, but it should be valid");
$ipB = Factory::parseAddressString($addressB);
$this->assertNotNull($ipB, "'{$addressB}' has been detected as an invalid IP, but it should be valid");
if ($expectedSum === null) {
$this->assertNull($ipA->add($ipB));
$this->assertNull($ipB->add($ipA));
} else {
$this->assertSame($expectedSum, (string) $ipA->add($ipB));
$this->assertSame($expectedSum, (string) $ipB->add($ipA));
}
}
}

0 comments on commit ed5e3c4

Please sign in to comment.