PHPackages                             ozdemir/subset-finder - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. ozdemir/subset-finder

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

ozdemir/subset-finder
=====================

Find how many complete sets can be built from a pool of item quantities - cart promotions, bundles, inventory allocation.

v3.0.0(4w ago)28[1 PRs](https://github.com/n1crack/subset-finder/pulls)MITPHPPHP ^8.2CI passing

Since Feb 6Pushed 1w ago1 watchersCompare

[ Source](https://github.com/n1crack/subset-finder)[ Packagist](https://packagist.org/packages/ozdemir/subset-finder)[ Docs](https://github.com/n1crack/subset-finder)[ GitHub Sponsors](https://github.com/n1crack)[ RSS](/packages/ozdemir-subset-finder/feed)WikiDiscussions main Synced yesterday

READMEChangelog (6)Dependencies (10)Versions (10)Used By (0)

SubsetFinder PHP Package
========================

[](#subsetfinder-php-package)

[![Latest Version on Packagist](https://camo.githubusercontent.com/91ec2a98be009ab96db94f6746c8386448bde29aeab60e4f33adcf2ded030f21/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6f7a64656d69722f7375627365742d66696e646572)](https://packagist.org/packages/ozdemir/subset-finder)[![GitHub Tests Action Status](https://camo.githubusercontent.com/076a1a6be4bd11b56fae4cbdecd140bd4db02c723aaedff14ef79d7e6ffd68da/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f6e31637261636b2f7375627365742d66696e6465722f72756e2d74657374732e796d6c)](https://github.com/n1crack/subset-finder/actions)[![License](https://camo.githubusercontent.com/6bfbc7a56705e7d3604693dd593df37215101a0bfd79ceaee99e3b8793f03d6b/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f6e31637261636b2f7375627365742d66696e646572)](LICENSE.md)

A dependency-free PHP package for finding subsets within collections based on quantity criteria.

Given a pool of items with quantities, it answers: *"How many complete sets can I build, which items go into them, and what is left over?"* — useful for bundle pricing, cart discounts ("buy 5 of X and 2 of Y"), and inventory allocation.

**[▶ See it in action](https://n1crack.github.io/subset-finder/)** — five practical use cases (cart promotions, gift boxes, assembly, capacity planning, shared stock) with output captured from the actual package. The page is generated by `php docs/build.php`.

Features
--------

[](#features)

- **Pure arithmetic solver**: quantities are never expanded into unit items, so memory stays flat and quantities in the billions solve in milliseconds
- **Overlap aware**: subsets sharing the same item ids draw from a shared pool and are never double counted
- **Flexible ordering**: allocate cheapest (or any sort order) items first
- **Type safe**: PHP 8.2+, strict `Subsetable` interface
- **Zero dependencies**: plain PHP; accepts arrays or any iterable (including Laravel collections)

Installation
------------

[](#installation)

```
composer require ozdemir/subset-finder
```

Quick Start
-----------

[](#quick-start)

```
use Ozdemir\SubsetFinder\Subset;
use Ozdemir\SubsetFinder\SubsetCollection;
use Ozdemir\SubsetFinder\SubsetFinder;
use Ozdemir\SubsetFinder\SubsetFinderConfig;

// Define your collection and subset criteria.
// Any iterable works: a plain array, a generator, or a Laravel collection.
$collection = [
    new Product(id: 1, quantity: 11, price: 15),
    new Product(id: 2, quantity: 6, price: 5),
    new Product(id: 3, quantity: 6, price: 5),
];

$subsetCollection = new SubsetCollection([
    Subset::of([1, 2])->take(5), // Each set needs 5 items from products 1 and 2
    Subset::of([3])->take(2),    // ...and 2 items from product 3
]);

// Allocate the cheapest items first
$config = new SubsetFinderConfig(sortField: 'price');

$subsetFinder = new SubsetFinder($collection, $subsetCollection, $config);
$subsetFinder->solve();

$subsetFinder->getSubsetQuantity(); // 3 — max number of complete sets
$subsetFinder->getFoundSubsets();   // Subsetable[] — id 2 ×6, id 1 ×9, id 3 ×6 (cheapest first)
$subsetFinder->getRemaining();      // Subsetable[] — id 1 ×2 left over
```

### The Subsetable interface

[](#the-subsetable-interface)

Collection items must implement `Subsetable`:

```
use Ozdemir\SubsetFinder\Subsetable;

class Product implements Subsetable
{
    public function __construct(
        public int|string $id,
        public int $quantity,
        public float $price,
    ) {
    }

    public function getId(): int|string
    {
        return $this->id;
    }

    public function getQuantity(): int
    {
        return $this->quantity;
    }

    public function setQuantity(int $quantity): void
    {
        $this->quantity = $quantity;
    }
}
```

Item ids and quantities are read through the interface, so your property names don't matter. Only `sortField` in the config refers to a property of your objects.

### Configuration

[](#configuration)

```
$config = new SubsetFinderConfig(
    sortField: 'price',    // Property used to order allocation (default: 'id')
    sortDescending: false  // Ascending = cheapest first (default)
);
```

### Using the Trait

[](#using-the-trait)

Add subset operations to any iterable collection class of your own — for example a Laravel collection:

```
use Illuminate\Support\Collection;
use Ozdemir\SubsetFinder\Traits\HasSubsetOperations;

class ProductCollection extends Collection
{
    use HasSubsetOperations;
}

$products = new ProductCollection([/* Subsetable items */]);

$subsetFinder = $products->findSubsets($subsetCollection);
$products->canSatisfySubsets($subsetCollection);   // bool
$products->getMaxSubsetQuantity($subsetCollection); // int
```

### Other methods

[](#other-methods)

```
$subsetFinder->getSubsetItems(10);          // First 10 units in sort order
$subsetFinder->isOptimal();                 // true if nothing is left over
$subsetFinder->getEfficiencyPercentage();   // Used / total quantity
$subsetFinder->getPerformanceMetrics();     // Timing and counts of the last solve()
```

When it fits
------------

[](#when-it-fits)

The solver models **fungible quantity pools**: units of the same id are interchangeable, and an item's quantity can split freely across sets (goods, portions, hours, credits). It does not model per-set distinctness — if each set needs N *different* individuals (e.g. two distinct people per shift), that's an assignment problem, not a quantity pool.

How it works
------------

[](#how-it-works)

1. Quantities are aggregated per item id; items are sorted by `sortField`.
2. The maximum number of complete sets is found by binary search. For each candidate, subsets claim quantities from the shared pool in definition order, consuming items in sort order.
3. The winning allocation becomes `getFoundSubsets()`; whatever is left becomes `getRemaining()`.

The solver never materializes individual units, so runtime and memory depend on the number of *distinct items*, not their quantities.

Error Handling
--------------

[](#error-handling)

```
use Ozdemir\SubsetFinder\Exceptions\InsufficientQuantityException;
use Ozdemir\SubsetFinder\Exceptions\InvalidArgumentException;

try {
    $subsetFinder = new SubsetFinder($collection, $subsetCollection);
    $subsetFinder->solve();
} catch (InvalidArgumentException $e) {
    // Empty collection, or items not implementing Subsetable
} catch (InsufficientQuantityException $e) {
    // Not even one complete set can be built
}
```

Testing
-------

[](#testing)

```
# Run tests
composer test

# Run tests with coverage
composer test-coverage

# Run static analysis
composer analyse
```

Contributing
------------

[](#contributing)

Contributions are welcome! Please feel free to submit a Pull Request.

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

46

—

FairBetter than 92% of packages

Maintenance96

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity60

Established project with proven stability

 Bus Factor1

Top contributor holds 76.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~142 days

Recently: every ~213 days

Total

7

Last Release

28d ago

Major Versions

1.0.3 → v2.1.12025-08-16

2.1.2 → v3.0.02026-06-07

PHP version history (2 changes)1.0.0PHP ^8.1

v3.0.0PHP ^8.2

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/712404?v=4)[Yusuf Özdemir](/maintainers/n1crack)[@n1crack](https://github.com/n1crack)

---

Top Contributors

[![n1crack](https://avatars.githubusercontent.com/u/712404?v=4)](https://github.com/n1crack "n1crack (58 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (12 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (6 commits)")

---

Tags

collectiondiscountecommercephpsubset-selectionlaravelcollectionssubsetozdemir

###  Code Quality

TestsPest

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/ozdemir-subset-finder/health.svg)

```
[![Health](https://phpackages.com/badges/ozdemir-subset-finder/health.svg)](https://phpackages.com/packages/ozdemir-subset-finder)
```

###  Alternatives

[grumpydictator/firefly-iii

Firefly III: a personal finances manager.

23.9k69.5k](/packages/grumpydictator-firefly-iii)[laravel/nightwatch

The official Laravel Nightwatch package.

36210.1M36](/packages/laravel-nightwatch)[firefly-iii/data-importer

Firefly III Data Import Tool.

8035.8k](/packages/firefly-iii-data-importer)[markwalet/nova-modal-response

A Laravel Nova asset for Modal responses on an action.

17878.9k](/packages/markwalet-nova-modal-response)[ronasit/laravel-helpers

Provided helpers function and some helper class.

2085.6k31](/packages/ronasit-laravel-helpers)[team-nifty-gmbh/tall-datatables

Server-side rendered datatables for Laravel and Livewire

1320.9k4](/packages/team-nifty-gmbh-tall-datatables)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
