How to Streamline Some Haphazard Ranking Logic PHP - php

I was given an academic assignment. Basically, I want to take an array named 'rank' through a procedure/function/service to crank out the following outputs in the following fashion:
example 1 : rank = [1,3,2,1] -> [1,4,3,2]
example 2 : rank = [1,1,2,3] -> [1,2,3,4]
example 3 : rank = [2,1,2,3] -> [2,1,3,4]
example 4 : rank = [2,1,2,3] -> [2,1,3,4]
example 5 : rank = [1,1,1,1] -> [1,2,3,4]
Each of the elements in the initial array represent someone getting a place (like 1st place, 2nd place, etc.) in a contest or say crossing a finish line. Each of the elements in the output array basically represent breaking ties. So, say 4 people got first place (in example 5), the first guy/gal gets 1st, the second person gets 2nd, and so on.
Now, I've got the following semi-working code:
<?php
class Ranker
{
private $rank, $result, $doWork;
public function rank() {
/*
rank = [1,3,2,1] -> [1,4,3,2]
rank = [1,1,2,3] -> [1,2,3,4]
rank = [2,1,2,3] -> [2,1,3,4]
rank = [2,1,2,3] -> [2,1,3,4]
rank = [1,1,1,1] -> [1,2,3,4]
*/
$rank = array(
"person1" => 1,
"person2" => 1,
"person3" => 1,
"person4" => 1
);
$result = array();
asort($rank);
/* Get funky */
foreach ($rank as $place) {
$initialplace = $place;
if (!empty($result)) {
{
while(in_array($place, $result)) {
$place++;
if (!in_array($place, $result)) {
break;
}
}
}
foreach (array_keys($rank, $initialplace) as $key) {
if (!array_key_exists($key, $result)) {
$result[$key] = $place;
break;
}
}
} else {
/* array_search returns first match */
$result[array_search($initialplace, $rank)] = $initialplace;
}
}
ksort($result);
/* Printing it out */
foreach ($result as $finalplace) {
echo $finalplace . ' ' . array_search($finalplace, $result) . '</br>';
}
}
}
/* Execute class function */
$ranker = new Ranker;
$doWork = $ranker->rank();
?>
It's semi-working because it doesn't strictly conform to the mapping scheme listed above. For example, it'll take [1,1,1,1] and spit out [4,3,2,1]. This may not really be an issue given that there's some flexibility in the use-case that I need it for. But I do have two questions:
(1) How can I clean up my code and make it way better/more efficient - (I'm not a PHP guy).
(2) How can I change my code to make it conform more precisely to the initial mappings in examples 1-5 above?

Related

How can I do a ranking on array values without ties?

I have an array representing ranks or places (like in a game or contest):
rank = [1,3,2,1]
I want the output to be as follows:
rank = [1,4,3,2]
This means, for any tied place, the tie is broken for each OTHER tying place
and all other subsequent places are also incremented by one. It's a simple mapping assignment.
Other cases:
rank = [1,1,2,3] -> [1,2,3,4]
rank = [2,1,2,3] -> [2,1,3,4]
rank = [2,1,2,3] -> [2,1,3,4]
rank = [1,1,1,1] -> [1,2,3,4]
Try this:
<?php
class Ranker
{
private $rank, $result, $doWork;
public function rank() {
$rank = array(
"slot1" => 1,
"slot2" => 1,
"slot3" => 1,
"slot4" => 1
);
$result = array();
asort($rank);
/* Get funky */
foreach ($rank as $place) {
$initialplace = $place;
if (!empty($result)) {
{
while(in_array($place, $result)) {
$place++;
if (!in_array($place, $result)) {
break;
}
}
}
foreach (array_keys($rank, $initialplace) as $key) {
if (!array_key_exists($key, $result)) {
$result[$key] = $place;
break;
}
}
} else {
/* array_search always returns first match */
$result[array_search($initialplace, $rank)] = $initialplace;
}
}
ksort($result);
/* Printing it out */
foreach ($result as $finalplace) {
echo $finalplace . ' ' . array_search($finalplace, $result) . '</br>';
}
}
}
/* Execute class function */
$ranker = new Ranker;
$doWork = $ranker->rank();
?>
OK so this mostly works, the only deviation from the mappings above is that it'll assign the tie breaks for tying scores a little differently but I don't think that should matter really. For example:
$rank = array(
"slot1" => 1,
"slot2" => 1,
"slot3" => 1,
"slot4" => 1
);
will return:
4 slot1
3 slot2
2 slot3
1 slot4
Instead of [1, 2, 3, 4]. Since they're all tied though and have the same 'initial rank/place/whatever' I don't seem that as being particularly bad - that's to say it's arbitrary how you decide to assign the second guy who got a '1' or the fourth and so on.
In any event that's a business logic decision and since you didn't share what the use case was I can't really say more about that. However, it seems you have some flexibility in how you actually "break" the ties. So this works in the event that your use case and business logic is flexible. Otherwise, you can modify this slightly to conform more closely to the precise mappings listed above.

Count unique value from associative array

First, thanks for any help.
I've spent countless hours on here and other forums trying to find my exact solution but either 1) I'm not understanding the one's I've read or 2)I haven't found the right answer.
In PHP, I've run a somewhat complex query which returns a set of records similar to:
id | name | direction|
1 aaa east
2 bbb west
3 ccc east
I've created an associative array such as:
$query=("select * from foo");
$result=mysql_query($query);
$array=mysql_fetch_assoc($result);
Now, what I need to do seems simple but I'm not grasping the concept for some reason.
I need to loop through the entire $array and return a count of any value that I want to specify and store that count in a variable.
i.e. Show me how many times east shows up in the "direction" column and put that in a variable called $eastcount.
I've tried various combinations of using foreach loops with incremental counts and have tried using array_count_values but have not been able to put the pieces together :/
// build query
$query=("select * from foo");
// execute query
$result=mysql_query($query);
// declare vars
$east_count = 0;
// iterate through results
while ($data = mysql_fetch_array($result)) {
// grab DIRECTION column value
$direction = $data['direction'];
// detect 'east'
if ($direction == 'east') {
// increment 'east' count
$east_count++;
}
}
// print # of times we had 'east'
echo("direction said 'east' $east_count times");
This should work (sorry for the lack of code block I'm on my iPhone).
http://www.php.net/manual/en/function.array-count-values.php
$array = array(1, "hello", 1, "world", "hello");
print_r(array_count_values($array));
Array
(
[1] => 2
[hello] => 2
[world] => 1
)
How about this:
query=("select * from foo");
$result=mysql_query($query);
$directions = array();
while($direction = mysql_fetch_assoc($result) {
$directions[] = $direction['direction'];
}
$directionCounts = array_count_values($directions);
//now you can access your counts like this:
echo $directionCounts['east'];
First, of all you should be using mysqli instead. But, anyhow I hope this makes some sense.
if ($result) {
$count = 0;
while ( $row = mysql_fetch_assoc($result)) {
if ($row["route"] === "east") {
$count += 1;
}
}
return $count;
}

Count result according level

I have Adjacency list mode structure like that and i want to count all title of parent according level like Food = (2,4,3), Fruit = (3,3)
tree tabel structure
after that make tree like that
by this code i m getting right total like for Food =9, Fruit = 6
function display_children($parent, $level)
{
$result = mysql_query('SELECT title FROM tree '.'WHERE parent="'.$parent.'"');
$count = 0;
while ($row = mysql_fetch_array($result))
{
$data= str_repeat(' ',$level).$row['title']."\n";
echo $data;
$count += 1 + $this->display_children($row['title'], $level+1);
}
return $count;
}
call function
display_children(Food, 0)
Result : 9 // but i want to get result like 2,4,3
But i want to get count total result like that For Food 2,4,3 and For Fruit 3,3 according level
so plz guide how to get total according level
function display_children($parent, $level)
{
$result = mysql_query('SELECT title FROM tree '.'WHERE parent="'.$parent.'"');
$count = "";
while ($row = mysql_fetch_array($result))
{
$data= str_repeat(' ',$level).$row['title']."\n";
echo $data;
if($count!="")
$count .= (1 + $this->display_children($row['title'], $level+1));
else
$count = ", ".(1 + $this->display_children($row['title'], $level+1));
}
return $count;
}
Lets try this once..
If you want to get amounts by level, then make the function return them by level.
function display_children($parent, $level)
{
$result = mysql_query('SELECT title FROM tree WHERE parent="'.$parent.'"');
$count = array(0=>0);
while ($row = mysql_fetch_array($result))
{
$data= str_repeat(' ',$level).$row['title']."\n";
echo $data;
$count[0]++;
$children= $this->display_children($row['title'], $level+1);
$index=1;
foreach ($children as $child)
{
if ($child==0)
continue;
if (isset($count[$index]))
$count[$index] += $child;
else
$count[$index] = $child;
$index++;
}
}
return $count;
}
Note that its hard for me to debug the code as i dont have your table. If there is any error let me know and i will fix it.
Anyways result will be array
which should contain amounts of levels specified by indices:
$result=display_children("Food", 0) ;
var_export($result);//For exact info on all levels
echo $result[0];//First level, will output 2
echo $result[1];//Second level, will output 4
echo $result[2];//Third level, will output 3
And by the way there is typo in your database, id 10 (Beef) should have parent "Meat" instead of "Beat" i guess.
If you want to see testing page, its here.
This article has all you need to creates a tree with mysql, and how count item by level
If you don't mind changing your schema I have an alternative solution which is much simpler.
You have your date in a table like this...
item id
-------------+------
Food | 1
Fruit | 1.1
Meat | 1.2
Red Fruit | 1.1.1
Green Fruit | 1.1.2
Yellow Fruit | 1.1.3
Pork | 1.2.1
Queries are now much simpler, because they're just simple string manipulations. This works fine on smallish lists, of a few hundred to a few thousand entries - it may not scale brilliantly - I've not tried that.
But to count how many things there are at the 2nd level you can just do a regexp search.
select count(*) from items
where id regexp '^[0-9]+.[0-9]+$'
Third level is just
select count(*) from items
where id regexp '^[0-9]+.[0-9]+.[0-9]+$'
If you just want one sub-branch at level 2
select count(*) from items
where id regexp '^[0-9]+.[0-9]+$'
and id like "1.%"
It has the advantage that you don't need to run as many queries on the database, and as a bonus it's much easier to read the data in the tables and see what's going on.
I have a nagging feeling this might not be considered "good form", but it does work very effectively. I'd be very interested in any critiques of this method, do DB people think this is a good solution? If the table were very large, doing table scans and regexps all the time would get very inefficient - your approach would make better use of the any indexes, which is why I say this probably doesn't scale very well, but given you don't need to run so many queries, it may be a trade off worth taking.
An solution by a php class :
<?php
class LevelDepCount{
private $level_count=array();
/**
* Display all child of an element
* #return int Count of element
*/
public function display_children($parent, $level, $isStarted=true)
{
if($isStarted)
$this->level_count=array(); // Reset for new ask
$result = mysql_query('SELECT title FROM tree '.'WHERE parent="'.$parent.'"');
$count = 0; // For the level in the section
while ($row = mysql_fetch_array($result))
{
$data= str_repeat(' ',$level).$row['title']."\n";
echo $data;
$count += 1 + $this->display_children($row['title'], $level+1,false);
}
if(array_key_exists($level, $this->level_count))
$this->level_count[$level]+=$count;
else
$this->level_count[$level]=$count;
return $count;
}
/** Return the count by level.*/
public function getCountByLevel(){
return $this->level_count;
}
}
$counter=new LevelDepCount();
$counter->display_children("Food",0);
var_dump($counter->getCountByLevel());
?>
If you modify your query you can get all the data in one swoop and without that much calculations (code untested):
/* Get all the data in one swoop and arrange it for easy mangling later */
function populate_data() {
$result = mysql_query('SELECT parent, COUNT(*) AS amount, GROUP_CONCAT(title) AS children FROM tree GROUP BY parent');
$data = array();
while ($row = mysql_fetch_assoc($result)) {
/* Each node has the amount of children and their names */
$data[$row['parent']] = array($row['children'], int($row['amount']));
}
return $data;
}
/* The function that does the whole work */
function get_children_per_level($data, $root) {
$current_children = array($root);
$next_children = array();
$ret = array();
while(!empty($current_children) && !empty($next_children)) {
$count = 0;
foreach ($current_children as $node) {
$count += $data[$node][0]; /* add the amount */
$next_children = array_merge($next_children, explode($data[$node][1])); /* and its children to the queue */
}
ret[] = $count;
$current_children = $next_children;
$next_children = array();
}
return $ret;
}
$data = populate_data();
get_children_per_level($data, 'Food');
It shouldn't be difficult to modify the function to make a call per invocation or one call per level to populate the data structure without bringing the whole table into memory. I'd suggest against that if you have deep trees with just a few children as it is a lot more efficient to get all the data in one swoop and calculate it. If you have shallow trees with a lot of children, then it may be worth changing.
It would also be possible to put everything together in a single function, but I'd avoid re-calculating data for repeated calls when they are not needed. A possible solution for this would be to make this a class, use the populate_data function as the constructor that stores it as an internal private property and a single method that is the same as get_children_per_level without the first parameter as it would get the data off its internal private property.
In any case, I'd also suggest you use the ID column as a "parent" reference instead of other columns. To start with, my code will break if any of the names contains a comma :P. Besides, you may have two different elements with the same name. For example, you could have Vegetables -> Red -> Pepper and the Red will get slumped together with the Fruit's Red.
Another thing to note is that my code will enter an infinite loop if your DB data is not a tree. If there is any cycle in the graph, it will never finish. That bug could be easily solved by keeping a $visited array with all the nodes that have already been visited and not pushing them into the $next_children array within the loop (probably using array_diff($data[$node][1], $visited).

A Challenge? converting mysql rows to very specific format with php

how to format sql server rows using php that look like this:
id company value monthyear
1 companyone 30 january2012
2 companytwo 20 february2012
3 companyone 10 february2012
into this:
monthyear: ['january2012', 'february2012']
and this:
company: 'companyone', value: [30, 10]
company: 'companytwo', value: [0, 20]
each instance of a month from the db is combined into one instance.
company one, which has two rows, is combined into one instance where each value is lined up in order of the month. company two, which only has one instance, has it's value defined as 0 where it has no instance in a month.
the farthest i've gotten is are two two dimensional array with array_merge_recursive and some conditional statements but then my head goes into knots.
SELECT
company,
GROUP_CONCAT(value SEPARATOR ',') AS value,
GROUP_CONCAT(monthyear SEPARATOR ',') AS monthyear
FROM
yourTable
GROUP BY
company
Some Reference for GROUP_CONCAT.
PHP solution:
Select the to be grouped attribute sorted (company). Loop over them and open a new group every time you encounter a different value for company. As long as the current row has the same row as the previous, add value and monthyear to the current company.
You could do this even without sorting:
while($row = mysql_fetch_assoc($resource))
{
$values[$row["country"]][] = $row["value"];
$monthyear[$row["country"]][] = $row["monthyear"];
}
Some output example
foreach ($values as $country => $valuesOneCountry)
{
// each country
var_dump($country);
foreach ($valuesOneCountry as $i => $value)
{
// value, monthyear for each original row
var_dump($value, $monthyear[$country][$i]);
}
}
Elegant way with OOP:
class Tuple
{
public $country, $values, $monthyears;
public function __construct($country, $values = array(), $monthyears = array())
{
$this->country = $country;
$this->values = $value;
$this->monthyears = $monthyears;
}
}
$tuples = array();
while($row = mysql_fetch_assoc($resource))
{
if (!isset($tuples[$row["country"]]))
$tuples[$row["country"]] = new Tuple($row["country"]);
// save reference for easy access
$tuple = $tuples[$row["country"]];
// or some method like $tuple->addValue($row["value"]);
$tuple->values[] = $row["value"];
$tuple->monthyears[] = $row["monthyear"];
}
var_dump($tuples);

How can I generate a tree structure from a table in a database?

I'm trying to generate a tree structure from a table in a database. The table is stored flat, with each record either having a parent_id or 0. The ultimate goal is to have a select box generated, and an array of nodes.
The code I have so far is :
function init($table, $parent_id = 0)
{
$sql = "SELECT id, {$this->parent_id_field}, {$this->name_field} FROM $table WHERE {$this->parent_id_field}=$parent_id ORDER BY display_order";
$result = mysql_query($sql);
$this->get_tree($result, 0);
print_r($this->nodes);
print_r($this->select);
exit;
}
function get_tree($query, $depth = 0, $parent_obj = null)
{
while($row = mysql_fetch_object($query))
{
/* Get node */
$this->nodes[$row->parent_category_id][$row->id] = $row;
/* Get select item */
$text = "";
if($row->parent_category_id != 0) {
$text .= " ";
}
$text .= "$row->name";
$this->select[$row->id] = $text;
echo "$depth $text\n";
$sql = "SELECT id, parent_category_id, name FROM product_categories WHERE parent_category_id=".$row->id." ORDER BY display_order";
$nextQuery = mysql_query($sql);
$rows = mysql_num_rows($nextQuery);
if($rows > 0) {
$this->get_tree($nextQuery, ++$depth, $row);
}
}
}
It's almost working, but not quite. Can anybody help me finish it off?
You almost certainly, should not continue down your current path. The recursive method you are trying to use will almost certainly kill your performance if your tree ever gets even slightly larger. You probably should be looking at a nested set structure instead of an adjacency list if you plan on reading the tree frequently.
With a nested set, you can easily retrieve the entire tree nested properly with a single query.
Please see these questions for a a discussion of trees.
Is it possible to query a tree structure table in MySQL in a single query, to any depth?
Implementing a hierarchical data structure in a database
What is the most efficient/elegant way to parse a flat table into a tree?
$this->nodes[$row->parent_category_id][$row->id] = $row;
This line is destroying your ORDER BY display_order. Change it to
$this->nodes[$row->parent_category_id][] = $row;
My next issue is the $row->parent_category_id part of that. Shouldn't it just be $row->parent_id?
EDIT: Oh, I didn't read your source closely enough. Get rid of the WHERE clause. Read the whole table at once. You need to post process the tree a second time. First you read the database into a list of arrays. Then you process the array recursively to do your output.
Your array should look like this:
Array(0 => Array(1 => $obj, 5 => $obj),
1 => Array(2 => $obj),
2 => Array(3 => $obj, 4 => $obj),
5 => Array(6 => $obj) );
function display_tree() {
// all the stuff above
output_tree($this->nodes[0], 0); // pass all the parent_id = 0 arrays.
}
function output_tree($nodes, $depth = 0) {
foreach($nodes as $k => $v) {
echo str_repeat(' ', $depth*2) . $v->print_me();
// print my sub trees
output_tree($this->nodes[$k], $depth + 1);
}
}
output:
object 1
object 2
object 3
object 4
object 5
object 6
I think it's this line here:
if($row->parent_category_id != 0) {
$text .= " ";
}
should be:
while ($depth-- > 0) {
$text .= " ";
}
You are only indenting it once, not the number of times it should be indented.
And this line:
$this->get_tree($nextQuery, ++$depth, $row);
should be:
$this->get_tree($nextQuery, $depth + 1, $row);
Note that you should probably follow the advice in the other answer though, and grab the entire table at once, and then process it at once, because in general you want to minimize round-trips to the database (there are a few use cases where the way you are doing it is more optimal, such as if you have a very large tree, and are selecting a small portion of it, but I doubt that is the case here)

Categories