My question might be a bit vague, because I cannot quite figure it out.
I have a piece of PHP that tries to convert a mysql query result into an array "tree".
I.e. arrays of arrays depending on the defined groups.
The code assumes that a column name would start with a double underscore __ to indicate grouping and the results will already be ordered by the grouping.
The code works , but in certain cases it slows down to unusable speeds.
Cases which I would expect it to be fast. Only one grouping with only a few unique values and many items in each branch sometimes takes upto 30seconds.
Where other cases with many layers of branches and many different values , it only takes 1 second. (The result set is usually around 20 000 rows)
So, my question I guess is simply, what is wrong with my code ? Where am messing up so bad that it would impact performance significantly.
P.S. I'm a relative php novice , so be gentle :)
Sorry, no code comments O_o
$encodable = array();
$rownum = 0;
$branch = null;
$row = null;
$first = true;
$NULL = null;
$result = mysql_query($value,$mysql);
error_log (date("F j, Y, g:i a")."\r\n",3,"debug.log");
if (gettype($result) == "resource")
{
while($obj = mysql_fetch_object($result))
{
$newrow = true;
$branch = &$encodable;
$row = &$NULL;
if (count($branch) > 0)
{
$row = &$branch[count($branch)-1];
}
foreach ($obj as $column => $value)
{
if ($column[0] == '_' && $column[1] == '_')
{
$gname = substr($column,2);
if (isset($row[$gname]) && $row[$gname] == $value)
{
$branch = &$row["b"];
$row = &$NULL;
if (count($branch) > 0)
{
$row = &$branch[count($branch)-1];
}
}
else
{
$branch[] = array();
$row = &$branch[count($branch)-1];
$row[$gname] = $value;
$row["b"] = array();
$branch = &$row["b"];
$row = &$NULL;
if (count($branch) > 0)
{
$row = &$branch[count($branch)-1];
}
}
}
else
{
if ($newrow)
{
$branch[] = array();
$row = &$branch[count($branch)-1];
$newrow = false;
}
$row[$column] = $value;
}
}
$rownum++;
}
}
$encoded = json_encode($encodable);
EDIT:
A sample output - the resulting arrays is converted to json.
This small set is grouped by "av" , b is created by the code for each branche and then contains a list of the [hid , utd] records per AV.
[{"av":"eset nod","b":[{"hid":"3","utd":"1"}]},{"av":"None","b":[{"hid":"2","utd":"0"},{"hid":"4","utd":"0"},{"hid":"5","utd":"0"},{"hid":"1","utd":"0"}]}]
The actual sql result that produced this result is:
+----------+-----+-----+
| __av | hid | utd |
+----------+-----+-----+
| eset nod | 3 | 1 |
| None | 2 | 0 |
| None | 4 | 0 |
| None | 5 | 0 |
| None | 1 | 0 |
+----------+-----+-----+
Turns out its all the calls to count($branch).
Apparently calling a function that doesnt expect a variable by reference like count , With a variable by reference , causes the function to make a Copy of the variable to operate on.
In my case arrays with thousands of elements. Which also explains why its the results with few (but large branches) are the ones that suffer the most.
See this thread:
Why is calling a function (such as strlen, count etc) on a referenced value so slow?
Related
I have a code in which foreach inside another foreach.
$order_id = '1,1,8';
$order_no_first= 'F,SH,C';
$order_id1 = explode(",", $order_id);
$order_no_first1 = explode(',', $order_no_first);
foreach($order_id1 as $ord_id){
foreach($order_no_first1 as $ord_no_first){
if($ord_id != '') {
$this->receipt->chageBagStatus($ord_id, $ord_no_first);
$add = $this->receipt->addJobOrderNew($ord_id, $ord_no_first, $bag_no);
}
}
}
Now the above code iterates 3 times resulting 9 rows in mysql.
//Current Output
order_id orderr_no_first
-------- ---------------
8 C
8 SH
8 F
1 C
1 SH
1 F
1 C
1 SH
1 F
The above output is wrong. I want the output as below,
//Required Output
order_id orderr_no_first
-------- ---------------
8 C
1 SH
1 F
I know it's because am using nested foreach. but I don't know how to solve this issue. Is there any solution. Thankyou.
just use one foreach like this,
foreach($order_id1 as $key => $ord_id){
if($ord_id != '') {
$this->receipt->chageBagStatus($ord_id, $order_no_first1[$key]);
$add = $this->receipt->addJobOrderNew($ord_id, $order_no_first1[$key], $bag_no);
}
}
I hope this will work for you
$order_id = '1,1,8';
$order_no_first= 'F,SH,C';
$order_id1 = explode(",", $order_id);
$order_no_first1 = explode(',', $order_no_first);
rsort($order_id1);
asort($order_no_first1);
$i = 0;
foreach($order_id1 as $ord_id){
echo $ord_id." ".$order_no_first1[$i]."<br/>";
$i++;
}
`
I'm struggling with (probably simple) array shuffling/generating algorithm.
I'm creating simple backend (PHP/MySQL) for a roulette wheel game (JS/HTML). The roulette doesn't have numeric values as usual instead there are 4 prizes user can win, distributed within 12 segments of a roulette wheel.
I need an array like this:
// Note that PRIZE4 is listed only once as it is valuable prize.
// PRIZE3 is less valuable so it is listed twice, etc.
// Prizes are skipping each other so you should never see two identic prizes next each other.
var items = [PRIZE1, PRIZE2, PRIZE1, PRIZE2, PRIZE1, PRIZE2, PRIZE1, PRIZE2, PRIZE1, PRIZE3, PRIZE4, PRIZE3];
And I have prizes in a SQL table like this:
+----+------------+--------------+
| id | name | giveaway_cap |
+----+------------+--------------+
| 1 | PRIZE1 | 255 |
| 2 | PRIZE2 | 300 |
| 3 | PRIZE3 | 30 |
| 4 | PRIZE4 | 15 |
+----+------------+--------------+
4 rows in set (0.00 sec)
Column giveaway_cap determines how many of each prize can be won (I'm storing these counts in different table) but could be used as weight of each prize.
I need some algorithm (preferably PHP) which will generate an array as described above based on this table.
Thanks.
I found this really nice algorithm searching on SO. It generates random numbers based on weight. Using it, one approach to your problem could be as follows:
// This is the function that generates random numbers based on weigth.
function getRandomWeightedElement(array $weightedValues) {
$rand = mt_rand(1, (int) array_sum($weightedValues));
foreach ($weightedValues as $key => $value) {
$rand -= $value;
if ($rand <= 0) {
return $key;
}
}
}
$items = [ "PRIZE1" => 255, "PRIZE2" => 300, "PRIZE3" => 30, "PRIZE4" => 15];// Array of available prizes. It can be retrieved from the DB.
$total = (int) array_sum($items);// Total. I use it to work out the weight.
$items_w = [ "PRIZE1" => (255 / $total) * 1000, "PRIZE2" => (300 / $total) * 1000, "PRIZE3" => (30 / $total) * 1000, "PRIZE4" => (15 / $total) * 1000];// find out the weight somehow. I just divide number of available items by the total.
$res = [];
$previous = NULL;
while ( count(array_diff(array_keys($items), $res))) {// Loop until the result has all available prizes.
$res = [];
for ($i = 0; $i < 12; $i++) {
while ($previous == ($new = getRandomWeightedElement($items_w))) {}// Two consecutive prizes of the same type aren't allowed.
$res[] = $new;
$previous = $new;
}
}
echo implode(',', $res);
It's just a solution, I'm sure there are multiple ways of solving the problem.
Note: I'm using php 5.4 short array syntax, if your PHP version is lower than PHP 5.4, substitute [] by array(). Also have in mind that there can be problems in certain situations such as there's only one type of prize left or if it's impossible to create an array of prizes without two consecutive prizes being the same. You'd have to control those situations anyhow.
Hope it helps.
<?php
$connect=mysqli_connect("localhost","my_user","my_password","my_db");
$sql="SELECT id,name,giveaway_cap FROM table";
$result=mysqli_query($connect,$sql);
$prizes = array();
while($row=mysqli_fetch_array($result)) //iterate 4 times on caps != to 0
{
$count = $row['giveaway_cap'];
if($count != '0')
{
$prizename = $row['name'];
$prizes[$prizename]=$count;
}
}
$prizewheel = array();
foreach ($prizes as $prize=>$value) // iterate 600 times if caps !=0
{
$prizewheel = array_merge($prizewheel, array_fill(0, $value, $prize));
}
$finalprize=array();
$f = 0;
while($f < 12) //iterate 12 times is # of caps >= 12,final array
{
$prizename = $prizewheel[array_rand($prizewheel)];
$finalprize[] = $prizename;
$f++;
}
?>
You could generate a random array like this:
$rows = array(
array(
'id' => 1,
'name' => 'PRIZE1',
'giveaway_cap' => 255,
),
array(
'id' => 2,
'name' => 'PRIZE2',
'giveaway_cap' => 300,
),
array(
'id' => 3,
'name' => 'PRIZE3',
'giveaway_cap' => 30,
),
array(
'id' => 4,
'name' => 'PRIZE4',
'giveaway_cap' => 15,
),
);
$output = array();
foreach ($rows as $row) {
for ($i = 0; $i < $row['giveaway_cap']; $i++) {
$output[] = $row['name'];
}
}
shuffle($output);
//to get the next prize
$nextPrizeKey = array_rand($output);
$nextPrize = $output[$nextPrizeKey];
//remove the won prize
unset($output[$nextPrizeKey]);
What this does is creates a separate element in the array for each of the prizes giveaway_caps. Then as prizes are won you reduce the number of the giveaway_cap and this will reduce the chance that a user will hit that prize again.
If you wanted the array to be static you could save it after the initial generation, either in a database or caching (APC, Memcahche). Then you would just remove each entry that the users land on until there are none left. You could get the next prize with array_rand and then remove the key from the array.
I hope this helps.
Mic
I need help writing the logic of the php script which sorts data into a certain format...
Firstly the script needs to loop through each s1 value and ping an endpoint to get the ml values (more like) which are actually referencing other s1 records. this is the easy part! the data is returned like so;
Table 1
s1 | ml
----------
1 | -
2 | 3,4
3 | 2,8,9
4 | -
5 | 2
6 | 1
7 | 10
8 | -
9 | -
10 | -
Condition 1: As you can see the endpoint returns data for the s1 value telling it other s1 records are similar to other ones, but the direction of ml is not always bidirectional. sometimes, like when s1=6 the ml value is 1 however when s1=1 there isn't a ml value.
Condition 2: Again just to explain the ml records, look above and below where s1=5 (above) and where s1=2 + rec=5 (below), this script needs to realise there is already an s1 record for it's value and that it should be added there.
Condition 3: Note how when s1=2,ml=3 this is stored but when s1=3,ml=2 this is ignored because we have the reverse record.
I basically want to match all the data into 1 sorted 'profile' so it ends up in the below format which i will store in another db table of 'sorted' records.
Table 2
s1 | rec
----------
2 | 3
2 | 4
2 | 8
2 | 9
2 | 9
2 | 5
6 | 1
7 | 10
This has been racking my brains for days now, I need something thats efficient because in the end it will deal with millions of records and I'm sure there is an easy solution but i just can't figure how to start it.
I tried the following, but I'm stuck and don't know how to go further;
public function getrelated($id='', $t=''){
if($id != ""){
$get = Easytest::where('s1','=',$id)->get();
if(count($get) > 0){
$ret= array();
foreach($get as $go){
$v = explode(",", $go->s2);
foreach ($v as $e) {
if($e != $t){
$ret[$e] = $this->getrelated($e, $id);
}
}
}
if(count($ret) > 0){
return $ret;
}else{
return "";
}
}else{
return $id;
}
}else{
return "";
}
}
public function easytest(){
ob_start();
$a = array(
array("s1"=>1,"s2"=>implode(",",array()).""),
array("s1"=>2,"s2"=>implode(",",array(3,4)).","),
array("s1"=>3,"s2"=>implode(",",array(2,8,9)).","),
array("s1"=>4,"s2"=>implode(",",array()).""),
array("s1"=>5,"s2"=>implode(",",array(2)).","),
array("s1"=>6,"s2"=>implode(",",array(1)).","),
array("s1"=>7,"s2"=>implode(",",array(10)).","),
array("s1"=>8,"s2"=>implode(",",array()).""),
array("s1"=>9,"s2"=>implode(",",array()).""),
array("s1"=>10,"s2"=>implode(",",array()).""),
array("s1"=>11,"s2"=>implode(",",array(12)).","),
array("s1"=>12,"s2"=>implode(",",array(2)).",")
);
//return Easytest::insert($a);
$records = Easytest::all();
foreach ($records as $record) {
$id = $record->s1;
echo "ROW: ".$id." > ";
$record->s2 = ltrim($record->s2,",");
$ml = explode(",",$record->s2);
if(count($ml) >= 1){
foreach ($ml as $t) {
echo "RESULT: ".$t." -".print_r($this->getrelated($t, $id), true);
echo ",\n";
}
}
echo " <br><br>\n\n";
}
return ob_get_clean();
}
Ok, so I eventually solved this... esentially this is the code below;
improvements welcome :)
You need to call the function like so
related(array('searched'=>array(),'tosearch'=>array(13)));
function:
public function related($input){
$searched = $input['searched'];
$ar = array();
$bits = array();
if(count($input['tosearch']) != 0){
$get = Easytest::orWhere(function($query) use ($input)
{
foreach ($input['tosearch'] as $k=>$v) {
$query->orWhere('s2', 'LIKE', '%,'.$v.',%')->orWhere('s1', '=', $v);
}
})
->orderBy('s1', 'ASC')->get();
foreach ($input['tosearch'] as $k=>$v) {
unset($input['tosearch'][$k]);
$input['searched'][$v] = $v;
}
foreach ($get as $result) {
$thesebits = explode(",", trim($result->s2,","));
foreach ($thesebits as $smallbit) {
if($smallbit != ""){
$bits[] = $smallbit;
}
}
$bits[] = $result->s1;
$bits = array_unique($bits);
foreach ($bits as $k=>$v) {
if(($key = array_search($v, $input['searched'])) == false) {
$input['tosearch'][$v] = $v;
}else{
unset($input['tosearch'][$v]);
}
}
$input['tosearch'] = array_unique($input['tosearch']);
}
return $this->related($input);
}else{
return $input;
}
}
I have a database which has 6 column A B C D E X, for each combination of ABCDE I have a different value of X.
I need a way to search through, that will allow all values of X for different combinations (for example all X when A=1, or all X when A=1 and B=2 etc)
My thought was to translate it into a 5-D array which looks like this:
Array[A][B][C][D][E]=X;
But now I'm trying to extract sub arrays, when I don't know how may of the dimensions will be constant. So I need to be able to extract all value of X for Array[1][5][][][] or Array[2][4][5][][]… etc.
And I'm totally stuck.
I'm trying to do 6 loops one inside another but I don't know how to handle those that are constant.
Help with ideas will be very very helpful.
Edit
Database:
A B C D E X
1 1 1 1 1 53
1 1 2 3 2 34
2 1 1 4 2 64
Turned it into an array:
Array[1][1][1][1][1]=53
Array[1][1][2][3][2]=34
For
Input: A=1
Output 53,34
Input A=1,B=1,C=1
Output: 53,
etc
Try this then
<?php
$arr = array();
$result = mysql_query("SELECT A,B,C,D,E,X FROM table_name ORDER BY A ASC,B ASC,C ASC,D ASC,E ASC");
if(mysql_num_rows($result) > 0) {
while($row = mysql_fetch_assoc($result)) {
array_push($arr,$row);
}
}
function search($arr,$values) {
$return = array();
foreach($arr AS $key => $value) {
$ok = true;
foreach(array('A','B','C','D','E') AS $letter) {
if(array_key_exists($letter,$values)) {
if($value[$letter] != $values[$letter]) {
$ok = false;
break;
}
}
}
if($ok) array_push($return,$value['X']);
}
return (($return) ? implode(',',$return) : false);
}
echo '<pre>';
print_r(search($arr,array('A' => 1)));
echo '</pre>';
?>
I'd like to convert base 10 numbers to base 31
I would like to use only these characters: 23456789abcdefghjkmnpqrstuvwxyz
As you can see, 5 characters are excluded (i don't need these): 1 0 o l i
The function I have now is below but of course it doesn't work. When 2 is input it outputs 4. The output for tenTo31(2) should be 2
function tenTo31($num)
{
$out = "";
$alpha = "23456789abcdefghjkmnpqrstuvwxyz";
while($num > 30)
{
$r = $num % 31;
$num = floor($num / 31) - 1;
$out = $alpha[$r] . $out;
}
return $alpha[$num] . $out;
}
Any ideas on how to make this work?
This is a blind guess at what you want:
$alpha = "yz23456789abcdefghjkmnpqrstuvwx";
There's a built-in function for converting from one base to another, base_convert(). The alphabet is fixed, but you can use strtr() to replace those digits with your own.
"The output for tenTo31(2) should be 2": One possibility is to make '2' the third symbol again.
function tenTo31($num) {
static $from = "0123456789abcdefghijklmnopqrstu";
static $to = "yz23456789abcdefghjkmnpqrstuvwx";
return strtr(base_convert($num, 10, 31), $from, $to);
}
for($i=0; $i<31; $i++) {
echo $i, '=', tenTo31($i), ' | ';
if ( 9===$i%10 ) echo "\n";
}
prints
0=y | 1=z | 2=2 | 3=3 | 4=4 | 5=5 | 6=6 | 7=7 | 8=8 | 9=9 |
10=a | 11=b | 12=c | 13=d | 14=e | 15=f | 16=g | 17=h | 18=j | 19=k |
20=m | 21=n | 22=p | 23=q | 24=r | 25=s | 26=t | 27=u | 28=v | 29=w |
30=x |
edit:
To convert the base(31) number back to decimal you first have to reverse the translation (strtr) and then call base_convert(.., 31, 10). You can combine the conversion from and to base(31) in a single function.
function convert_ten_31($num, $numIsDecimal) {
static $default = "0123456789abcdefghijklmnopqrstu";
static $symbols = "yz23456789abcdefghjkmnpqrstuvwx";
if ( $numIsDecimal ) {
return strtr(base_convert($num, 10, 31), $default, $symbols);
}
else {
return base_convert(strtr($num, $symbols, $default), 31, 10);
}
}
// testing
for($i=0; $i<10000; $i++) {
$x = convert_ten_31($i, true);
$x = convert_ten_31($x, false);
if ( $i!==(int)$x ) {
var_dump($i, $x);
die;
}
}
echo 'done.';
It's also easily possible to write a function like base_convert() yourself that take the symbols as parameter and thus having one flexible function instead of tenTo30(), tenTo31(), tenTo32(), ....
You aren't using the 1 and 0 characters, the first digit in your numbering system is 2 meaning 2 is the equivalent of 0 in base 10. 3 is equivalent to 1 in base 10 and 4 is equivalent to 2 in base 10.
Why are you taking modules by 32? You should use %31 and /31. In base 10 we are using modules by 10, so should be in base 31. But if we forget about this, I think your logic is correct. I can't understand why 2 in base 10 is equal to 4 in base 31 using your "modified digits".
While I'd encourage you to continue along with your algorithm for the learning exercise, consider using base_convert if you just need to get the job done.
The mapping according to http://www.crockford.com/wrmg/base32.html appears to be:
function symbolToEncode ($num) {
$out = "";
static $alpha = "0123456789ABCDEFGHJKMNPQRSTVWXYZ*~$=U";
while ($num >= 37) {
$r = $num % 37;
$num = floor ($num / 37);
$out = $out . $alpha[$r];
}
return $out . $alpha[$num];
}
function decodeToEncode ($str) {
static $from = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ*~=$";
static $to = "0123456789ABCDEFGH1JK1MN0PQRSTUVWXYZABCDEFGH1JK1MN0PQRSTUVWXYZ*~=$";
return strtr ($str, $from, $to);
}
Though clearly the real challenge is to write a encodeToSymbol() function. I am not really a PHP expert (my $'s in the strings probably needs to be escaped somehow -- hints?), so I will leave that to others.