Skip to content

Commit

Permalink
Rearrange equal items for non-homogeneous arrays, fixes #33
Browse files Browse the repository at this point in the history
  • Loading branch information
vearutop committed Sep 24, 2020
1 parent ab1ce3a commit 4159378
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 15 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.7.6] - 2020-09-25

### Added
- Rearrangement of equal items for non-homogeneous arrays with `JsonDiff::REARRANGE_ARRAYS` option.

## [3.7.5] - 2020-05-26

### Fixed
Expand Down Expand Up @@ -45,6 +50,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Compatibility option to `TOLERATE_ASSOCIATIVE_ARRAYS` that mimic JSON objects.

[3.7.6]: https://github.com/swaggest/json-diff/compare/v3.7.5...v3.7.6
[3.7.5]: https://github.com/swaggest/json-diff/compare/v3.7.4...v3.7.5
[3.7.4]: https://github.com/swaggest/json-diff/compare/v3.7.3...v3.7.4
[3.7.3]: https://github.com/swaggest/json-diff/compare/v3.7.2...v3.7.3
Expand Down
56 changes: 41 additions & 15 deletions src/JsonDiff.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ class JsonDiff


private $options = 0;
private $original;
private $new;

/**
* @var mixed Merge patch container
Expand Down Expand Up @@ -80,6 +78,12 @@ class JsonDiff
/** @var JsonPatch */
private $jsonPatch;

/** @var JsonHash */
private $jsonHashOriginal;

/** @var JsonHash */
private $jsonHashNew;

/**
* @param mixed $original
* @param mixed $new
Expand All @@ -92,15 +96,13 @@ public function __construct($original, $new, $options = 0)
$this->jsonPatch = new JsonPatch();
}

$this->original = $original;
$this->new = $new;
$this->options = $options;

if ($options & self::JSON_URI_FRAGMENT_ID) {
$this->path = '#';
}

$this->rearranged = $this->rearrange();
$this->rearranged = $this->process($original, $new);
if (($new !== null) && $this->merge === null) {
$this->merge = new \stdClass();
}
Expand Down Expand Up @@ -241,14 +243,6 @@ public function getMergePatch()

}

/**
* @return array|null|object|\stdClass
* @throws Exception
*/
private function rearrange()
{
return $this->process($this->original, $this->new);
}

/**
* @param mixed $original
Expand Down Expand Up @@ -406,7 +400,7 @@ private function rearrangeArray(array $original, array $new)
{
$first = reset($original);
if (!$first instanceof \stdClass) {
return $new;
return $this->rearrangeEqualItems($original, $new);
}

$uniqueKey = false;
Expand Down Expand Up @@ -450,7 +444,7 @@ private function rearrangeArray(array $original, array $new)
}

if (!$uniqueKey) {
return $new;
return $this->rearrangeEqualItems($original, $new);
}

$newRearranged = [];
Expand Down Expand Up @@ -499,4 +493,36 @@ private function rearrangeArray(array $original, array $new)
$newRearranged = array_values($newRearranged);
return $newRearranged;
}

private function rearrangeEqualItems(array $original, array $new)
{
if ($this->jsonHashOriginal === null) {
$this->jsonHashOriginal = new JsonHash($this->options);
$this->jsonHashNew = new JsonHash($this->options);
}

$origIdx = [];
foreach ($original as $i => $item) {
$origIdx[$i] = $this->jsonHashOriginal->xorHash($item);
}

$newIdx = [];
foreach ($new as $i => $item) {
$hash = $this->jsonHashNew->xorHash($item);
$newIdx[$hash][] = $i;
}

$rearranged = $new;
foreach ($origIdx as $i => $hash) {
if (empty($newIdx[$hash])) {
continue;
}

$j = array_shift($newIdx[$hash]);
$rearranged[$i] = $new[$j];
$rearranged[$j] = $new[$i];
}

return $rearranged;
}
}
76 changes: 76 additions & 0 deletions src/JsonHash.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace Swaggest\JsonDiff;

class JsonHash
{
private $options = 0;

public function __construct($options = 0)
{
$this->options = $options;
}

/**
* @param mixed $data
* @param string $path
* @return string
*/
public function xorHash($data, $path = '')
{
$xorHash = '';

if (!$data instanceof \stdClass && !is_array($data)) {
$s = $path . (string)$data;
if (strlen($xorHash) < strlen($s)) {
$xorHash = str_pad($xorHash, strlen($s));
}
$xorHash ^= $s;

return $xorHash;
}

if ($this->options & JsonDiff::TOLERATE_ASSOCIATIVE_ARRAYS) {
if (is_array($data) && !empty($data) && !array_key_exists(0, $data)) {
$data = (object)$data;
}
}

if (is_array($data)) {
if ($this->options & JsonDiff::REARRANGE_ARRAYS) {
foreach ($data as $key => $item) {
$itemPath = $path . '/' . $key;
$itemHash = $path . $this->xorHash($item, $itemPath);
if (strlen($xorHash) < strlen($itemHash)) {
$xorHash = str_pad($xorHash, strlen($itemHash));
}
$xorHash ^= $itemHash;
}
} else {
foreach ($data as $key => $item) {
$itemPath = $path . '/' . $key;
$itemHash = md5($itemPath . $this->xorHash($item, $itemPath), true);
if (strlen($xorHash) < strlen($itemHash)) {
$xorHash = str_pad($xorHash, strlen($itemHash));
}
$xorHash ^= $itemHash;
}
}

return $xorHash;
}

$dataKeys = get_object_vars($data);
foreach ($dataKeys as $key => $value) {
$propertyPath = $path . '/' .
JsonPointer::escapeSegment($key, (bool)($this->options & JsonDiff::JSON_URI_FRAGMENT_ID));
$propertyHash = $propertyPath . $this->xorHash($value, $propertyPath);
if (strlen($xorHash) < strlen($propertyHash)) {
$xorHash = str_pad($xorHash, strlen($propertyHash));
}
$xorHash ^= $propertyHash;
}

return $xorHash;
}
}
38 changes: 38 additions & 0 deletions tests/src/JsonHashTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Swaggest\JsonDiff\Tests;

use Swaggest\JsonDiff\JsonDiff;
use Swaggest\JsonDiff\JsonHash;

class JsonHashTest extends \PHPUnit_Framework_TestCase
{
public function testHash()
{
$h1 = (new JsonHash())->xorHash(json_decode('{"data": [{"A": 1},{"B": 2}]}'));
$h2 = (new JsonHash())->xorHash(json_decode('{"data": [{"B": 2},{"A": 1}]}'));
$h3 = (new JsonHash())->xorHash(json_decode('{"data": [{"B": 3},{"A": 2}]}'));

$this->assertNotEmpty($h1);
$this->assertNotEmpty($h2);
$this->assertNotEmpty($h3);
$this->assertNotEquals($h1, $h2);
$this->assertNotEquals($h1, $h3);
}

public function testHashRearrange()
{
$h1 = (new JsonHash(JsonDiff::REARRANGE_ARRAYS))
->xorHash(json_decode('{"data": [{"A": 1},{"B": 2}]}'));
$h2 = (new JsonHash(JsonDiff::REARRANGE_ARRAYS))
->xorHash(json_decode('{"data": [{"B": 2},{"A": 1}]}'));
$h3 = (new JsonHash(JsonDiff::REARRANGE_ARRAYS))
->xorHash(json_decode('{"data": [{"B": 3},{"A": 2}]}'));

$this->assertNotEmpty($h1);
$this->assertNotEmpty($h2);
$this->assertNotEmpty($h3);
$this->assertEquals($h1, $h2);
$this->assertNotEquals($h1, $h3);
}
}
21 changes: 21 additions & 0 deletions tests/src/RearrangeArrayTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,25 @@ function testRearrangeKeepOriginal()
json_encode($m->getRearranged(), JSON_PRETTY_PRINT)
);
}

public function testEqualItems()
{
$diff = new \Swaggest\JsonDiff\JsonDiff(
json_decode('{"data": [{"A": 1, "C": [1,2,3]},{"B": 2}]}'),
json_decode('{"data": [{"B": 2},{"A": 1, "C": [3,2,1]}]}'),
JsonDiff::REARRANGE_ARRAYS);

$this->assertEmpty($diff->getDiffCnt());
}

public function testEqualItemsDiff()
{
$diff = new \Swaggest\JsonDiff\JsonDiff(
json_decode('{"data": [{"A": 1, "C": [1,2,3,4]},{"B": 2}]}'),
json_decode('{"data": [{"B": 2},{"A": 1, "C": [5,3,2,1]}]}'),
JsonDiff::REARRANGE_ARRAYS);

$this->assertEquals('[{"value":4,"op":"test","path":"/data/0/C/3"},{"value":5,"op":"replace","path":"/data/0/C/3"}]',
json_encode($diff->getPatch(), JSON_UNESCAPED_SLASHES));
}
}

0 comments on commit 4159378

Please sign in to comment.