Skip to content

Commit

Permalink
add interface
Browse files Browse the repository at this point in the history
  • Loading branch information
n1crack committed Feb 8, 2024
1 parent adb958a commit ea1a8a5
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 190 deletions.
70 changes: 52 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ composer require ozdemir/subset-finder
```

## Usage
Here's a basic example of how to use the SubsetFinder package:
Here's a basic example of how to use the SubsetFinder package:


```php
use Ozdemir\SubsetFinder\SubsetFinder;
Expand All @@ -23,11 +24,33 @@ use Ozdemir\SubsetFinder\Subset;

// Define your collection and subset criteria

// Collection should be an instance of Illuminate\Support\Collection
// and contain items that implement the Ozdemir\SubsetFinder\Subsetable interface.

// example class that implements the Subsetable interface
// if you use field names other than 'id' and 'quantity', you need to define them with defineProps method
class Something implements Subsetable
{
public function __construct(public int $id, public int $quantity, public int $price)
{
}

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

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

$collection = collect([
["id" => 1, "quantity" => 11, "price" => 15],
["id" => 2, "quantity" => 6, "price" => 5],
["id" => 3, "quantity" => 6, "price" => 5]
// Add more items...
new Something(id: 1, quantity: 11, price: 15),
new Something(id: 2, quantity: 6, price: 5),
new Something(id: 3, quantity: 6, price: 5)
// Add more items...
]);

$subsetCollection = new SubsetCollection([
Expand All @@ -42,29 +65,33 @@ $subsetter = new SubsetFinder($collection, $subsetCollection);

// Optionally, configure sorting
$subsetter->sortBy('price');
// Solve the problem
$subsetter->solve();

// $subsets will contain the subsets that meet the criteria
$subsets = $subsetter->get();
$subsets = $subsetter->getFoundSubsets();
// Illuminate\Support\Collection:
// all:[
// ["id" => 2, "quantity" => 6, "price" => 5],
// ["id" => 1, "quantity" => 9, "price" => 15],
// ["id" => 3, "quantity" => 6, "price" => 5]
// Something(id: 2, quantity: 6, price: 5),
// Something(id: 1, quantity: 9, price: 15),
// Something(id: 3, quantity: 6, price: 5)
// ]

// $remaining will contain the items that were not selected for any subset
$remaining = $subsetter->getRemaining();
// Illuminate\Support\Collection:
// all:[
// ["id" => 1, "quantity" => 2, "price" => 15],
// Something(id: 1, quantity: 2, price: 15),
// ]

// Get the maximum quantity of sets that can be created from the collection.
$subSetQuantity = $subsetter->getSetQuantity()
$subSetQuantity = $subsetter->getSubsetQuantity()
// 3

```

You can check the tests for more examples.

## Configuration

### Prioritize items to be included in the subset
Expand All @@ -80,27 +107,34 @@ $subsetCollection = new SubsetCollection([
// When we have multiple applicable items for a subset, we can choose to prioritize the ones
// with any field that exists in the main collection.
$subsetter->sortBy('price');
$subsetter->solve();

```

### Define the field names for the quantity, items and id fields.

```php
// We can use the fields with the defined names.
$subsetter->defineProps(
id: 'name',
quantity: 'amount'
);

$collection = collect([
["name" => 1, "amount" => 11, "price" => 15],
new Something() // let's say, we have an object with the ["name" => 1, "amount" => 11, "price" => 15]
// Add more items...
]);

// Find a subset with a total amount (quantity) of 5 from items named 1 and 2 (id) in the collection
// Find a subset with a total amount of 5 from items named 1 and 2 in the collection
// this part doesn't change
$setCollection = collect([
Subset::of([1, 2])->take(5)
// define more...
]);

// We need to define the field names for the 'quantity' and 'id' fields.
$subsetter->defineProps(
id: 'name',
quantity: 'amount'
);

$subsetter->solve();

```

## Testing
Expand Down
184 changes: 89 additions & 95 deletions src/SubsetFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@

class SubsetFinder
{
protected Collection $flatCollection;

private string $idFieldName = 'id';
private string $quantityFieldName = 'quantity';
private string $sortByField = 'id';
private bool $sortByDesc = false;

protected Collection $flatCollection;

protected Collection $filteredFlatCollection;

protected Collection $foundSubsets;

protected Collection $remainingSubsets;

protected int $subsetQuantity;

/**
* SubsetFinder constructor.
*
Expand Down Expand Up @@ -55,16 +63,51 @@ public function sortBy($field, bool $descending = false): self
return $this;
}

/**
* Get the maximum quantity of sets that can be created from the collection.
*
* @return int
*/
public function getSetQuantity(): int
public function solve(): void
{
return $this->subsetCollection
// Get the maximum quantity of sets that can be created from the collection.
$this->subsetQuantity = $this->subsetCollection
->map(fn ($subset) => $this->calculateQuantityForSet($subset))
->min();

// Get the flattened collection based on the set criteria.
$this->flatCollection = $this->collection
->sortBy($this->sortByField, SORT_REGULAR, $this->sortByDesc)
->whereIn($this->idFieldName, $this->subsetCollection->pluck('items')->flatten(1))
->flatMap(fn ($item) => $this->duplicateItemForQuantity($item));

// Find Subsets
// Initialize a collection to store flattened items
$cartFlatten = collect();
$this->filteredFlatCollection = clone $this->flatCollection;

// Iterate over the subset criteria
foreach ($this->subsetCollection as $subset) {
// Filter and limit items based on subset criteria
$filteredItems = $this->filterAndLimit(
$subset->items,
$subset->quantity * $this->subsetQuantity
);

// Add filtered items to the collection
$cartFlatten->push($filteredItems);
}

// Flatten the collection of collections, group by ID, update the quantity and return the values
$this->foundSubsets = $cartFlatten
->flatten(1)
->groupBy($this->idFieldName)
->map(fn ($itemGroup) => $this->mapItemGroup($itemGroup))
->values();

// Get the set items with their quantities
$setItems = $this->foundSubsets->pluck($this->quantityFieldName, $this->idFieldName)->toArray();

// Calculate remaining quantities for each item, filter out items with zero or negative quantities and return the values
$this->remainingSubsets = clone($this->collection)
->map(fn ($item) => $this->calculateRemainingQuantity(clone $item, $setItems))
->reject(fn ($item) => $item->getQuantity() <= 0)
->values();
}

/**
Expand All @@ -82,28 +125,15 @@ protected function calculateQuantityForSet(Subset $subset): int
return (int)floor($quantity / $subset->quantity);
}

/**
* Get the flattened collection based on the set criteria.
*
* @return Collection
*/
protected function getFlatCollection(): Collection
{
return $this->collection
->sortBy($this->sortByField, SORT_REGULAR, $this->sortByDesc)
->whereIn($this->idFieldName, $this->subsetCollection->pluck('items')->flatten(1))
->flatMap(fn ($item) => $this->duplicateItemForQuantity($item));
}

/**
* Duplicate an item in the collection based on its quantity field value.
*
* @param array $item
* @param Subsetable $item
* @return Collection
*/
protected function duplicateItemForQuantity(array $item): Collection
protected function duplicateItemForQuantity(Subsetable $item): Collection
{
return Collection::times($item[$this->quantityFieldName], fn () => $item);
return Collection::times($item->getQuantity(), fn () => $item);
}

/**
Expand All @@ -115,97 +145,45 @@ protected function duplicateItemForQuantity(array $item): Collection
*/
protected function filterAndLimit($filterIds, $filterLimit): Collection
{
$filtered = $this->flatCollection
->filter(fn ($item) => in_array($item[$this->idFieldName], $filterIds))
$filtered = $this->filteredFlatCollection
->filter(fn (Subsetable $item) => in_array($item->getId(), $filterIds))
->map(fn (Subsetable $item) => $item)
->take($filterLimit);

$this->flatCollection->forget($filtered->keys()->toArray());
// Remove the filtered items from the collection, so it won't be included in the next iteration
$this->filteredFlatCollection->forget($filtered->keys()->toArray());

return $filtered;
}

/**
* Get the subset of the collection based on the set criteria.
* Map the item group to set quantity and return the Subsetable.
*
* @return Collection
* @param Collection<int, Subsetable> $itemGroup
* @return Subsetable
*/
public function get(): Collection
protected function mapItemGroup(Collection $itemGroup): Subsetable
{
// Get the maximum quantity of sets that can be created from the collection
$maxSetQuantity = $this->getSetQuantity();
$firstItem = clone $itemGroup->first();
$firstItem->setQuantity($itemGroup->count());


// Flatten the collection
$this->flatCollection = $this->getFlatCollection();

// Initialize a collection to store flattened items
$cartFlatten = collect();

// Iterate over the subset criteria
foreach ($this->subsetCollection as $subset) {
// Filter and limit items based on subset criteria
$filteredItems = $this->filterAndLimit(
$subset->items,
$subset->quantity * $maxSetQuantity
);

// Add filtered items to the collection
$cartFlatten->push($filteredItems);
}

// Flatten the collection of collections, group by ID, update the quantity and return the values
return $cartFlatten
->flatten(1)
->groupBy($this->idFieldName)
->mapWithKeys(fn ($itemGroup) => $this->mapItemGroup($itemGroup))
->values();
}

/**
* Map the item group to set quantity and return the mapped key-value pair.
*
* @param Collection $itemGroup
* @return array
*/
protected function mapItemGroup(Collection $itemGroup): array
{
$item = $itemGroup->first();
$item[$this->quantityFieldName] = $itemGroup->count();

return [$item[$this->idFieldName] => $item];
}

/**
* Get the remaining items in the collection.
*
* @return Collection
*/
public function getRemaining(): Collection
{
// Get the set items with their quantities
$setItems = $this->get()->pluck($this->quantityFieldName, $this->idFieldName)->toArray();

// Calculate remaining quantities for each item, filter out items with zero or negative quantities and return the values
return $this->collection
->map(fn ($item) => $this->calculateRemainingQuantity($item, $setItems))
->reject(fn ($item) => $item[$this->quantityFieldName] <= 0)
->values();
return $firstItem;
}

/**
* Calculate the remaining quantity for the given item after applying discounts.
*
* @param array $item
* @param Subsetable $item
* @param array $setItems
* @return array
* @return Subsetable
*/
protected function calculateRemainingQuantity(array $item, array $setItems): array
protected function calculateRemainingQuantity(Subsetable $item, array $setItems): Subsetable
{
// Calculate the remaining quantity by subtracting the quantity of the item included in the discount sets
$remainingQuantity = $item[$this->quantityFieldName] - ($setItems[$item[$this->idFieldName]] ?? 0);
$remainingQuantity = $item->getQuantity() - ($setItems[$item->getId()] ?? 0);

// Ensure the remaining quantity is non-negative
$item[$this->quantityFieldName] = max($remainingQuantity, 0);
$item->setQuantity(max($remainingQuantity, 0));

return $item;
}
Expand All @@ -218,6 +196,22 @@ protected function calculateRemainingQuantity(array $item, array $setItems): arr
*/
public function getSubsetItems(int $int): Collection
{
return $this->getFlatCollection()->take($int);
return $this->flatCollection->take($int);
}

public function getSubsetQuantity(): int
{
return $this->subsetQuantity;
}

public function getFoundSubsets(): Collection
{
return $this->foundSubsets;
}

public function getRemaining(): Collection
{
return $this->remainingSubsets;
}

}
12 changes: 12 additions & 0 deletions src/Subsetable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Ozdemir\SubsetFinder;

interface Subsetable
{
public function getId(): mixed;

public function getQuantity(): mixed;

public function setQuantity($quantity): void;
}
Loading

0 comments on commit ea1a8a5

Please sign in to comment.