Utility classes

2019
/**
 * Wrapper for any array to allow OOP methods on them
 *
 * @author Stanley Lambot
 * @package Syltaen
 */

class Set extends \ArrayObject implements \JsonSerializable
{
    // ==================================================
    // > FINDING ITEMS
    // ==================================================
    /**
     * Implementation of the "array_search" function
     *
     * @param  mixed $item
     * @return mixed Key
     */
    public function search($item)
    {
        return array_search($item, (array) $this);
    }

    /**
     * Find an array|object item by its property.
     * Find the first match's key.
     * @param  string $key
     * @param  mixed $value
     * @return mixed
     */
    public function searchBy($key, $value)
    {
        foreach ($this as $i => $item) {
            if (static::itemMatchBy($item, $key, $value)) {
                return $i;
            }

        }
        return false;
    }

    /**
     * Find first number in the set that is over (or equals to) a given number and return its key
     *
     * @param  $num
     * @param  $strict
     * @return mixed The key of the item
     */
    public function searchNumberOver($num, $strict = false)
    {
        $num = Scalar::int($num);
        foreach ($this as $i => $item) {
            $item = Scalar::int($item);
            if (($strict && $item > $num) || (!$strict && $item >= $num)) {
                return $i;
            }
        }
        return false;
    }

    /**
     * Find first number in the set that is under (or equals to) a given number and return its key
     *
     * @param  $num
     * @param  $strict
     * @return mixed The key of the item
     */
    public function searchNumberUnder($num, $strict = false)
    {
        $num = Scalar::int($num);
        foreach ($this as $i => $item) {
            $item = Scalar::int($item);
            if (($strict && $item < $num) || (!$strict && $item <= $num)) {
                return $i;
            }
        }
        return false;
    }

    /**
     * Find an array|object item by its property.
     * Return the first match.
     *
     * @param  string         $key
     * @param  mixed          $value
     * @return array|object
     */
    public function getBy($key, $value)
    {
        $found = $this->searchBy($key, $value);
        if ($found !== false) {
            return $this[$found];
        }

        return false;
    }

    /**
     * Return the last item in the set
     *
     * @return mixed
     */
    public function first()
    {
        $array = $this->getArrayCopy();
        return current($array);
    }

    /**
     * Return the last item in the set
     * or the nth item from the end
     *
     * @return mixed
     */
    public function last($nth_from_end = false)
    {
        if ($nth_from_end) {
            return $this[$this->count() - $nth_from_end];
        }

        $array = $this->getArrayCopy();
        return end($array);
    }

    /**
     * Return a random item of the set
     *
     * @return mixed
     */
    public function getRandom()
    {
        $array = $this->getArrayCopy();
        return $array[array_rand($array)];
    }

    /**
     * Find the item that has the most occurrences
     *
     * @return mixed
     */
    public function mostCommon()
    {
        $occurrences = $this->occurrences();
        return $occurrences->flip()[$occurrences->max()];
    }

    /**
     * Find the item that has the most occurrences
     *
     * @return mixed
     */
    public function leastCommon()
    {
        $occurrences = $this->occurrences();
        return $occurrences->flip()[$occurrences->min()];
    }

    // ==================================================
    // > SORTING ITEMS
    // ==================================================
    /**
     * Shortcut for uasort and asort
     *
     * @param  mixed  $callback Sort automatically or with a callback function
     * @return self
     */
    public function sort($callback = false)
    {
        if ($callback) {
            $this->uasort($callback);
        } else {
            $this->asort();
        }
        return $this;
    }

    /**
     * Sort the elements of the set by a given property
     *
     * @return self
     */
    public function sortBy($key)
    {
        $this->uasort(function ($a, $b) use ($key) {
            return ((array) $a)[$key] > ((array) $b)[$key] ? 1 : -1;
        });
        return $this;
    }

    /**
     * Sort keys of the set
     *
     * @return self
     */
    public function sortKeys()
    {
        $this->ksort();
        return $this;
    }

    /**
     * Implementation of the "array_reverse" function
     *
     * @return Set
     */
    public function reverse()
    {
        return new static(array_reverse((array) $this));
    }

    // ==================================================
    // > ADDING ITEMS
    // ==================================================
    /**
     * Insert new elements in the list at a specific position
     *
     * @return self
     */
    public function insert($array, $position = null)
    {
        // Get the numerical index where the set should be split
        $index = is_null($position) ? $this->count() : (
            is_int($position) ? $position
            // If a string/key is given : try to get its position
                : (($index = $this->keys()->search($position)) !== false ? $index + 1
                // Default to the end of the set
                    : $this->count())
        );

        $this->exchangeArray(array_merge(
            array_slice((array) $this, 0, $index, true),
            (array) $array,
            array_slice((array) $this, $index, null, true)
        ));

        return $this;
    }

    /**
     * Repeat the set a given number of times
     *
     * @param int $times
     * @return Set
     */
    public function repeat($times)
    {
        $array = array_fill(0, Scalar::int($times), (array) $this);
        return new static(array_merge(...$array));
    }

    /**
     * Implement the "array_splice" function
     *
     * @param int $offset
     * @param int $length
     * @param array|Set $replacement
     * @return Set
     */
    public function splice($offset, $length = null, $replacement = [])
    {
        $array = (array) $this;
        $return = array_splice($array, $offset, $length, (array) $replacement);
        $this->exchangeArray($array);
        return new static($return);
    }

    /**
     * Remove and return the first item of the set
     *
     * @return mixed
     */
    public function shift()
    {
        $array = (array) $this;
        $item = array_shift($array);
        $this->exchangeArray($array);
        return $item;
    }

    /**
     * Insert a new item at the beginning of the array
     *
     * @return self
     */
    public function unshift($item)
    {
        $this->insert([$item], 0);
        return $this;
    }

    /**
     * Remove and return the last item of the set
     *
     * @return mixed
     */
    public function pop()
    {
        $array = (array) $this;
        $item = array_pop($array);
        $this->exchangeArray($array);
        return $item;
    }

    /**
     * Add a new item at the end of the array
     *
     * @param  mixed  $item
     * @return self
     */
    public function push($item)
    {
        $this->append($item);
        return $this;
    }

    /**
     * Merge the set with another or with itself
     *
     * @param  mixed  $item
     * @return self
     */
    public function merge($items = false)
    {
        if ($items) {
            return new static(array_merge(
                (array) $this,
                (array) $items
            ));
        }

        // No items to merge with, try to merge all the set's children
        return $this->reduce(function ($set, $row) {
            return $set->merge($row);
        }, new static );
    }

    /**
     * Fill the array with a value
     * Implementation of array_fill
     *
     * @param  int $start_index
     * @param  int $count
     * @param  mixed $value
     * @return Set
     */
    public function fill($start_index, $count, $value)
    {
        // Classic implementation : fill with the same value
        if (!is_callable($value)) {
            return new static(array_fill($start_index, $count, $value));
        }

        // Callable : allow to fill with a callback function that is called for each item
        $array = [];
        for ($i = $start_index; $i < $start_index + $count; $i++) {
            $array[$i] = $value($i);
        }

        return $this->insert($array, $start_index);
    }

    // ==================================================
    // > REMOVING/FILTERING ITEMS
    // ==================================================
    /**
     * Implementation of the "array_filter" function
     *
     * @param  callable $callback
     * @return Set
     */
    public function filter($callback = "\Scalar::isNotEmpty")
    {
        return new static(array_filter((array) $this, $callback, ARRAY_FILTER_USE_BOTH));
    }

    /**
     * Filter allarray|object item by their properties.
     * Return only the one that match a speicifc key=>value
     *
     * @param  string $key
     * @param  mixed  $value
     * @return Set
     */
    public function filterBy($key, $value)
    {
        return $this->filter(function ($item) use ($key, $value) {
            return static::itemMatchBy($item, $key, $value);
        });
    }

    /**
     * Remove an array|object item by its property.
     * Remove all
     *
     * @param  string         $key
     * @param  mixed          $value
     * @return array|object
     */
    public function removeBy($key, $value)
    {
        foreach ($this as $i => $item) {
            if (static::itemMatchBy($item, $key, $value)) {
                unset($this[$i]);
            }
        }
        return $this;
    }

    /**
     * Unset all the keys
     *
     * @return self
     */
    public function clear()
    {
        foreach ($this->keys() as $key) {
            unset($this[$key]);
        }
        return $this;
    }

    /**
     * Implementation of the "array_unique" function
     *
     * @param  callable $callback
     * @return Set
     */
    public function unique($preserve_keys = false, $flags = SORT_STRING)
    {
        $array = array_unique((array) $this, $flags);
        if (!$preserve_keys) {
            $array = array_values($array);
        }

        return new static($array);
    }

    /**
     * Get all items that apear more than once
     * @param  int   $limit Minimum number of occurrences
     * @return Set
     */
    public function duplicates($limit = 2)
    {
        return $this->groupBy(function ($item) {
            return $item;
        })->filter(function ($item) use ($limit) {
            return count($item) >= $limit;
        })->keys();
    }

    /**
     * Get the number of occurrences of each unique item
     *
     * @return Set
     */
    public function occurrences()
    {
        return $this->groupBy(function ($item) {
            return $item->value ?? $item;
        })->mapAssoc(function ($item, $group) {
            return [$item => count($group)];
        });
    }

    /**
     * Find a pattern in a repeating set using chunks
     *
     * @return array Pattern, items before, items after
     */
    public function pattern($minimum_size = 3, $minimum_repeats = 3)
    {
        $length = $this->count();

        $items = $this->reverse()->getArrayCopy();
        $l = $minimum_size;

        while (true) {
            $chunks = array_chunk($items, $l);
            if (count($chunks) < $minimum_repeats) return false;

            if (count(array_unique(array_map(fn ($c) => implode(";", $c), array_slice($chunks, 0, $minimum_repeats)))) === 1) {
                return array_reverse($chunks[0]);
            }

            $l++;
            if ($l == $length) break;
        }
        return false;
    }

    /**
     * Find a pattern in a repeating set using regex
     *
     * @return array Pattern, items before, items after
     */
    public function pattern2($join = ";")
    {
        $string = $pattern = (string) $this->join($join);

        preg_match('/(.+)\1+$/', $string, $match);
        while (isset($match[1])) {
            $pattern = $match[1];
            preg_match('/^(.+)\1+$/', $pattern, $match);
        }

        preg_match("/^(.*?)($pattern)+(.*?)$/", $string, $parts);

        $p = explode($join, trim($parts[2], $join));

        return [
            explode($join, trim($parts[2], $join)),
            explode($join, trim($parts[1], $join)),
            explode($join, trim($parts[3], $join)),
        ];
    }

    /**
     * Implementation of the "array_diff" function
     *
     * @param  array $array
     * @return Set
     */
    public function remove($array, $preserve_keys = false)
    {
        $array = array_diff((array) $this, (array) $array);
        if (!$preserve_keys) {
            $array = array_values($array);
        }

        return new static($array);
    }

    /**
     * Implementation of the "array_diff" function
     *
     * @param  array $array
     * @return Set
     */
    public function keep($array)
    {
        $array = array_intersect((array) $this, (array) $array);
        return new static($array);
    }

    /**
     * Campare both keys and value and return the difference
     *
     * @return Set
     */
    public function fullDiff($array)
    {
        return $this->filter(function ($value, $key) use ($array) {
            if (!isset($array[$key]) || $array[$key] != $value) {
                return true;
            }
            return false;
        });
    }

    /**
     * Implementation of the "array_slice" function
     *
     * @return Set
     */
    public function slice($offset, $length = null, $preserve_keys = false)
    {
        return new static(array_slice((array) $this, $offset, $length, $preserve_keys));
    }

    /**
     * Retrieve a list of items based on a callback
     *
     * @return Set The filtered results
     */
    public function keepKeys($keys_to_keep)
    {
        return new static(array_intersect_key(
            (array) $this,
            array_flip((array) $keys_to_keep)
        ));
    }

    /**
     * Retrieve a list of items based on a callback
     *
     * @return Set The filtered results
     */
    public function removeKeys($keys_to_remove)
    {
        return new static(array_diff_key(
            (array) $this,
            array_flip((array) $keys_to_remove)
        ));
    }

    // ==================================================
    // > CHANGING ITEMS
    // ==================================================
    /**
     * Implementation of the "array_map" function
     *
     * @param  callable $callback
     * @return Set
     */
    public function map($callback, $with_key = false)
    {
        return $with_key
            ? new static(array_map($callback, (array) $this->values(), (array) $this->keys()))
            : new static(array_map($callback, (array) $this));
    }

    /**
     * Shortcut to update only the keys by maping them
     *
     * @param callable $callback
     * @return Set
     */
    public function mapKeys($callback)
    {
        return $this->mapAssoc(fn ($k, $v) => [$callback($k, $v) => $v]);
    }

    /**
     * Map keys recursively
     *
     * @param callbable $callback
     * @return Set
     */
    public function mapKeysRecursive($callback)
    {
        return $this->mapAssoc(fn ($k, $v) => [$callback($k, $v) => $v instanceof Set ? $v->mapKeysRecursive($callback) : $v]);
    }

    /**
     * Apply a callback to each item without changing the array
     *
     * @param callable $callback
     * @return self
     */
    public function each($callback)
    {
        foreach ($this as $key => $value) {
            $callback($value, $key);
        }
        return $this;
    }

    /**
     * Map an associative array, allow to change its key and value
     *
     * @param  callable $callback Should return [$key, $value] array
     * @param  array    $assoc    The array to process
     * @return Set
     */
    public function mapAssoc($callback)
    {
        return new static(array_reduce(array_map($callback, (array) $this->keys(), (array) $this->values()), function ($total, $subarray) {
            return $total + $subarray;
        }, []));
    }

    /**
     * Implementation of the "array_keys" function
     *
     * @return Set
     */
    public function keys()
    {
        return new static(array_keys((array) $this));
    }

    /**
     * Implementation of the array_v"alues function
     *
     * @return Set
     */
    public function values()
    {
        return new static(array_values((array) $this));
    }

    /**
     * Transform all elements of the set into ints
     *
     * @return Set
     */
    public function ints()
    {
        return $this->map(function ($item) {
            return Scalar::int($item);
        });
    }

    /**
     * Use the values as keys
     *
     * @return Set
     */
    public function valuesAsOptions()
    {
        return $this->mapAssoc(function ($i, $value) {
            return [$value => $value];
        });
    }

    /**
     * Implementation of the "array_flip" function
     * @param bool $multiple Support duplicates values
     *
     * @return Set
     */
    public function flip($multiple = false)
    {
        if (!$multiple) return new static(array_flip((array) $this));

        $set = new self;
        foreach ($this as $key=>$value) $set[$value][] = $key;
        return $set;
    }

    /**
     * Rotate the element in the array
     *
     * @return Set
     */
    public function rotate($num = 1)
    {
        $array = (array) $this;
        $num   = $num % count($array);

        return new self(array_merge(
            array_slice($array, -$num, null),
            array_slice($array, 0, -$num)
        ));
    }

    /**
     * Flatten a multi-dimensional array into a single level
     *
     * @return Set
     */
    public function flatten()
    {
        $return = [];
        $array  = (array) $this->getArrayCopy();
        array_walk_recursive($array, function ($a, $k) use (&$return) {
            $return[] = $k;
        });
        return new static($return);
    }

    /**
     * Keep only a specific column of each child array/set
     *
     * @param  string $name
     * @return Set
     */
    public function column($name)
    {
        $columns = [];

        foreach ($this as $i => $row) {
            $columns[$i] = (new static($row))->get($name);
        }

        return new static($columns);
    }

    /**
     * Reindex an set using a specific column of each each item, or a callback
     *
     * @param  string|callback      $key
     * @param  bool|string|callback $value_key The key to keep for each value
     * @return Sed
     */
    public function index($key, $value_key = false)
    {
        return new static($this->reduce(function ($set, $item) use ($key, $value_key) {
            $key = is_string($key) ? ((array) $item)[$key]
                : $key($item);

            $value = is_string($value_key) ? (((array) $item)[$value_key] ?? null)
                : (is_callable($value_key) ? $value_key($item)
                    : $item);

            $set[$key] = $value;
            return $set;
        }));
    }

    /**
     * Group all children by a common value
     *
     * @param  string               $key       Key of the value to group by
     * @param  bool|string|callback $value_key The key to keep for each value
     * @return Set
     */
    public function groupBy($key, $value_key = false)
    {
        return $this->reduce(function ($groups, $item) use ($key, $value_key) {
            // Get group key
            if (is_callable($key)) {
                $key = $key($item);
            } else {
                $item = (array) $item;
                $key  = ((array) $item)[$key];
            }

            // Get value
            $value = is_callable($value_key) ? $value_key($item)
                : (is_string($value_key) ? ((array) $item)[$value_key]
                    : $item);

            // Init a new group if it does not exist
            $groups[$key] = $groups[$key] ?? [];

            // Add value to the group
            $groups[$key][] = $value;
            return $groups;
        });
    }

    /**
     * Create sub-arrays of size $size
     *
     * @param int $size
     * @return Set
     */
    public function chunk($size)
    {
        return (new static(array_chunk((array) $this, $size)))->instanciate(static::class);
    }

    /**
     * Create an object with each element of the set
     *
     * @return Set
     */
    public function instanciate($class)
    {
        return $this->mapAssoc(fn ($key, $item) => [$key => new $class($item)]);
    }

    /**
     * Merge all ranges of the set
     *
     * @return Set of ranges
     */
    public function mergeRanges()
    {
        // Sort ranges by where they start
        $this->sort(fn ($a, $b) => $a[0] <=> $b[0]);

        // Merge ranges
        return $this->reduce(function ($merged, $range) {
            $last = $merged->last();

            // If the last range overlaps with the current one, extend it
            if ($range[0] <= $last[1] + 1) {
                $last[1] = max($last[1], $range[1]);
                $merged[$merged->count() - 1] = $last;
            // Else, add the range to the list
            } else {
                $merged[] = $range;
            }
            return $merged;
        }, new self([$this->shift()]));
    }

    // ==================================================
    // > ACT ON ITEMS
    // ==================================================
    /**
     * Custom implementation of the "array_walk" function
     *
     * @param  callable $callback
     * @return Set
     */
    public function walk($callback)
    {
        foreach ($this as $key => &$value) {
            $callback($value, $key);
        }
        return $this;
    }

    /**
     * Custom implementation of the "array_walk_recursive" function
     *
     * @param  callable $callback
     * @return Set
     */
    public function walkRecursive($callback)
    {
        foreach ($this as $key => &$value) {
            if ($value instanceof Set) {
                $value->walkRecursive($callback);
            } else {
                $callback($value, $key);
            }
        }
        return $this;
    }

    /**
     * Return a CallableSet that allows to use a specific method on each element of this set.
     *
     * @return CallableMap
     */
    public function callEach($deepness = 1)
    {
        $cc = new CallableMap($this);

        if ($deepness == 1) {
            return $cc;
        }

        return $cc->callEach($deepness - 1)->callEach();
    }

    // ==================================================
    // > REDUCING ITEMS
    // ==================================================
    /**
     * Implementation of the "array_reduce" function
     *
     * @param  callable $callback
     * @param  mixed    $initial    new Set by default
     * @return mixed
     */
    public function reduce($callback, $initial = null, $with_key = false)
    {
        $initial = is_null($initial) ? new static : $initial;

        if ($with_key) {
            return array_reduce((array) $this->keys(), function ($carry, $key) use ($callback) {
                return $callback($carry, $this[$key], $key);
            }, $initial);
        }

        return array_reduce((array) $this, $callback, $initial);
    }

    /**
     * Implode all items with a join
     *
     * @param string $join
     * @return Scalar
     */
    public function join($join = "")
    {
        return scalar(implode($join, (array) $this->values()));
    }

    /**
     * Get the minimum value in the set
     *
     * @return mixed
     */
    public function min()
    {
        return min((array) $this);
    }

    /**
     * Get the maxmium value in the set
     *
     * @return mixed
     */
    public function max()
    {
        return $this->empty() ? null : max((array) $this);
    }

    /**
     * Sum all values in the set, or a specific column of sub-elements if specified
     *
     * @param  string|bool $column
     * @return int|float
     */
    public function sum($column = false)
    {
        $items = $column ? $this->column($column) : $this;

        return $items->reduce(function ($sum, $item) {
            return $sum + Scalar::int($item);
        }, 0);
    }

    /**
     * Multiply all values in the set, or a specific column of sub-elements if specified
     *
     * @param  string|bool $column
     * @return int|float
     */
    public function multiply($column = false)
    {
        $items = $column ? $this->column($column) : $this;

        return $items->reduce(function ($total, $item) {
            return $total * Scalar::int($item);
        }, 1);
    }

    /**
     * Get the average value of the set (or of a specific column)
     *
     * @param boolean $column
     * @return int|float
     */
    public function average($column = false)
    {
        return $this->sum($column) / $this->count();
    }

    /**
     * Get the median value of the set (or of a specific column)
     *
     * @param boolean $column
     * @return mixed
     */
    public function median($column = false)
    {
        $items = $column ? $this->column($column) : $this;
        $items = $items->sort()->values();
        $count = $items->count();

        if ($count % 2 == 0) {
            return ($items[$count / 2 - 1] + $items[$count / 2]) / 2;
        }

        return $items[($count - 1) / 2];
    }

    /**
     * Get all the possible way of sorting the items
     *
     * @param callable $sub_selector A funciton to filter the possible next items
     * @param array    $sofar        The items already selected
     *
     * @return self
     */
    public function permutations()
    {
        $items = (array) $this;
        $count = count($items);

        if ($count === 1) {
            yield $items;
        }

        foreach ($items as $i => $value) {
            $remaining = $items;
            unset($remaining[$i]);

            foreach ((new Self($remaining))->permutations() as $permutation) {
                $permutation[] = $value;
                yield $permutation;
            }
        }
    }

    /**
     * Generator for all the possible ways of combining the items
     *
     * @return Iterable
     */
    public function combinations($min = null, $max = null)
    {
        $combinations = [[]];

        foreach ((array) $this as $item) {
            foreach ($combinations as $combination) {
                $new_combination = array_merge($combination, [$item]);
                $length = count($new_combination);

                if ((is_null($max) || $length <= $max))
                    $combinations[] = $new_combination;

                if (is_null($min) || $length >= $min)
                    if (is_null($max) || $length <= $max)
                        yield $new_combination;
            }
        }
    }

    /**
     * Generator for all pairs of item
     *
     * @return Iterable
     */
    public function pairs()
    {
        $keys = $this->keys();

        for ($i = 0; $i < count($keys) - 1; $i++) {
            for ($j = $i + 1; $j < count($keys); $j++) {
                yield [$this[$keys[$i]], $this[$keys[$j]]];
            }
        }
    }

    /**
     * Generator for consecutive pairs
     *
     * @return Iterable
     */
    public function consecutivePairs()
    {
        $keys = $this->keys();
        for ($i = 0; $i < count($keys) - 1; $i++) {
            yield [$this[$keys[$i]], $this[$keys[$i + 1]]];
        }
    }


    /**
     * Get the count for each unique value
     *
     * @return Set
     */
    public function valueCounts()
    {
        return $this->filter()->groupBy(function ($value) {
            return $value;
        })->map("count");
    }

    /**
     * Check if a value is present in the set
     *
     * @return boolean
     */
    public function hasValue($value, $recursive = false)
    {
        if (in_array($value, (array) $this)) {
            return true;
        }

        if ($recursive) {
            foreach ($this as $item) {
                if (is_array($item)) {
                    $item = new static($item);
                }

                if ($item instanceof Set && $item->hasValue($value, true)) {
                    return true;
                }
            }
            return false;
        }

        return false;
    }

    /**
     * Check if a key is defined in the set
     *
     * @return boolean
     */
    public function hasKey($key, $recursive = false)
    {
        if (array_key_exists($key, (array) $this)) {
            return true;
        }

        if ($recursive) {
            foreach ($this as $item) {
                if (is_array($item)) {
                    $item = new static($item);
                }

                if ($item instanceof Set && $item->hasKey($key, true)) {
                    return true;
                }
            }
            return false;
        }

        return false;
    }

    /**
     * Check if the set is empty : /!\ the empty function will always return false
     *
     * @return bool
     */
    function empty() {
        return !$this->count();
    }

    // ==================================================
    // > DISPLAYING ITEMS
    // ==================================================
    /**
     * Return the items as an html list
     *
     * @param  string   $tag
     * @return string
     */
    public function htmlList($tag = "ul", $class = false)
    {
        return "<$tag" . ($class ? " class='$class'" : "") . ">" . $this->reduce(function ($html, $item) {
            return $html . "<li>$item</li>";
        }, "") . "</$tag>";
    }

    // ==================================================
    // > STATIC TOOLS
    // ==================================================

    /**
     * Check that a set item match a key/value pair
     *
     * @param  array|object $item
     * @param  string       $key
     * @param  mixed        $value
     * @return bool
     */
    public static function itemMatchBy($item, $key, $value)
    {
        if (is_array($item) && isset($item[$key]) && $item[$key] == $value) {
            return true;
        }
        if (is_object($item) && isset($item->{$key}) && $item->{$key} == $value) {
            return true;
        }
        return false;
    }

    /**
     * @return mixed
     */
    public function getArray()
    {
        $array = [];
        foreach ($this as $key => $item) {
            $array[$key] = $item;
        }

        return $array;
    }

    /**
     * Transform a 2 dimentions set into a grid
     */
    public function getGrid()
    {
        return new Grid($this);
    }

    /**
     * Check if the item is a set
     *
     * @param  mixed     $object
     * @return boolean
     */
    public static function is($object)
    {
        return $object instanceof self;
    }

    // ==================================================
    // > MAGIC METHODS
    // ==================================================
    /**
     * When used as string, auto-join with a comma
     *
     * @return string
     */
    public function __toString()
    {
        return $this->join(", ");
    }

    /**
     * Set a key in the array using object notation
     *
     * @param string $key
     * @param self
     */
    public function set($key, $val)
    {
        $parts = static::getKeyParts($key);
        $array = $this->getArrayCopy();
        $pos   = &$array;

        foreach ($parts as $part) {
            $pos[$part] = $pos[$part] ?? [];
            $pos        = &$pos[$part];
        }

        $pos = $val;
        $this->exchangeArray($array);

        return $this;
    }

    /**
     * Get a key from the array using object notation
     *
     * @param  string  $key
     * @return mixed
     */
    public function get($key)
    {
        $parts = static::getKeyParts($key);
        $value = $this->getArrayCopy();

        foreach ($parts as $part) {
            $value = $value[$part] ?? null;
            if (is_null($value)) {return $value;}
        }

        return $value;
    }

    /**
     * Get the parts for a complexe key
     *
     * @param  string  $key
     * @return array
     */
    public static function getKeyParts($key)
    {
        $key = trim($key, "[]");
        $key = str_replace(["][", "[", "]"], ".", $key);
        return explode(".", $key);
    }

    // ==================================================
    // > DEBUG / JsonSerializable Interface
    // ==================================================
    /**
     * When parsed to JSON, return the array version
     *
     * @return array
     */
    public function jsonSerialize(): mixed
    {
        return (array) $this;
    }

    /**
     * Dump the result of a model with all its fields loaded
     *
     * @return void
     */
    public function json()
    {
        \Syltaen\Log::json($this);
    }

    /**
     * Multiply items until a specifc number of items is met, for testing purposes
     *
     * @param  int   $number
     * @return Set
     */
    public function dummies($number)
    {
        $items   = (array) $this;
        $dummies = new static;

        if (empty($items)) {
            return $dummies;
        }

        for ($i = 0; $i < $number; $i++) {
            $dummies = $dummies->push($items[$i % count($items)]);
        }

        return $dummies;
    }
}