Php concatenates two or more ArrayIterators by one of their values

I have two iterators that I have to combine into one result.

Here are some sample data:

ArrayIterator Object
(
[storage:ArrayIterator:private] => Array
    (
        [0] => Array
            (
                [period] => 04/04/2012 16:00:00
                [bl_subs] => 1
                [bl_unsubs] => 1
                [bl_block_total] => 1
            )

        [1] => Array
            (
                [period] => 04/04/2012 17:00:00
                [bl_subs] => 1
                [bl_unsubs] => 2
                [bl_block_total] => 0
            )

        [2] => Array
            (
                [period] => 04/04/2012 18:00:00
                [bl_subs] => 0
                [bl_unsubs] => 0
                [bl_block_total] => -1
            )

        [3] => Array
            (
                [period] => 04/04/2012 19:00:00
                [bl_subs] => 2
                [bl_unsubs] => 0
                [bl_block_total] => -2
            )

        [4] => Array
            (
                [period] => 04/04/2012 20:00:00
                [bl_subs] => 2
                [bl_unsubs] => 0
                [bl_block_total] => 1
            )

    )

)


ArrayIterator Object
(
[storage:ArrayIterator:private] => Array
    (
        [0] => Array
            (
                [period] => 04/04/2012 15:00:00
                [bl_avg] => 5
                [bl_full] => 0
            )

        [1] => Array
            (
                [period] => 04/04/2012 17:00:00
                [bl_avg] => 0
                [bl_full] => 7
            )

        [2] => Array
            (
                [period] => 04/04/2012 18:00:00
                [bl_avg] => 1
                [bl_full] => 0
            )

    )

)

      

I would like to combine them by key "period" into a final iterator.

The end result should look like this:

ArrayIterator Object
(
[storage:ArrayIterator:private] => Array
    (
        [0] => Array
            (
                [period] => 04/04/2012 15:00:00
                [bl_subs] => 0
                [bl_unsubs] => 0
                [bl_avg] => 5
                [bl_full] => 0
                [bl_block_total] => 0
            )

        [1] => Array
            (
                [period] => 04/04/2012 16:00:00
                [bl_subs] => 1
                [bl_unsubs] => 1
                [bl_avg] => 0
                [bl_full] => 0
                [bl_block_total] => 1
            )

        [2] => Array
            (
                [period] => 04/04/2012 17:00:00
                [bl_subs] => 1
                [bl_unsubs] => 2
                [bl_avg] => 0
                [bl_full] => 7
                [bl_block_total] => 0
            )

        [3] => Array
            (
                [period] => 04/04/2012 18:00:00
                [bl_subs] => 0
                [bl_unsubs] => 0
                [bl_avg] => 1
                [bl_full] => 0
                [bl_block_total] => -1
            )

        [4] => Array
            (
                [period] => 04/04/2012 19:00:00
                [bl_subs] => 2
                [bl_unsubs] => 0
                [bl_avg] => 0
                [bl_full] => 0
                [bl_block_total] => -2
            )

        [5] => Array
            (
                [period] => 04/04/2012 20:00:00
                [bl_subs] => 2
                [bl_unsubs] => 0
                [bl_avg] => 0
                [bl_full] => 0
                [bl_block_total] => 1
            )

    )

)

      

It's best if we don't use foreach, for, while, or any other loop. This is because the data will be large and we do not want to have memory problems. I tried to use current()

and next()

for using internal array pointers.

If anyone knows a way out of this, please let us know.

+3


source to share


3 answers


Both iterators are always sorted, you can cache both of them, compare on each iteration that comes first (if not equal) and process it. If they are equal, treat them the same.

Not equal:

$it1[[period] => 04/04/2012 16:00:00] > $it2[[period] => 04/04/2012 15:00:00]

=> process $it2 data:

    [period] => 04/04/2012 15:00:00
    [bl_avg] => 5
    [bl_full] => 0

  as current():

    [period] => 04/04/2012 15:00:00
    [bl_subs] => 1
    [bl_unsubs] => 1
    [bl_avg] => 5
    [bl_full] => 0
    [bl_block_total] => 1

+ $it2->next();

      

Note. I have no idea where the items that don't exist in the original data ( $it2[0] (15:00)

) come from [bl_subs => 1]

, [bl_unsubs] => 1

and [bl_block_total] => 1

. Default values?

Equals: (one iteration skipped)

$it1[[period] => 04/04/2012 17:00:00] == $it2[[period] => 04/04/2012 17:00:00]

=> process $it1 and $it2 data:

    $it1:
        [period] => 04/04/2012 17:00:00
        [bl_subs] => 1
        [bl_unsubs] => 2
        [bl_block_total] => 0

    $it2:
        [period] => 04/04/2012 17:00:00
        [bl_avg] => 0
        [bl_full] => 7

  as current():

        [period] => 04/04/2012 17:00:00
        [bl_subs] => 1
        [bl_unsubs] => 2
        [bl_avg] => 0
        [bl_full] => 7
        [bl_block_total] => 0

 + $it1->next(); $it2->next();

      

You can port this processing to Iterator

her own so that it is well encapsulated. Since the information provided was limited, I created a simplified example that reduces the date to the problematic domain: iterating over two iterators at the same time. If both iterators are equal, return both. If not equal, return the first one when both are compared.

Simplified data used:

$ar1 = array('04/04/2012 16:00:00', '04/04/2012 17:00:00', '04/04/2012 18:00:00', '04/04/2012 19:00:00', '04/04/2012 20:00:00');
$ar2 = array('04/04/2012 15:00:00', '04/04/2012 17:00:00', '04/04/2012 18:00:00');

      

Just two arrays that contain the comparison value. They turn into two iterators:

$it1 = new ArrayIterator($ar1);
$it2 = new ArrayIterator($ar2);

      

Released issue is limited to two iterators. To solve the problem more broadly, it must work with 0 or more iterators. So what happens is that on each iteration, the iterators are compared against each other based on their current value. The comparison function is used for this. You can compare this to the way usort

Docs
works : the function compares A and B and returns an integer value based on the two:

  • A <B: -1 (A is less than B, return value is less than zero)
  • A = B: 0 (A equals B, return value equals zero)
  • A> B: 1 (A is greater than B, return value is greater than zero)

This allows an unlimited number of pairs to be compared with each other. It only needs two functions, one that gets the current value from the iterator being used, and the other is the actual comparison between A and B (you can actually combine both into one function, but since this is an example example and your arrays / iterators are slightly different each from a friend, I thought it was worth separating so you can easily change it later). So first, to get the value from the iterator, I compare it to the ISO date values, because I can do it with a simple strcmp

:

/**
 * Get Comparison-Value of an Iterator
 *
 * @param Iterator $iterator
 * @return string
 */
$compareValue = function(Iterator $iterator) {
    $value = $iterator->current();
    sscanf($value, '%d/%d/%d %s', $month, $day, $year, $timeISO);
    $dateISO = sprintf('%04d-%02d-%02d %s', $year, $month, $day, $timeISO);
    return $dateISO;
};

      

Note. I don't know what date format you are using, maybe I mixed month with day, just swapping variables, this is pretty much self-descriptive.



This whole function does this to get a value that can be easily compared to an iterator. This does not yet do the comparison described above, so another function is needed that will use this comparison-value function as a dependency:

/**
 * Compare two Iterators by it value
 *
 * @param Iterator $a
 * @param Iterator $b
 * @return int comparison result (as of strcmp())
 */
$compareFunction = function(Iterator $a, Iterator $b) use ($compareValue) {
    return strcmp($compareValue($a), $compareValue($b));
};

      

And that's now a comparison function based on the string comparison function strcmp

and using the function $compareValue

to get the strings for comparison.

So, let's say you have an array with two iterators, it can now be sorted. It is also possible to compare the first element with the next element to tell if they are equal.

Now you can create an iterator consisting of multiple iterators, and at each iteration, the bound iterators are sorted, and only the first iterator (and those equal to it) will be returned, both current and forward ones. Something like this thread:

DITAA chartSrc

Since the sort is already done with the compare function, only this iterative logic needs to be encapsulated. Since sorting works for an array of any size (0 or more elements), it is already generalized. Usage example:

/**
 * Usage
 */
$it = new MergeCompareIterator($compareFunction, array($it1, $it2));

foreach ($it as $index => $values) {
    printf("Iteration #%d:\n", $index);
    foreach ($values as $iteratorIndex => $value) {
        printf("  * [%d] => %s\n", $iteratorIndex, $value);
    }
}

      

This use case will output the iteration and the corresponding value (s) for that iteration. In this case, only the time information as an array of examples consists of only these. It also puts in square brackets, of which iterators are from (0 for the first, 1 for the second). This generates the following output:

Iteration #0:
  * [1] => 04/04/2012 15:00:00
Iteration #1:
  * [0] => 04/04/2012 16:00:00
Iteration #2:
  * [0] => 04/04/2012 17:00:00
  * [1] => 04/04/2012 17:00:00
Iteration #3:
  * [0] => 04/04/2012 18:00:00
  * [1] => 04/04/2012 18:00:00
Iteration #4:
  * [0] => 04/04/2012 19:00:00
Iteration #5:
  * [0] => 04/04/2012 20:00:00

      

As you can see, for those comparison values ​​that are equal in both (pre-sorted) iterators, they are returned as a pair. In your case, you will need to further process these values, eg. combine them by providing default values:

$defaults = array('bl_subs' => 0, ...);
foreach ($it as $values) {
    array_unshift($values, $default);
    $value = call_user_func_array('array_merge', $values);
}

      

So this is actually a use MergeCompareIterator

. The implementation is pretty straight forward, it doesn't cache the sorter / current iterators yet, I leave that as an exercise if you want to improve it.

Complete code:

<?php
/**
 * @link http://stackoverflow.com/q/10024953/367456
 * @author hakre <http://hakre.wordpress.com/>
 */

$ar1 = array('04/04/2012 16:00:00', '04/04/2012 17:00:00', '04/04/2012 18:00:00', '04/04/2012 19:00:00', '04/04/2012 20:00:00');
$ar2 = array('04/04/2012 15:00:00', '04/04/2012 17:00:00', '04/04/2012 18:00:00');

$it1 = new ArrayIterator($ar1);
$it2 = new ArrayIterator($ar2);

/**
 * Get Comparison-Value of an Iterator
 *
 * @param Iterator $iterator
 * @return string
 */
$compareValue = function(Iterator $iterator)
{
    $value = $iterator->current();
    sscanf($value, '%d/%d/%d %s', $month, $day, $year, $timeISO);
    $dateISO = sprintf('%04d-%02d-%02d %s', $year, $month, $day, $timeISO);
    return $dateISO;
};

/**
 * Compare two Iterators by it value
 *
 * @param Iterator $a
 * @param Iterator $b
 * @return int comparison result (as of strcmp())
 */
$compareFunction = function(Iterator $a, Iterator $b) use ($compareValue)
{
    return strcmp($compareValue($a), $compareValue($b));
};

/**
 * Iterator with a comparison based merge-append strategy over 0 or more iterators.
 *
 * Compares 0 or more iterators with each other. Returns the one that comes first
 * and any additional one that is equal to the first as an array of their current()
 * values in this current().
 * next() forwards all iterators that are part of current().
 */
class MergeCompareIterator implements Iterator
{
    /**
     * @var Iterator[]
     */
    private $iterators;

    /**
     * @var callback
     */
    private $compareFunction;

    /**
     * @var int
     */
    private $index;

    /**
     * @param callback $compareFunction (same sort of usort()/uasort() callback)
     * @param Iterator[] $iterators
     */
    public function __construct($compareFunction, array $iterators = array())
    {
        $this->setCompareFunction($compareFunction);
        foreach ($iterators as $iterator) {
            $this->appendIterator($iterator);
        }
    }

    /**
     * @param callback $compareFunction
     */
    public function setCompareFunction($compareFunction)
    {
        if (!is_callable($compareFunction)) {
            throw new InvalidArgumentException('Compare function is not callable.');
        }
        $this->compareFunction = $compareFunction;
    }

    public function appendIterator(Iterator $it)
    {
        $this->iterators[] = $it;
    }

    public function rewind()
    {
        foreach ($this->iterators as $it) {
            $it->rewind();
        }
        $this->index = 0;
    }

    /**
     * @return Array one or more current values
     * @throws RuntimeException
     */
    public function current()
    {
        $current = array();
        foreach ($this->getCurrentIterators() as $key => $value) {
            $current[$key] = $value->current();
        }
        return $current;
    }

    /**
     * @return Iterator[]
     */
    private function getCurrentIterators()
    {
        /* @var $compareFunction Callable */
        $compareFunction = $this->compareFunction;

        $iterators = $this->getValidIterators();
        $r = uasort($iterators, $compareFunction);
        if (FALSE === $r) {
            throw new RuntimeException('Sorting failed.');
        }

        $compareAgainst = reset($iterators);
        $sameIterators = array();
        foreach ($iterators as $key => $iterator) {
            $comparison = $compareFunction($iterator, $compareAgainst);
            if (0 !== $comparison) {
                break;
            }
            $sameIterators[$key] = $iterator;
        }
        ksort($sameIterators);
        return $sameIterators;
    }

    /**
     * @return Iterator[]
     */
    private function getValidIterators()
    {
        $validIterators = array();
        foreach ($this->iterators as $key => $iterator) {
            $iterator->valid() && $validIterators[$key] = $iterator;
        }
        return $validIterators;
    }

    /**
     * @return int zero based iteration count
     */
    public function key()
    {
        return $this->index;
    }

    public function next()
    {
        foreach ($this->getCurrentIterators() as $iterator) {
            $iterator->next();
        }
        $this->index++;
    }

    public function valid()
    {
        return (bool)count($this->getValidIterators());
    }
}

/**
 * Usage
 */
$it = new MergeCompareIterator($compareFunction, array($it1, $it2));

foreach ($it as $index => $values) {
    printf("Iteration #%d:\n", $index);
    foreach ($values as $iteratorIndex => $value) {
        printf("  * [%d] => %s\n", $iteratorIndex, $value);
    }
}

      

Hope this is helpful. It only works with preconfigured data inside your "internal" iterators, otherwise a merge / append strategy with matching current elements makes no sense.

+7


source


Ok, this is pretty simple. Assuming both iterators are sorted, all you have to do is do merge sort (mostly):

function mergeIterators(Iterator $it1, Iterator $it2, $compare, $merge) {
    $result = array();
    //rewind both itertators
    $it1->rewind();
    $it2->rewind();
    while ($it1->valid() || $it2->valid()) {
        if (!$it1->valid()) {
            $cmp = 1;
        } elseif (!$it2->valid()) {
            $cmp = -1;
        } else {
            $cmp = $compare($it1->current(), $it2->current());
        }

        if ($cmp === 0) {
            // equal, merge together
            $result[] = $merge($it1->current(), $it2->current());
            $it1->next();
            $it2->next();
        } elseif ($cmp < 0) {
            //first is less than second
            $result[] = $it1->current();
            $it1->next();
        } else {
            $result[] = $it2->current();
            $it2->next();
        }
    }
    return $result;
}

      

The only trick here is to pass the correct function $compare

and function $merge

...

Here's an example:



$compare = function(array $val1, array $val2) {
    return strtotime($val1['period']) - strtotime($val2['period']);
};

$merge = function(array $val1, array $val2) {
    return array_merge($val1, $val2);
};

      

The merge function may need to be changed as it only does a rough merge where you can do something more complex (like add keys together, etc.) ...

But this function is versatile enough to be able to be used to merge any 2 iterators, not just your use case ...

+1


source


What you can use AppendIterator

see http://php.net/manual/en/class.appenditerator.php for more documentation.

Example: http://php.net/manual/en/function.iterator-to-array.php

$first = new ArrayIterator ( array (
        'k1' => 'a',
        'k2' => 'b',
        'k3' => 'c',
        'k4' => 'd' 
) );
$second = new ArrayIterator ( array (
        'k1' => 'X',
        'k2' => 'Y',
        'Z' 
) );

$combinedIterator = new AppendIterator ();
$combinedIterator->append ( $first );
$combinedIterator->append ( $second );

var_dump ( iterator_to_array ( $combinedIterator, false ) );

      

thank

:)

0


source







All Articles