Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion benchmarks/ArrayValueSetBench.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public function benchStrictNestedUnique(): void
public function provideValueSets(): array
{
$workloads = [];
foreach ([16, 32, 48, 64, 96, 128, 256, 512] as $bytes) {
foreach ([16, 32, 48, 64, 96, 128, 256, 512, 768, 1024, 1536, 2048, 4096, 8192] as $bytes) {
$workloads[$bytes . 'b-duplicates'] = [
'bytes' => $bytes,
'distribution' => 'duplicate-heavy',
Expand Down
169 changes: 169 additions & 0 deletions benchmarks/MembershipCrossoverBench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

declare(strict_types=1);

namespace Infocyph\ArrayKit\Benchmarks;

use Infocyph\ArrayKit\Array\ArrayMulti;
use Infocyph\ArrayKit\Array\ArraySingle;
use PhpBench\Attributes\BeforeMethods;
use PhpBench\Attributes\Iterations;
use PhpBench\Attributes\ParamProviders;
use PhpBench\Attributes\Revs;

#[Revs(3)]
#[Iterations(5)]
#[BeforeMethods('setUp')]
#[ParamProviders('provideMembershipWorkloads')]
final class MembershipCrossoverBench
{
/** @var array<int, int> */
private array $allNeedles = [];

/** @var array<int, int> */
private array $haystack = [];

/** @var array<int, int> */
private array $needles = [];

/** @var array<int, array{id:int}> */
private array $rows = [];

/** @param array{size:int, distribution:string} $params */
public function setUp(array $params): void
{
$this->haystack = range(0, 9999);
$this->rows = array_map(
static fn(int $id): array => ['id' => $id],
$this->haystack,
);
$this->needles = $this->makeNeedles($params['size'], $params['distribution']);
$this->allNeedles = $this->makeAllNeedles($params['size'], $params['distribution']);

// Keep autoloading outside the timed subjects so native and adaptive
// membership paths measure only repeated-operation cost.
ArraySingle::containsAll([], [], true);
ArraySingle::containsAny([], [], true);
ArraySingle::diff([], [], true);
ArraySingle::intersect([], [], true);
ArrayMulti::whereIn([], 'id', [], true);
}

public function benchContainsAllAdaptive(): void
{
ArraySingle::containsAll($this->haystack, $this->allNeedles, true);
}

public function benchContainsAllScan(): void
{
array_all(
$this->allNeedles,
fn(int $needle): bool => in_array($needle, $this->haystack, true),
);
}

public function benchContainsAnyAdaptive(): void
{
ArraySingle::containsAny($this->haystack, $this->needles, true);
}

public function benchContainsAnyScan(): void
{
array_any(
$this->needles,
fn(int $needle): bool => in_array($needle, $this->haystack, true),
);
}

public function benchDiffAdaptive(): void
{
ArraySingle::diff($this->haystack, $this->needles, true);
}

public function benchDiffScan(): void
{
$results = [];
foreach ($this->haystack as $key => $value) {
if (!in_array($value, $this->needles, true)) {
$results[$key] = $value;
}
}
}

public function benchIntersectAdaptive(): void
{
ArraySingle::intersect($this->haystack, $this->needles, true);
}

public function benchIntersectScan(): void
{
$results = [];
foreach ($this->haystack as $key => $value) {
if (in_array($value, $this->needles, true)) {
$results[$key] = $value;
}
}
}

public function benchWhereInAdaptive(): void
{
ArrayMulti::whereIn($this->rows, 'id', $this->needles, true);
}

public function benchWhereInScan(): void
{
$results = [];
foreach ($this->rows as $key => $row) {
if (in_array($row['id'], $this->needles, true)) {
$results[$key] = $row;
}
}
}

/** @return array<string, array{size:int, distribution:string}> */
public function provideMembershipWorkloads(): array
{
$workloads = [];

foreach ([1, 2, 8, 16, 32, 64, 128, 192, 256, 512] as $size) {
foreach (['hit-first', 'hit-last', 'miss'] as $distribution) {
$workloads[$size . '-' . $distribution] = [
'size' => $size,
'distribution' => $distribution,
];
}
}

return $workloads;
}

/**
* @param int $size Number of values in the membership set.
* @param string $distribution Hit placement for the workload.
* @return array<int, int>
*/
private function makeAllNeedles(int $size, string $distribution): array
{
return match ($distribution) {
'hit-first' => range(0, $size - 1),
'hit-last' => range(10000 - $size, 9999),
default => range(10000, 10000 + $size - 1),
};
}

/**
* @param int $size Number of values in the membership set.
* @param string $distribution Hit placement for the workload.
* @return array<int, int>
*/
private function makeNeedles(int $size, string $distribution): array
{
$misses = range(10000, 10000 + $size - 1);

return match ($distribution) {
'hit-first' => [0, ...array_slice($misses, 1)],
'hit-last' => [...array_slice($misses, 0, -1), 9999],
default => $misses,
};
}
}
29 changes: 23 additions & 6 deletions docs/array-helpers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,9 @@ ArraySingle: Search, Partition, Aggregation
$hasAll = ArraySingle::containsAll($arr, [1, 2, 3]); // true
$hasAny = ArraySingle::containsAny($arr, [99, 3]); // true
[$even, $odd] = ArraySingle::partition($arr, fn ($v) => $v % 2 === 0);
$dupes = ArraySingle::duplicates($arr); // [2]
$unique = ArraySingle::unique($arr); // [1,2,3,4,5]
$dupes = ArraySingle::duplicates($arr); // [2] (loose by default)
$strictDupes = ArraySingle::duplicates($arr, true); // strict comparison
$unique = ArraySingle::unique($arr); // [0=>1,1=>2,3=>3,4=>4,5=>5,6=>'x']
$sum = ArraySingle::sum($arr); // 17 (non-numeric ignored)
$avg = ArraySingle::avg($arr); // 17/6 (non-numeric ignored)
$median = ArraySingle::median($arr); // 2.5
Expand All @@ -126,8 +127,8 @@ ArraySingle: Numeric and Value Helpers

$values = [-2, -1, 0, 1, 2, 3, 'x'];

$positive = ArraySingle::positive($values); // [1,2,3]
$negative = ArraySingle::negative($values); // [-2,-1]
$positive = ArraySingle::positive($values); // [3=>1,4=>2,5=>3]
$negative = ArraySingle::negative($values); // [0=>-2,1=>-1]
$isInt = ArraySingle::isInt([1, 2, 3]); // true
$isPositive = ArraySingle::isPositive([1, 2, 'x']); // true
$isNegative = ArraySingle::isNegative([-1, -2, 'x']); // true
Expand Down Expand Up @@ -308,7 +309,7 @@ Behavior Matrix
- Dot-path support
- Wildcard support
* - ``ArraySingle``
- yes (except ``values()``, ``unique()``, positional list helpers)
- yes (except ``values()`` and positional list helpers)
- no
- no
- no
Expand Down Expand Up @@ -337,14 +338,30 @@ Behavior Notes
- Many methods preserve original keys (especially ``slice``, ``where``, ``skip`` variants).
- ``ArraySingle::isAssoc([])`` is ``false``; empty arrays are treated as non-associative.
- ``ArraySingle::nth($array, $step, $offset)`` starts at ``$offset`` then takes every ``$step`` item.
- ``ArraySingle::unique()`` has loose mode (default) and strict mode.
- ``ArraySingle::unique()`` preserves original keys. Both ``unique()`` and
``duplicates()`` have loose mode (default) plus explicit strict mode.
- ``ArraySingle::avg()``, ``sum()``, ``isPositive()``, and ``isNegative()`` ignore non-numeric values.
- Numeric min/max/sum paths retain integer values and precision unless PHP naturally
promotes an arithmetic result to ``float``.
- ``ArraySingle::paginate()`` requires ``$page >= 1`` and ``$perPage >= 1``.
- ``chunk()`` rejects non-positive sizes, ``BaseArrayHelper::range()`` rejects a
zero step, ``ArraySingle::combine()`` requires equal counts, and
``ArrayMulti::transpose()`` requires rows with identical keys.
- ``ArraySingle::mode()`` counts only integer and string values; other value
types are ignored.
- Key-producing callbacks must return an actual integer or string. Missing
string fields are skipped by ``uniqueBy()``, ``duplicatesBy()``, ``keyBy()``,
``indexBy()``, ``countBy()``, and indexed ``pluck()``; an explicit ``null``
field remains a present value and throws where an array key is required.
- ``ArrayMulti::whereIn()`` / ``whereNotIn()`` treat ``null`` as a real value when the key exists.
- ``ArrayMulti::where()`` / ``firstWhere()`` distinguish explicit ``null`` from
the two-argument shorthand form.
- ``ArrayMulti::flatten($array, 0)`` returns unchanged top-level values.
- Use ``depthGuarded()``, ``flattenGuarded()``, and ``sortRecursiveGuarded()`` when processing untrusted/deep inputs.
- ``ArrayMulti`` callback helpers such as ``sortBy()``, ``sum()``, ``maxBy()``, ``minBy()`` support ``($row, $key)``.
- In ``string|callable`` row APIs, strings always identify fields; use a closure or
another non-string callable for callback behavior.
- ``ArrayMulti::where()`` uses ``Infocyph\ArrayKit\compare()`` semantics for operators.
- ``BaseArrayHelper::random()`` throws ``InvalidArgumentException`` when requested count exceeds array size.
- Recursive/reference-containing arrays are outside the supported input contract
for unguarded equality, fingerprint, flatten, and recursive-sort helpers.
7 changes: 6 additions & 1 deletion docs/collection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ HookedCollection
// Dot-notation hooks are supported
$c->onGet('user.city', fn ($v) => ucfirst((string) $v));

// Copies keep registered hooks but have isolated data and pipeline state
$copy = $c->copy();

echo $c['name']; // ALICE
$c['role'] = 'admin';
echo $c['role']; // Role: admin
Expand Down Expand Up @@ -193,7 +196,6 @@ Structure and reshape:
- ``flatten()``, ``flattenByKey()``, ``collapse()``
- ``groupBy()``, ``keyBy()``, ``indexBy()``, ``pluck()``, ``transpose()``
- ``mapWithKeys()``, ``values()``, ``rekey()``
- ``wrap()``, ``unWrap()``

Ordering and uniqueness:

Expand Down Expand Up @@ -328,3 +330,6 @@ Behavior Notes
- ``paginate()`` throws ``InvalidArgumentException`` when ``page < 1`` or ``perPage < 1``.
- ``flatten(0)`` keeps top-level values unchanged; ``flatten(1)`` flattens one level.
- ``sum()`` and numeric min/max flows ignore non-numeric values.
- ``HookedCollection::copy()`` preserves hooks while isolating collection data
and cached pipeline state.
- ``duplicates()`` is loose by default and accepts ``true`` for strict comparison.
13 changes: 12 additions & 1 deletion docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,13 @@ Reading Values
$hasAny = $config->hasAny(['missing.path', 'queue.driver']); // true
$required = $config->getOrFail('app.name'); // throws if missing

Defaults may be plain values or ``Closure`` instances. A closure is evaluated
only when the requested key is missing; other callable values are returned as
ordinary config values.

Bulk ``getOrFail([...])`` validates every requested path individually. Existing
``null`` values are valid; the first missing path raises ``OutOfBoundsException``.

Typed Getters
-------------

Expand Down Expand Up @@ -265,6 +272,10 @@ Single key:
$config->set('cache.driver', 'file');
$config->set('db.port', 5432);

``append()`` and ``prepend()`` create a list when the path is missing and modify
an existing array. They throw ``InvalidArgumentException`` for scalar paths so
existing data is never silently discarded.

Bulk set:

.. code-block:: php
Expand Down Expand Up @@ -318,7 +329,7 @@ Merging, Snapshots, and Read-Only Mode
$config->snapshot('before-runtime');

$config->merge(['app' => ['env' => 'production']]); // deep merge
$config->overlay(['features' => ['beta' => true]]); // top-level overlay
$config->overlay(['features' => ['beta' => true]]); // recursive overlay

$changed = $config->changed('before-runtime'); // true/false
$config->restore('before-runtime'); // rollback
Expand Down
26 changes: 25 additions & 1 deletion docs/dot-notation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ Use ``\\.`` inside a path segment to target literal key dots:
DotNotation::set($data, 'service\\.env', 'prod');
DotNotation::forget($data, 'service\\.env');

Plain dotted strings always identify paths. Direct lookup is reserved for keys
without path syntax, so reads and mutations resolve the same location even when
both a literal dotted key and an equivalent nested path exist.

Reading Multiple Keys
---------------------

Expand Down Expand Up @@ -80,6 +84,9 @@ Fill vs Set
DotNotation::fill($data, 'app.env', 'staging'); // does not overwrite
DotNotation::fill($data, 'app.debug', true); // writes

If an intermediate segment is already a scalar, ``fill()`` leaves it unchanged;
it does not replace existing data merely to create a deeper path.

Object properties (including null-valued properties) are treated as existing
when filling:

Expand Down Expand Up @@ -145,6 +152,12 @@ Forgetting Keys
// Wildcard remove (all users.*.secret)
DotNotation::forget($data, 'users.*.secret');

// Terminal wildcards clear every item at their target
DotNotation::forget($data, 'users.*');

Forgetting an impossible child path never deletes its scalar parent. For example,
``forget($data, 'a.b')`` leaves ``a`` unchanged when ``a`` is scalar.

Wildcards and Special Segments in get()
---------------------------------------

Expand Down Expand Up @@ -258,7 +271,18 @@ Behavior Notes
- Existing keys with ``null`` values return ``null`` (not the default).
- Existing object properties with ``null`` values are also treated as present.
- Missing integer keys return the provided default.
- Defaults may be plain values or callables, and callables are only evaluated when path resolution fails.
- Plain dotted strings are paths; escape a dot (``foo\\.bar``) to address a
literal dotted key.
- ``get()``, ``set()``, ``fill()``, ``forget()``, ``rename()``, and ``move()``
share that path-resolution rule.
- ``flatten()`` and ``paths()`` escape literal dots, backslashes, wildcards, and
``{first}``/``{last}`` selectors, so ``expand(flatten($data))`` round-trips
those keys.
- Object writes support ``stdClass`` additions, existing public writable
properties, and magic ``__set`` handlers. Missing ordinary properties and
inaccessible or readonly properties fail with ``InvalidArgumentException``.
- Defaults may be plain values or ``Closure`` instances. Closures are evaluated
only when path resolution fails; other callable values are returned unchanged.
- Wildcard traversal in ``get`` returns arrays of matched results.
- ``set`` supports wildcards at any path depth, including multiple wildcards.
- ``forget`` supports wildcard and nested removal across arrays.
Expand Down
1 change: 1 addition & 0 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Requirements
ArrayKit has the following requirements:

* **PHP 8.4+**
* **ext-hash** (used by collision-safe strict value-set acceleration)

Autoload is PSR-4 based and loads namespaced helper functions from
``Infocyph\ArrayKit\*`` by default.
Expand Down
11 changes: 10 additions & 1 deletion docs/migration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ Recent Additions
Compatibility Notes
-------------------

- ``unWrap()`` is the current helper/pipeline method name.
- ``wrap()`` and ``unWrap()`` remain array-helper methods but are no longer
exposed on ``Pipeline`` because pipeline state is always an array.
- Pipeline methods are mutable by design: most transformation methods update the same collection instance and return it.
- Use ``copy()`` or ``immutable()`` before pipeline operations when functional immutability is preferred.

Expand All @@ -47,6 +48,14 @@ Behavior Changes
- ``Collection`` relies on ``IteratorAggregate``. Calls to the former manual
pointer surface (``current()``, ``key()``, ``next()``, ``rewind()``, and
``valid()``) should be replaced with ``foreach`` or ``getIterator()``.
- Plain dotted strings now consistently identify DotNotation paths across reads
and mutations. Escape literal dots (for example ``service\\.name``).
- ``duplicates()`` now mirrors ``unique()`` with loose comparison by default and
an optional ``$strict`` flag.
- Strings in ``string|callable`` row APIs always identify field names. Use a
closure or another non-string callable to select callback behavior.
- Numeric selection and accumulation preserve integer precision instead of
coercing all values to ``float``.

Recommended Upgrade Checklist
-----------------------------
Expand Down
Loading