uasort() ? - sorting posts from a bank account - php

How can I sort an array with some posts from a bank account?
I need to sort by three fields: date, amount, accumulated amount
ie.
date | amount | accum_amount
01-01-11 500 500 ('amount' + previous 'accum_amount' = 'accum_amount' => 500 + 0 = 500)
01-01-11 100 600 ('amount' + previous 'accum_amount' = 'accum_amount' => 100 + 500 = 600)
01-02-11 -25 575 ('amount' + previous 'accum_amount' = 'accum_amount' => -25 + 600 = 575)
01-02-11 150 725 ('amount' + previous 'accum_amount' = 'accum_amount' => 150 + 575 = 725)
01-03-11 200 925 ('amount' + previous 'accum_amount' = 'accum_amount' => 200 + 725 = 925)
01-04-11 -25 900 ('amount' + previous 'accum_amount' = 'accum_amount' => -25 + 925 = 900)
btw. the date field is an UNIX timestamp
array(
array(
'time' => 1200000000,
'amount' => 500,
'accum_amount' => 500
),
array(
'time' => 1200000000,
'amount' => 150,
'accum_amount' => 725
),
array(
'time' => 1200000000,
'amount' => 100,
'accum_amount' => 600
),
array(
'time' => 1300000000,
'amount' => 200,
'accum_amount' => 925
),
array(
'time' => 1300000000,
'amount' => -25,
'accum_amount' => 900
),
array(
'time' => 1200000000,
'amount' => -25,
'accum_amount' => 575
)
)

Change your array structure to something like:
$tmp = array(
time => array(1200000000, 1200000000, 1200000000, ...),
amount => array(500, 150, 100, ...),
accum_amount => array(500, 725, 600, ...),
)
and then use array_multisort for sorting (http://php.net/manual/en/function.array-multisort.php) like:
array_multisort($tmp['time'], SORT_ASC, $tmp['amount'], SORT_DESC, $tmp['accum_amount'], SORT_ASC);
It's not very intuitive, but it should work.
Of course you can write a helper function to map the new sorting order on other array structure.
Example:
function mySort($array, $sort) {
$tmp = array();
foreach ($array as $row) {
foreach ($row as $field => $value) {
if (!isset($tmp[$field])) {
$tmp[$field] = array();
}
$tmp[$field][] = $value;
}
}
$params = array();
foreach ($sort as $field => $direction) {
$params[] = $tmp[$field];
$params[] = $direction;
}
$keys = array_keys($array);
$params[] =& $keys;
call_user_func_array('array_multisort', $params);
$result = array();
foreach ($keys as $key) {
$result[$key] = $array[$key];
}
return $result;
}
Call:
$data = mySort($data, array(
'time' => SORT_ASC,
'amount' => SORT_ASC,
'accum_amount' => SORT_ASC
));

function usort() is made for this purpose:
usort($data, "my_sort");
function my_sort($row1, $row2) {
if ($row1['time']<$row2['time']) return -1;
if ($row1['time']>$row2['time']) return 1;
if ($row1['amount']<$row2['amount']) return -1;
if ($row1['amount']>$row2['amount']) return 1;
if ($row1['accum_amount']<$row2['accum_amount']) return -1;
if ($row1['accum_amount']>$row2['accum_amount']) return 1;
return 0;
}

Your example isnt really clear what you want. You don't describe it this way, but from example it appears you want to sort by date increasing, then accum_amount increasing, then amount (increasing?)
You just have to write a comparison function and pass it to uasort along with your array
$comp = function($a,$b)
{
if ($a['date'] != $b['date'])
return $a['date'] - $b['date'];
else if ($a['accum_amount'] != $b['accum_amount'])
return $a['accum_amount'] - $b['accum_amount'];
return $a['amount'] - $b['amount'];
}
uasort($arr, $comp);
You might find a function like this helpful. It will create the comparison function when given an array, in order of importance, of the keys to compare by. (All ascending, support for descending would necessarily complicate it a good deal)
$compMakerAsc = function($l)
{
return function($a, $b) use ($l)
{
foreach($l as $k)
{
if ($a[$k] != $b[$k])
return $a[$k] - $b[$k];
//OR return ($a[$k] > $b[$k]) ? 1 : -1;
}
return 0;
}
$myComp = $compMakerAsc('date', 'accum_amount', 'amount');
$uasort($arr, $myComp);

$key = 'accum_amount';
function sortme($a, $b)
{
global $key;
if ($a[$key] == $b[$key]) {
return 0;
}
return ($a[$key] < $b[$key]) ? -1 : 1;
}
usort($array, "sortme");
perhaps this should help. Bad thing, have to define global variable. Complete code!!!
$array = array(
array(
'time' => 1200000000,
'amount' => 500,
'accum_amount' => 500
),
array(
'time' => 1200000000,
'amount' => 150,
'accum_amount' => 725
),
array(
'time' => 1200000000,
'amount' => 100,
'accum_amount' => 600
),
array(
'time' => 1300000000,
'amount' => 200,
'accum_amount' => 925
),
array(
'time' => 1300000000,
'amount' => -25,
'accum_amount' => 900
),
array(
'time' => 1200000000,
'amount' => -25,
'accum_amount' => 575
)
);
$key = 'accum_amount';
function sortme($a, $b)
{
global $key;
if ($a[$key] == $b[$key]) {
return 0;
}
return ($a[$key] < $b[$key]) ? -1 : 1;
}
usort($array, "sortme");
echo "<pre>";
print_r($array);
echo "</pre>";

Let's combine this from the comments
the query is just as simple as SELECT time, amount, accum_amount FROM table
with this requirement from the original question:
I need to sort by three fields: date, amount, accumulated amount
Now, there were no requirements stated as to whether the sort is ascending or not. Let's assume ascending.
SELECT time, amount, accum_amount
FROM table
ORDER BY time ASC, amount ASC, accum_amount ASC
This will give us all of the data first sorted by time. When the time is identical, it will sort second by amount, lowest first and highest last. When the time and amount are identical, it will sort third by the accumulated amount, again sorted lowest first, highest last.
Changing the order of operations with the sort is as simple as swapping around the list in the ORDER BY clause. Going from largest to smallest is as easy as switching from ASC to DESC.
Note how relatively easy this method is in comparison to sorting in PHP. Whenever you can, let your database give you pre-sorted results.
Because I need to leave the site for a few hours, I'm just going to type this up real quick. I'm operating right now under the assumption that you want to recalculate the accumulated value.
Our query is now:
SELECT time, amount
FROM table
ORDER BY time ASC, amount DESC
We'll be a friendly bank and perform all deposits (increases in amount) first, then all debits (decreases in amount) last, processing the largest debits later than the smaller debits.
Given $data is our array:
$running_total = 0;
foreach($data as $index => $row) {
$data[$index]['accum_amount'] = $running_total + $row['amount'];
$running_total = $data[$index]['accum_amount'];
}
This should successfully rebuild the accum_amount list, given the business rules I have outlined above.

I have found another way to do it before the data is put into the db

Related

get the least Frequent item from array of objects

I'm trying to output [device_id] of the least frequent [device_ip_isp] from this array.
Also, If the array only has two SharedDevice Object available, and having different [device_ip_isp], it should output the 2nd SharedDevice Object's [device_id]
array (
0 =>
SharedDevice::__set_state(array(
'device_no' => 1,
'device_id' => '82',
'device_ip_isp' => 'Verizon Fios',
)),
1 =>
SharedDevice::__set_state(array(
'device_no' => 2,
'device_id' => '201',
'device_ip_isp' => 'Spectrum',
)),
2 =>
SharedDevice::__set_state(array(
'device_no' => 3,
'device_id' => '312',
'device_ip_isp' => 'Verizon Fios',
)),
3 =>
SharedDevice::__set_state(array(
'device_no' => 4,
'device_id' => '9715',
'device_ip_isp' => 'Verizon Fios',
)),
4 =>
SharedDevice::__set_state(array(
'device_no' => 5,
'device_id' => '11190',
'device_ip_isp' => 'Verizon Fios',
)),
)
The output should be 201 because "Spectrum" is the least frequent.
I tried the following and had issues:
I'm not sure how I can sort the object variables before comparing to find the least frequent.
/*
$user->getUser_devices() will output the array shown above.
*/
leastFrequent($user->getUser_devices(), 5);
function leastFrequent($arr, $n){
// find the min frequency
// using linear traversal
$min_count = $n + 1;
$res = -1;
$curr_count = 1;
for ($i = 1; $i < $n; $i++) {
if ($arr[$i]['device_ip_isp'] == $arr[$i - 1]['device_ip_isp']) {
$curr_count++;
} else {
if ($curr_count < $min_count) {
$min_count = $curr_count;
$res = $arr[$i - 1]['device_id'];
}
$curr_count = 1;
}
}
// If last element is
// least frequent
if ($curr_count < $min_count) {
$min_count = $curr_count;
$res = $arr[$n - 1]['device_id'];
}
return $arr[$n]$res['device_id'];
}
Ok, below is the explanation of the snippet inside out.
array_column to get all the device_ip_isp values in a single array.
array_count_values to get the frequency of each value.
array_reverse to reverse the count frequency array since you need the latest share device ID incase of count collision for a minimum count value.
min to get the lowest value among all the frequency counts.
array_search to get the key of the first frequency min element.
In the end, we reverse the input array to immediate return the device IP the moment we find the key from the above step matching the current device_ip_isp.
Snippet:
<?php
function getLeastFrequentElement($arr){
$freqs = array_reverse(array_count_values(array_column($arr, 'device_ip_isp')));
$deviceIPISP = array_search(min($freqs), $freqs);
foreach(array_reverse($arr) as $sharedDevice){
if($sharedDevice->device_ip_isp === $deviceIPISP){
return $sharedDevice->device_id;
}
}
throw new Exception("No shared object found!");
}
echo getLeastFrequentElement($arr);
Online Demo

Generate multi dimensional PHP array using range

The job to be done is show the price of postage per KG. So starting at 1KG, I want to increase by 0.50 for every KG.
I tried doing it like this which doesn't seem to work for me:
$shipping_second_class = array (
array ('weight' => range(1,5), 'cost' => range(1,3, 0.50))
);
foreach ($shipping_second_class as $shipping_second_class) {
echo('weight '.$shipping_second_class['weight'].' costs £'.$shipping_second_class['cost'].'<br/>');
}
That doesn't seem to work. What I'm trying to do in a way that's easier to maintain is something like this, but with less code:
$shipping_second_class = array (
array ('weight' => '1', 'cost' => '1'),
array ('weight' => '2', 'cost' => '1.5'),
array ('weight' => '3', 'cost' => '2'),
array ('weight' => '4', 'cost' => '2.5'),
array ('weight' => '5', 'cost' => '3'),
);
foreach ($shipping_second_class as $shipping_second_class) {
echo('weight '.$shipping_second_class['weight'].' costs £'.$shipping_second_class['cost'].'<br/>');
}
Another simple way
$per_kg_extra_cost = 0.5;
foreach (range(0, 4) as $shipping) {
$cost = 1 + ( $shipping * $per_kg_extra_cost );
$weight = ++$shipping;
echo '<pre>';
echo 'Weight: '. $weight . ' cost : ' .$cost;
}
Here's one way to do this:
$shipping_second_class = array (
array ('weight' => 1, 'cost' => 1)
);
for($x = 2; $x < 6; $x++){
$newdata = ['weight' => $x, 'cost' => 0.5 + $x * 0.5];
array_push($shipping_second_class, $newdata);
}
foreach ($shipping_second_class as $s) {
echo('weight '.$s['weight'].' costs £'.$s['cost'].'<br/>');
}
Essentially, you need to add to the array after it's been called rather than initialize it fully filled. You start with the array at 1 value (you could modify this code to start with an empty array) and it expands from there. If you need to add more to the array, just increase the 6 to (Desired Number) + 1 in the for loop.
I'm not sure if there is a way to add to the array in the manner you do in the first block of code.
Here is a simple way to do it:
$key = -1;
$interval = 0.5;
$shipping_second_class = array_map(function($v) use (&$key,$interval) {
$key++;
return ["weight"=>$v,"cost"=>(1 + ($interval * $key))];
},range(1,5));
echo json_encode($shipping_second_class);
You can change the $interval variable if you need to change the way it increments cost

An algorithm in PHP to select a subset of the first exactly N elements that add up to X threshold

This is the input:
$deals = array(
array('deal' => '1', 'deal_date' => '2017-02-13', 'amount' => '400'),
array('deal' => '2', 'deal_date' => '2017-04-17', 'amount' => '8900'),
array('deal' => '3', 'deal_date' => '2017-04-23', 'amount' => '1000'),
array('deal' => '4', 'deal_date' => '2017-06-02', 'amount' => '2400'),
array('deal' => '5', 'deal_date' => '2017-07-05', 'amount' => '10500'),
);
I am searching for a subset of exactly N elements where the sum of the 'amount' properties is greater then or equal to X and the elements have the lowest 'deal_date' property possible.
If there is no subset that fits the rules:
$subset = false;
So for N=2 and X=10000, I get this output:
$subset = array(
array('deal' => '2', 'deal_date' => '2017-04-17', 'amount' => '8900'),
array('deal' => '4', 'deal_date' => '2017-06-02', 'amount' => '2400'),
);
for N=3 and X=12000:
$subset = array(
array('deal' => '2', 'deal_date' => '2017-04-17', 'amount' => '8900'),
array('deal' => '3', 'deal_date' => '2017-04-23', 'amount' => '1000'),
array('deal' => '4', 'deal_date' => '2017-06-02', 'amount' => '2400'),
);
My current idea entails creating an array that contains arrays of the list of deals in every conceivable order. Then I scan through those for my list of deals that fit the criteria but then I have a list of deals and I am unsure how to determine the 'earliest'.
I'm also looking for the algorithm with the lowest time complexity.
Any ideas would be appreciated.
It is not simple but roughly this
There will be a better way.
The approximate source is
$calculate = 0;
$end_case = array();
$sum = 10000;
// first amount 10000 over value array remove
foreach($deals as $key => $val){
if($val['amount']>=$sum)unset($deals[$key]);
}
// second Obtaining data
foreach($deals as $key => $val){
foreach($deals as $k => $v){
// same deal excetpion
if($val['deal']==$v['deal']) continue;
$calc = deal_sum($val['amount'],$v['amount']);
if($calc>=$sum){
echo "#".$v['deal']." => ".$val['amount']."+".$v['amount']." = ".$calc."\n";
array_push($end_case,$v['deal']);
}
print_r($end_case);
}
}
function deal_sum($source,$destination){
$result = $source+$destination;
return $result;
}
You should keep in mind the time complexity of your algorithm, otherwise large input sets will take forever.
Algorithm for N:
Returns array() of the first $number_of_deals_to_combine deals from
$deals ordered by 'deal_date', that have at least $threshold or
greater combined amounts or false if no such combination is found.
Uses a rolling window of $number_of_deals_to_combine elements
excluding deals that have too small 'amount'.
function combine_deals($deals, $threshold, $number_of_deals_to_combine){
$return = false;
//are there enough deals to combine?
if(count($deals) >= $number_of_deals_to_combine){
//are deals sorted by date? sort them
usort($deals,function($a, $b){return strnatcmp($a['deal_date'],$b['deal_date']);});
//find out if there is a possible solution, by adding up the biggest deals
$amounts = array_map('intval',array_column($deals,'amount'));
rsort($amounts); //sort descending
if(array_sum(array_slice($amounts, 0, $number_of_deals_to_combine)) >= $threshold){
//find the smallest possible number that is part of a solution
$min_limit = $threshold - array_sum(array_slice($amounts, 0, $number_of_deals_to_combine - 1));
//rolling window
$combined = array();
foreach($deals as $deal){
if(intval($deal['amount']) < $min_limit){continue;} //this deal is not part of any solution
$combined[] = $deal;
//keep the number of deals constant
if(count($combined) > $number_of_deals_to_combine){
$combined = array_slice($combined, count($combined) - $number_of_deals_to_combine);
}
//there are enough deals to test
if(count($combined) == $number_of_deals_to_combine){
$amount = array_sum(array_map('intval',array_column($combined, 'amount')));
if($amount >= $threshold){
$return = $combined;
break;
}
}
}
}
}
return $return;
}
$threshold = 10000;
$number_of_deals_to_combine = 2;
$result = combine_deals($deals, $threshold, $number_of_deals_to_combine);
Are the amount fields always integers? If not, replace all intval with floatval everywhere.

find and sort array by lowest value in multidimensional array

I have an array that contains a number of arrays. Like this:
array{
array{
id => 1
name => place_1
lat => 56.1705
lon => 10.2010
distance => 1.545
}
array{
id => 14
name => place_14
lat => 56.1715
lon => 10.2049
distance => 1.765
}
//etc etc
}
I need to sort the arrays within the array by distance, from low to high - or at least get the position of the lowest distance in the array (like $array[1][4] == 1.765).
I have done something similar before. Then I did it with a function like this:
function sort_by_dist($a, $b){
return $a['distance'] - $b['distance'];
}
usort($array, 'sort_by_dist');
However, this will in this case only return bool(true)
I have no idea why it acts this way.
I know this question has probably been asked (and answered) before, but as a non-native speaker of English I am a bit at a loss as to what I should search for.
Thank you for your help!
My answer just reformats your function a little to make it more explicit what is going on:
$a = array(
array(
'id' => 14,
'name' => 'place_14',
'lat' => 56.1715,
'lon' => 10.2049,
'distance' => 1.765,
),
array(
'id' => 1,
'name' => 'place_1',
'lat' => 56.1705,
'lon' => 10.2010,
'distance' => 1.545,
),
);
usort($a, function($a, $b) {
$d1 = $a['distance'];
$d2 = $b['distance'];
if ($d1 == $d2) {
return 0;
}
return ($d1 < $d2) ? -1 : 1;
});
// the array $a is sorted.
print_r($a);
The input array to usort is sorted, usort will return false if the sorting failed and true otherwise.
You can use array_multisort by iterating thought the array like so:
function sort_by(&$array, $subkey ) {
foreach ($array as $subarray) {
$keys[] = $subarray[$subkey];
}
array_multisort($keys, SORT_ASC, $array);
}
sort_by($coords, 'distance');// $coords is now sorted by distance
$coords is your multidimensional array.

How to Efficently count user's rank by amout of likes?

I have an array, that looks like this,
$users = array(
0 => array(
'user_id' => 'user_1',
'likes' => 50,
),
1 => array(
'user_id' => 'user_2',
'likes' => 72
),
2 => array(
'user_id' => 'user_3',
'likes' => 678
)
);
All I want to do is to implement ratings system according to amount of likes, so that it would look like as:
#rank 1 - user_3
#rank 2 - user_2
#rank 3 - user_1
I ended up with,
$user_counter = new User_Counter();
$user_counter->setData($users);
echo $user_counter->fetchRankByLikes(678);
class User_Counter
{
protected $array;
public function setData(array $array)
{
$this->array = $this->sort($array);
}
protected function sort(array $array)
{
$result = array();
foreach ($array as $index => $_array) {
$result[] = $_array['likes'];
}
// This will reset indexes
sort($result, SORT_NUMERIC);
$result = array_reverse($result);
$return = array();
$count = 0;
foreach ($result as $index => $rank) {
$count++;
$return[$count] = $rank;
}
$return = array_unique($return);
return $return;
}
public function getAll()
{
return $this->array;
}
public function fetchRankByLikes($likes)
{
$data = array_flip($this->array);
return $data[$likes];
}
public function fetchLikesByRank($rank)
{
return $this->array[$rank];
}
}
My problem is that, this approach lies sometimes - for example it gives incorrect info when there are no likes at all ( === i.e all members have 0 likes) - in that case it gives first rank to them all.
Is there another efficient approach to count user's rating by amount of their likes?
Or what am I doing wrong in my computations?
Thanks.
If you're looking for efficiency, I would look at the usort() function, native to PHP:
http://us1.php.net/manual/en/function.usort.php
What usort does is take an array and iteratively walk through it, providing an outside function with 2 items from it's input at a time. It then waits for the function to return either 1, -1 or 0 and determines the following truths:
-1 means left < right
+1 means left > right
0 means both parameters are equal
Here's a practical example:
$users = array(
0 => array(
'user_id' => 'user_1',
'likes' => 50,
),
1 => array(
'user_id' => 'user_2',
'likes' => 72
),
2 => array(
'user_id' => 'user_3',
'likes' => 678
)
);
usort($users, "sortLikesAscending");
function sortLikesAscending($a, $b) {
if ($a['likes'] > $b['likes']) {
return 1;
} elseif ($a['likes'] < $b['likes']) {
return -1;
} else {
return 0;
}
}
Hope that helps!
EDIT:
If you want to implement this usort() methodology from inside your User_Counter class, then call usort like this:
// From somewhere inside User_Counter, assumes User_Counter::mySortingFunction() is defined
usort($this->array, array(&$this, "mySortingFunction"));
The function callback is passed as an array with two entries: a &reference to an object containing the function and the function name as a string.

Categories