Laravel | PHP - Segmenting an array of results to top 10 - php

I'm new to PHP so my solution might be very inefficient so asking here to figure out something more efficient/better.
Consider an array of objects:
[
['sku' => 'AAA', 'amount' => ###],
['sku' => 'BBB', 'amount' => ###],
['sku' => 'CCC', 'amount' => ###],
['sku' => 'DDD', 'amount' => ###],
['sku' => 'EEE', 'amount' => ###],
['sku' => 'FFF', 'amount' => ###],
['sku' => 'GGG', 'amount' => ###],
['sku' => 'HHH', 'amount' => ###],
['sku' => 'III', 'amount' => ###],
['sku' => 'JJJ', 'amount' => ###],
['sku' => 'KKK', 'amount' => ###],
['sku' => 'LLL', 'amount' => ###],
['sku' => 'MMM', 'amount' => ###],
]
We want to keep the first 9 as they are, but consolidate the remaining under the 'sku' => 'Other' and some the amount.
Here is the working version of the code:
$data = DB::table('analytics')
->whereBetween('order_date', [$start, $end])
->whereIn('source', $suppliers)->select(
[
'sku',
DB::raw('SUM(analytics.price' . ($costs ? ' + ' . $costs : '') . ') as amount'),
]
)
->orderBy('amount', 'DESC')
->groupBy('sku')
->get();
$dataArray = $data->toArray();
$topNine = array_slice($dataArray, 0, 9);
$other = array_slice($dataArray, 9);
if (count($other)) {
$otherSum = array_reduce($other, function ($carry, $item) {
return $carry += moneyStringToFloat($item->cogs);
}, 0);
$otherObj = new stdClass();
$otherObj->sku = 'Other';
$otherObj->cogs = floatToMoneyString($otherSum);
$topNine[] = $otherObj;
}
And the final result looks something like this:
[
['sku' => 'AAA', 'amount' => ###],
['sku' => 'BBB', 'amount' => ###],
['sku' => 'CCC', 'amount' => ###],
['sku' => 'DDD', 'amount' => ###],
['sku' => 'EEE', 'amount' => ###],
['sku' => 'FFF', 'amount' => ###],
['sku' => 'GGG', 'amount' => ###],
['sku' => 'HHH', 'amount' => ###],
['sku' => 'III', 'amount' => ###],
['sku' => 'Other', 'amount' => ###],
]
Is there a better way to do this. Is there a way to do it directly in QueryBuilder?
Thank you,

Laravel is about working with the Laravel Collection and all the methods it provides.
Firstly, you don't have to convert it to an array, work with the data as is. Collections has a slice method, and instead of reducing it has a sum method which does the same as you are doing. So instead of juggling between arrays, PHP functions etc. keep it simple, shorter and Laravelistic. Creating a default object in PHP, can be done multiple ways, I like to create arrays and cast them to objects, all methods are OK, but I feel this is cleanest and shortest.
$analytics = DB::table('analytics')->...;
$topNine = $analytics->slice(0, 9);
$otherSum = $analytics->slice(9)->sum(function ($analytic) {
return moneyStringToFloat($item->cogs);
});
$topNine->push((object)[
'sku' => 'Other',
'cogs' => floatToMoneyString($otherSum),
]);
return $topNine;
Your code was fine, I cleaned it up and used a more Laravel approach, hope you get inspiration from it.
As a bonus, you can use analytics as a model, create an Eloquent accessor. This can make your sum into this nice syntactic sugar using Higher Order Functions on the Collection methods.
class Analytic extends Model
{
protected function cogsPrice(): Attribute
{
return Attribute::make(
get: fn ($value) => moneyStringToFloat($this->cogs),
);
}
}
$topNine = $analytics->slice(0, 9);
$topNine->push((object)[
'sku' => 'Other',
'cogs' => floatToMoneyString($analytics->slice(9)->sum->cogsPrice),
]);
return $topNine;

Depending on the size of the data you're returning and what you intend to do with that data (i.e. do you need it all later?) you could return all the data and then manipulate it in memory.
$all = Analytics::orderBy('amount', 'DESC');
$merged = collect($all->take(9)->get(['sku', 'amount'])->toArray())
->merge(collect(array(['sku' => 'other', 'amount' => $all->skip(9)->sum('amount')])));
Alternatively, if you're only interested in the first 9 individual records and everything from the 10th record onward is of no interest and you don't require them later for any logic, you could get the first 9 and then everything else:
$top9 = Analytics::orderBy('amount', 'DESC')->take(9);
$other = collect(array(['sku' => 'other', 'amount' => Analytics::whereNotIn('sku', $top9->pluck('sku'))->sum('amount')]));
$merged = collect($top9->get(['sku', 'amount'])->toArray())
->merge($other);
The above option means not loading a potentialy large data set into memory and performing the limiting and summing operation on the database, but does require some additional calls to the database. So there is a trade off to be taken into consideration with these.

You can use take() or limit() here like this:
$data = DB::table('analytics')
->whereBetween('order_date', [$start, $end])
->whereIn('source', $suppliers)->select()
->orderBy('amount', 'DESC')
->groupBy('sku')
->get()
->take(9);
OR
$data = DB::table('analytics')
->whereBetween('order_date', [$start, $end])
->whereIn('source', $suppliers)->select()
->orderBy('amount', 'DESC')
->groupBy('sku')
->limit(9)
->get();
The Difference
Although they both do pretty much the same thing there's some difference worth knowing in them.
limit()
limit() only works on eloquent ORM or query builder objects. which means that the number n specified in the limit(n) once the query finds the number of record equal to this it will simple stop executing making query run faster.
Syntax:
limit(9)->get() // correct
get()->limit(9) // incorrect
take()
take() take will simply let the query run until the records are fetched and then it simply extracts the number of records specified, making it slower as compared to limit() but it has it's own uses such as having the count of all records but taking only few records as in your case.
Syntax:
take(9)->get() // correct
get()->take(9) // correct
Your Case
Since you want all the records here's what you can do is, once the data is fetched in $data, you can simply:
$topNine = $data->take(9)->toArray()
$other = $data->skip(9)->take(PHP_INT_MAX)->toArray() // Some very large number to take all the remaining records PHP_INT_MAX in that case
Here skip() basically skips any number of elements you want to exclude in $other. you can skip(count($topNine)).
Hope this makes thing easy to understand for you.

Related

Sort collection in alphabetic order

I'm trying to print my collection sorted alphabeticall. Here's what I've tried inside my controller:
public function listForCategories(Category $category)
{
return $category->subcategories->sortBy('title');
}
But It's not sorting my output :/ Please help!
$category->subcategories->sortBy('title')->values()->all();
I don't know exact hierarchy but you can use the solution as per your needs:
The sortBy method sorts the collection by the given key. The sorted collection keeps the original array keys,
so in this example, we'll use the values method to reset the keys to consecutively numbered indexes:
Here is an example :
$category = collect([
['title' => 'Desk', 'price' => 200],
['title' => 'Chair', 'price' => 100],
['title' => 'Bookcase', 'price' => 150],
]);
$sorted = $category->sortBy('title')->values()->all();
Result-:
[
['title' => 'Bookcase', 'price' => 150],
['title' => 'Chair', 'price' => 100],
['title' => 'Desk', 'price' => 200],
]
try return $category->subcategories->orderBy('title');
The sortBy method sorts the internal fields, however, it preserves the original keys, therefore, if you want to have the sorted values, you should call the values() method after you've sorted the collection. That way you get the sorted collection back.
return $category->subcategories->sortBy('title')->values()->all();
Try using orderBy when you retrieve them from the database (I'm assuming you do)
public function listForCategories(Category $category)
{
return $category->subcategories()->orderBy('title')->get();
}

Find combinations of a two dimensional array

I need some help with generation of combinations, specifically in the store they're the variants of each product, e.g size and colour.
Let's say we have 3 customizable properties of the product:
Colour, Size, and Type.
For this specific product, the following are available of each property:
Color: [red, green], Size: [10, 11, 15], Type: [person]
Now according to the above data, I need to generate 6 combinations, however if we added another type it would increase even more.
I have been drawing on my board for 2 hours now trying to come up with a sane algorithm for this, something that's fast and can deal with thousands of combinations in a matter of seconds.
Take this example:
$options = ['Color' => ['Red', 'Green'], 'Size' => ['10', '11', '15'], 'Type' => ['person']];
$combinations = generateCombinations($options);
genereateCombinations would then need to generate the following output:
[
['Color' => 'Red', 'Size' => '10', 'Type' => 'person'],
['Color' => 'Red', 'Size' => '11', 'Type' => 'person'],
['Color' => 'Red', 'Size' => '15', 'Type' => 'person'],
['Color' => 'Green', 'Size' => '10', 'Type' => 'person'],
['Color' => 'Green', 'Size' => '11', 'Type' => 'person'],
['Color' => 'Green', 'Size' => '15', 'Type' => 'person']
];
What algorithm could do this efficiently and with unlimited input "titles"? (of course I'll enforce a limit earlier, but the algorithm should be able to do unlimited granted all the resources in the world)
Extending what I mean:
This function also needs to be able to take for example an array with 100 property rows, not just 3, it needs to be able to do this dynamically no matter the number of input rows.
Three foreach loops are enough to generate all combinations, no matter how many entries are in $options:
function generateCombinations(array $options)
{
// Start with one combination of length zero
$all = array(array());
// On each iteration append all possible values of the new key
// to all items in $all; generate this way all the combinations
// one item longer than before
foreach ($options as $key => $values) {
// Move all combinations of length N from $all to $current
$current = $all;
// Start with an empty list of combinations of length N+1
$all = array();
// Combine each combination of length N
// with all possible values for the (N+1)th key
foreach ($current as $one) {
foreach ($values as $val) {
// Put each new combination in $all (length N+1)
$all[] = array_merge($one, array($key => $val));
}
}
}
return $all;
}
$options = [
'Color' => ['Red', 'Green'],
'Size' => ['10', '11', '15'],
'Type' => ['person'],
'Answer' => ['Yes', 'No'],
];
$combinations = generateCombinations($options);
echo(count($combinations));
# 12
It can probably be slightly improved but, all in all, if you don't know in advance the length of $options it does a lot of duplicate iterations. If you know in advance the number of items in $options (let's say it is N) then N nested loops are the fast way to do it.

Best way to convert keys in PHP

Im retrieving data from a mysql database like following Array:
$data = [
0 => [
'id' => 1,
'Benutzer' => 'foo',
'Passwort' => '123456',
'Adresse' => [
'Strasse' => 'bla', 'Ort' => 'blubb'
],
'Kommentare' => [
0 => ['Titel' => 'bar', 'Text' => 'This is great dude!'],
1 => ['Titel' => 'baz', 'Text' => 'Wow, awesome!']
]
],
]
Data like this shall be stored in a mongo database and therefore i want to replace the keynames with translated strings that come from a config- or languagefile ('Benutzer' -> 'username').
Do i really have to iterate over the array and replace the keys or is the a better way to achieve that?
If you don't want to iterate over the array then you can change the column name in the query itself using select() function.
Considering your model name is Client then your query will be:
Client::select('Benutzer as username', '...') // you can use `trnas()` function here also
->get()

How to concatenate values of a laravel collection

Here is a small challenge for Laravel fanboys :-)
I want to build a simple list of request segments along to their url.
I start with:
// http://domain/aaa/bbb/ccc/ddd
$breadcrumbs = collect(explode('/', $request->path()))
But I don't know how to map it to a collection looking like:
$breadcrumbs = collect([
['title' => 'aaa', 'link' => 'http://domain/aaa'],
['title' => 'bbb', 'link' => 'http://domain/aaa/bbb'],
['title' => 'ccc', 'link' => 'http://domain/aaa/bbb/ccc'],
['title' => 'ddd', 'link' => 'http://domain/aaa/bbb/ccc/ddd'],
])
I could easily do it with a for loop but I am looking for a really elegant way to do it. I tried with map() or each() without success.
As Adam Wathan says: "Never write another loop again." ;-)
There are quite a few ways you can go about doing this, but since you will inevitably require knowledge of past items, I would suggest using reduce(). Here's a basic example that will show you how to build up the strings. You could easily add links, make the carry into an array, etc.
collect(['aaa', 'bbb', 'ccc', 'ddd'])
->reduce(function ($carry, $item) {
return $carry->push($carry->last() . '/' . $item);
}, collect([]));
Results in
Illuminate\Support\Collection {#928
#items: array:4 [
0 => "/aaa"
1 => "/aaa/bbb"
2 => "/aaa/bbb/ccc"
3 => "/aaa/bbb/ccc/ddd"
]
}
Not claiming it's by any means optimised, but it does work. :)
This is old, but a bit different approach - works in Laravel 5.1 and up.
//your collection
$breadcrumbs = collect([
['title' => 'aaa', 'link' => 'http://domain/aaa'],
['title' => 'bbb', 'link' => 'http://domain/aaa/bbb'],
['title' => 'ccc', 'link' => 'http://domain/aaa/bbb/ccc'],
['title' => 'ddd', 'link' => 'http://domain/aaa/bbb/ccc/ddd'],
])
//one-liner to get you what you want
$result = explode(',', $breadcrumbs->implode('link', ','));
//here is what you will get:
array:4 [▼
0 => "http://domain/aaa"
1 => "http://domain/aaa/bbb"
2 => "http://domain/aaa/bbb/ccc"
3 => "http://domain/aaa/bbb/ccc/ddd"
]

How to turn sql result to multi-dimensional array dynamically?

Here is the query string.
$query = "SELECT t.id, t.assignee, t.owner,
d.code, d.status, d.target_completion_date,
d.target_extension_date, d.submission_date, d.approval_date,
d.revision_start_date, d.revision_completion_date, d.message,
ty.name, f.orig_name, f.new_name,
b.payment_date, b.discount, b.total_cost, b.amount_payed, b.edit_level,
b.billing_type, b.pages, b.words
FROM tasks t
INNER JOIN details d ON t.detail_id = d.id
INNER JOIN billing b ON t.billing_id = b.id
INNER JOIN TYPE ty ON d.document_type_id = ty.id
INNER JOIN files f ON t.file_id = f.id
WHERE t.assignee = 'argie1234'";
And this is the array i would like the query result to turn into.
$user = array('allTask'=>array(array('taskid' => 1,
'assignee'=>'argie1234',
'owner'=>'austral1000',
'details' => array( 'code' => 'E',
'status'=>'TC',
'targetCompletionDateUTC'=>'1379401200',
'targetExtentionDateUTC'=>'1379401200',
'submissionDateUTC'=>'1379401200',
'approvalDateUTC'=>'1379401200',
'revisionStartDateUTC'=>'1379401200',
'revisionCompletionDateUTC'=>'1379401200',
'messageToEditor'=>'Please work on it asap.',
'documentType' => 'Thesis'),
'file' => array('orig_name' =>'originalname.docx',
'new_name' => 'newname.docx'),
'billing'=>array('paymentDate'=>'July 26,2013 12:40',
'discount' => '0',
'totalRevisionCharge' => '$20.00',
'totalAmountPayed' => '$20.00',
'revisionLevel' => '1',
'chargeType'=> '1',
'numPages' => '60',
'numWords' => '120,000' ) ),
array('taskid' => 12,
'assignee'=>'argie1234',
'owner'=>'usaroberto',
'details' => array( 'code' => 'E',
'status'=>'TC',
'targetCompletionDateUTC'=>'1379401200',
'targetExtentionDateUTC'=>'1379401200',
'submissionDateUTC'=>'1379401200',
'approvalDateUTC'=>'1379401200',
'revisionStartDateUTC'=>'1379401200',
'revisionCompletionDateUTC'=>'1379401200',
'messageToEditor'=>'Please work on it asap.',
'documentType' => 'Thesis'),
'file' => array('orig_name' => 'originalname.docx',
'new_name' => 'newname.docx'),
'billing'=>array('paymentDate'=>'July 26,2013 12:40',
'discount' => '0',
'totalRevisionCharge' => '$20.00',
'totalAmountPayed' => '$20.00',
'revisionLevel' => '1',
'chargeType'=> '1',
'numPages' => '60',
'numWords' => '120,000' ) ),
'account' => array( 'username' => 'marooon55',
'emailadd' => 'marooon#yahoo.com',
'firstname' => 'Maroon',
'initial' => 'E',
'lastname' => 'Young',
'country' => 'Australia',
'gender' => 'M',
'password' =>'360e2801190744a2af74ef6cbfdb963078b59709',
'activationDate' => '2013-09-13 14:30:34') );
How can i create the above array? I sure know how to define multi dimensional array, regretfully though i am having difficulty creating this complex array dynamically. As a beginner i don't even know where to begin.
Here is an example that might help you out. Try starting with simple multi dimensional arrays, once you get a hold of it, you can move onto building complex ones. You will then find that the array you want to build is not really difficult than you initially thought it to be.
$mycomplexarray = array('key1' => array('val1', 'val2'),
'key2' => array('val3', 'val4' => array('val5', 'val6')
)
);
You could create the array just as you have here. I'm not gonna write the whole thing out, but something like this...
$result = $mysqli->query($query); // however you query the db is up to you.
$row = $result->fetch_assoc(); //same as query use your prefered method to fetch
$user = array('allTask'=>array(array('taskid' => $row['id'],
'assignee'=>$row['assignee'],
'owner'=>$row['owner'],
'details' => array( 'code' => $row['code'],
'status'=>$row['status'],
...etc, Hope this makes sense for you.
Set up a structure array first that defines which columns will be stored in a sub array like
$struc=array('Id'->0, 'assignee'->0, 'owner'->0,
'code'->'detail', 'status'->'detail', 'target_completion_date'->'detail',
'target_extension_date'->'detail', 'submission_date'->'detail', 'approval_date'->'detail',
'revision_start_date'->'detail', 'revision_completion_date'->'detail', 'message'->'detail',
'name'->'file', 'orig_name'->'file', 'new_name'->'file',
'payment_date'->'billing', 'discount'->'billing', 'total_cost'->'billing', 'amount_payed'->'billing', 'edit_level'->'billing', 'billing_type'->'billing', 'words');
In your while ($a=mysqli_fetch_assoc($res)) loop you can now use this structure to decide whether you want to store an element directly in your target array or whether you want to place it in the subarray named in this structure array. Like
$res=mysqli_query($con,$sql);
$arr=array();
while($a=mysqli_fetch_assoc($res)) {
// within result loop: $a is result from mysqli_fetch_assoc()
$ta=array(); // temp array ...
foreach ($a as $k => $v){
if ($struc[$k]) $ta[struc[$k]][$k]=$v;
else $ta[$k]=$v;
}
$arr[]=$ta; // add to target array
}
This is the complete code, no more is needed. It was typed up on my iPod, so it is NOT tested yet.
The generated array should be equivalent to your $user['allTask'] array.

Categories