Related
The target is to check a product description and to identify different characteristics/product options. The input data has the following structure:
// TABLE WITH INPUT DATA. STRUCTURE: PRODUCT_CATEGORY [0], PRODUCT_NUMBER[1], DESCRIPTION OF AN OPTION [2]. THE INPUT DATA TABLE CAN CONSIST OF UP TO 400-500 ROWS
$input_product_data = array (
array('AAAA','1111','Chimney with red bricks in the center of the room'),
array('BBBB','2222','Two wide windows in the main floor'),
array('BBBB','2233','Plastic window has to be changed later'),
array('CCCC','3333','Roof tiles renewed in 2015'),
array('NULL','4444','Floor has been renovated for two years. Currently it has ground in wood.'),
array('NULL','NULL','Beautiful door in green color built at begin of 20th century')
);
There are 3 different constelations to indicate a product option:
Only by search string within product description,
Example:
Input data: array('NULL','NULL','Beautiful door in green color built at begin of 20th century')
Search string: 'green color' within PRODUCT_DESCRIPTION
Result: Available
By search string within product description + product category:
Example:
Input data: array('CCCC','NULL','Roof tiles renewed in 2015'),
Search strings: 'CCCC' within PRODUCT_CATEGORY + 'green color' within PRODUCT_DESCRIPTION
Result: Available
By search string within product description + product category + product number.
Example:
Input data: array('AAAA','1111','Chimney with red bricks in the center of the room')
Search strings: 'AAAA' within PRODUCT_CATEGORY + '1111' within PRODUCT_NUMBER + 'Chimney' within PRODUCT_DESCRIPTION
Result: Available
IMPORTANT:
The table with input data per product can consist of up to 450 description rows.
The search strings can be defined many times ( e. g. 10 different search strings for the option "Windows" like "windows in the floor", "big windows", "window without glas" etc.).
The start set of rules (combinations of product description + product category + product number) will consist of ca. 3000 rows and will be extented permanently by business guys.
REALIZATION VARIANT A (by use of preg_match):
// TABLE FOR PRODUCT OPTIONS. STRUCTURE: ID[0], OPTION NAME[1], OPTION CATEGORY[2], OPTION-FAMILY[3], PROD.-NR[4], REG. EXPRESSION[5], PRIORITY[6], OUTPUT[7]
$ct_product_options = array (
array('0001', 'Chimney', 'Additional options', '/^AAAA/', '/9999/', '/^Chimney with./', '0', 'Available'),
array('0002', 'Material of ground floor', 'Additional options', '/NULL/', '/^4444$/', '/.wood./', '0', 'Wood'),
array('0003', 'Roof tiles', 'Basic options', '/^CCCC/', '/0022/', '/^Roof tiles./', '0', 'Available'),
array('0004', 'Windows', 'Basic options', '/^B...$/', '/^2.../', '/.window$/', '0', 'Available'),
array('0004', 'Windows', 'Basic options', '/^B...$/', '/^2.../', '/.wide windows./', '0', 'Available'),
array('0005', 'Door color', 'Basic options', '/NULL/', '/NULL/', '/green/', '0', 'Green'),
array('0006', 'Air condition', 'Additional options', '/NULL/', '/NULL/', '/^Air condition made in Japan/', '0', 'Green')
);
// FOR LOOP TO MAKE COMPARISON BETWEEN INPUT PRODUCT DATA AND PREDEFINED CUST. STRINGS
$matches_array = array();
foreach ($input_product_data as [$product_family, $product_number, $product_description]) {
foreach($ct_product_options as [$option_id, $option_name, $option_category, $product_family_reg_exp, $product_number_reg_exp, $regular_expression, $priority, $output]) {
if (preg_match($regular_expression, $product_description) == 1
&& preg_match($product_family_reg_exp, $product_family) == 1 ||
preg_match($regular_expression, $product_description) == 1
&& preg_match($product_number_reg_exp, $product_number) == 1) {
$matches_array [] = array("id" => $option_id, "option_name" => $option_name, "option_category" => $option_category, "output"=> $output);
}
else {
if (empty($product_family) && empty($product_number)) {
if (preg_match($regular_expression, $product_description) == 1) {
$matches_array [] = array("id" => $option_id, "option_name" => $option_name, "option_category" => $option_category, "output"=> $output);
}
}
}
}
}
//echo "<pre>";
//print_r($matches_array);
// FUNCTION FOR DELETE DUBLICATES FROM ARRAY WITH MATCHES
function unique_multidimensional_array($array, $key) {
$temp_array = array();
$i = 0;
$key_array = array();
foreach($array as $val) {
if (!in_array($val[$key], $key_array)) {
$key_array[$i] = $val[$key];
$temp_array[$i] = $val;
}
$i++;
}
return $temp_array;
}
//echo "<br><h3>UNIQUE MATCHES</h3>";
// CALL OF THE FUNCTION TO GET UNIQUE MATCHES
$unique_matches = unique_multidimensional_array($matches_array, 'id');
sort($unique_matches);
//echo "<pre>";
//print_r($unique_matches);
// CALL OF THE FUNCTION TO CREATE LIST/ARRAY WITH ALL AVAILABLE PRODUCT OPTIONS
$list_all_product_options = unique_multidimensional_array($ct_product_options, 0);
$list_all_product_options_short = array();
foreach ($list_all_product_options as $option_item) {
$list_all_product_options_short[] = array("id" => $option_item[0], "option_name" => $option_item[1], "option_category" => $option_item[2]);
}
sort($list_all_product_options_short);
//echo "<h3>LIST WITH ALL PRODUCT OPTIONS (SHORT VERSION)</h3>\n";
//echo "<pre>";
//print_r($list_all_product_options_short);
$unique_matches = array_column($unique_matches, null, 'id');
foreach ($list_all_product_options_short as $key => $value) {
if (isset($unique_matches[$value['id']])) {
$result[$key] = array_merge($value, $unique_matches[$value['id']]);
} else {
$result[$key] = array_merge($value, ['output' => 'Not available']);
}
}
echo "<h3>FINAL RESULTS</h3>\n";
//echo "<pre><br>\n";
print_r($result);
The variant realized with preg_match works well and provide quite good flexibilty by defining of the regex. E. g. Instead to define the whole product number "2222" I can use only "/^2.../". Or I can combine many regex within one row by use of "|" (e. g. ".wide windows. | some window | etc."). The problem is that by real data volume 500 rows within $input_product_data and 3000 rows within $ct_product_options the code is quite slow.
REALIZATION VARIANT B (by use of stripos):
// INPUT DATA WITH PRODUCT DESCRIPTION. STRUCTURE: PROD. FAMILY, PROD. NUMBER, PRODUCT DESCRIPTION
$input_product_data = array (
array('AAAA','1111','Chimney with red bricks in the center of the room'),
array('BBBB','2222','Two wide windows in the main floor'),
array('BBBB','2233','Plastic window has to be changed later'),
array('CCCC','3333','Roof tiles renewed in 2015'),
array('NULL','4444','Floor has been renovated for two years. Currently it has ground in wood.'),
array('NULL','NULL','Beautiful door in green color built at begin of 20th century')
);
// CUSTOMIZING TABLE FOR PRODUCT OPTIONS. STRUCTURE: ID[0], OPTION NAME[1], OPTION CATEGORY[2], OPTION-FAMILY[3], PROD.-NR[4], REG. EXPRESSION[5], PRIORITY[6], OUTPUT[7]
$ct_product_options = array (
array('0001', 'Chimney', 'Additional options', 'AAAA', '9999', 'Chimney with', '0', 'Available'),
array('0002', 'Material of ground floor', 'Additional options', 'NULL', '4444', 'wood', '0', 'Wood'),
array('0003', 'Roof tiles', 'Basic options', 'CCCC', '0022', 'Roof tiles', '0', 'Available'),
array('0004', 'Windows', 'Basic options', 'BBBB', '2222', 'window', '0', 'Available'),
array('0004', 'Windows', 'Basic options', 'BBBB', '2222', 'wide windows', '0', 'Available'),
array('0005', 'Door color', 'Basic options', 'NULL', 'NULL', 'green', '0', 'Green'),
array('0006', 'Air condition', 'Additional options', 'NULL', 'NULL', 'Air condition made in Japan', '0', 'Green')
);
// IMPORTANT: THE REG. EXPRESSIONS CAN BE DEFINED MANY TIME (e. g. 10 DIFFERENT REG: EXPRESSIONS FOR WINDOW). POINTS "." REPRESENTS EMPTY SPACES WHICH ARE IMPORTANT TO INDETIFY EXACTLY AN OPTION.
// FOR LOOP TO MAKE COMPARISON BETWEEN INPUT PRODUCT DATA AND PREDEFINED CUST. STRINGS
$matches_array = array();
foreach ($input_product_data as [$product_family, $product_number, $product_description]) {
foreach($ct_product_options as [$option_id, $option_name, $option_category, $product_family_reg_exp, $product_number_reg_exp, $regular_expression, $priority, $output]) {
if (stripos($product_description, $regular_expression) !== false
&& stripos($product_family, $product_family_reg_exp) !== false ||
stripos($product_description, $regular_expression) !== false
&& stripos($product_number, $product_number_reg_exp) !== false) {
$matches_array [] = array("id" => $option_id, "option_name" => $option_name, "option_category" => $option_category, "output"=> $output);
}
else {
if (empty($product_family) && empty($product_number)) {
if (stripos($product_description, $regular_expression) !== false) {
$matches_array [] = array("id" => $option_id, "option_name" => $option_name, "option_category" => $option_category, "output"=> $output);
}
}
}
}
}
//echo "<pre>";
//print_r($matches_array);
// FUNCTION FOR DELETE DUBLICATES FROM ARRAY WITH MATCHES
function unique_multidimensional_array($array, $key) {
$temp_array = array();
$i = 0;
$key_array = array();
foreach($array as $val) {
if (!in_array($val[$key], $key_array)) {
$key_array[$i] = $val[$key];
$temp_array[$i] = $val;
}
$i++;
}
return $temp_array;
}
//echo "<br><h3>UNIQUE MATCHES</h3>";
// CALL OF THE FUNCTION TO GET UNIQUE MATCHES
$unique_matches = unique_multidimensional_array($matches_array, 'id');
sort($unique_matches);
//echo "<pre>";
//print_r($unique_matches);
// CALL OF THE FUNCTION TO CREATE LIST/ARRAY WITH ALL AVAILABLE PRODUCT OPTIONS
$list_all_product_options = unique_multidimensional_array($ct_product_options, 0);
$list_all_product_options_short = array();
foreach ($list_all_product_options as $option_item) {
$list_all_product_options_short[] = array("id" => $option_item[0], "option_name" => $option_item[1], "option_category" => $option_item[2]);
}
sort($list_all_product_options_short);
//echo "<h3>LIST WITH ALL PRODUCT OPTIONS (SHORT VERSION)</h3>\n";
//echo "<pre>";
//print_r($list_all_product_options_short);
// ::::::::::::::::::::::::::::::::::
$unique_matches = array_column($unique_matches, null, 'id');
foreach ($list_all_product_options_short as $key => $value) {
if (isset($unique_matches[$value['id']])) {
$result[$key] = array_merge($value, $unique_matches[$value['id']]);
} else {
$result[$key] = array_merge($value, ['output' => 'Not available']);
}
}
echo "<h3>FINAL RESULTS</h3>\n";
//echo "<pre><br>\n";
print_r($result);
It works much faster, but does not provide the felixibility of regex.
So, my questions:
Do you see any ways to optimize VARIANT A to get it faster or optimize VARIANT B to get it more flexible?
Especial question: How I can add the logic for the parameter PRIORITY from the table $ct_product_options?
The business logic is the following for it: As default all rows/rules have priority "0". But some of them will get priority ">0" (e. g. "1" or "2" etc.). The rule with highest priority should overwrite other rules.
E. g.
This rule with priority "0" identified windows in the house.
array('0004', 'Windows', 'Basic options', '/^B...$/', '/^2.../', '/.wide windows./', '0', 'Available')
At the same time this rule with priority "1" tells us that all windows are not available more. So, that means we have to get "Not available" within the final results.
array('0004', 'Windows', 'Basic options', '/^B...$/', '/^2.../', '/^Windows have been removed from the whole building last year/', '1', 'Not available')
Before optimizing the variants, I believe I should tell how I would implement a solution to solve generate the intended array.
I ran your code to understand better what should be the result. But instead of using print_r, I did this:
echo json_encode($result, JSON_PRETTY_PRINT);
I got this:
[
{
"id": "0001",
"option_name": "Chimney",
"option_category": "Additional options",
"output": "Available"
},
{
"id": "0002",
"option_name": "Material of ground floor",
"option_category": "Additional options",
"output": "Wood"
},
{
"id": "0003",
"option_name": "Roof tiles",
"option_category": "Basic options",
"output": "Available"
},
{
"id": "0004",
"option_name": "Windows",
"option_category": "Basic options",
"output": "Available"
},
{
"id": "0005",
"option_name": "Door color",
"option_category": "Basic options",
"output": "Green"
},
{
"id": "0006",
"option_name": "Air condition",
"option_category": "Additional options",
"output": "Not available"
}
]
I noticed each array element is an element from $ct_product_options mapped to some format. So, I used array_map like this:
$result = array_map(
fn($option) => [
'id' => $option[0],
'option_name' => $option[1],
'option_category' => $option[2],
'output' => get_option_output($option, $input_product_data),
],
$ct_product_options
);
Now I have to implement get_option_output. I think all those nested foreach and if in both A and B variants make the code hard to understand (besides how each line is indented). If I understand correctly your intentions, it seems this has a bug:
if (
preg_match($regular_expression, $product_description) == 1
&& preg_match($product_family_reg_exp, $product_family) == 1 ||
preg_match($regular_expression, $product_description) == 1
&& preg_match($product_number_reg_exp, $product_number) == 1) {
And you wanted to do something like this:
$productDescriptionMatches = preg_match($regular_expression, $product_description);
if (
(
$productDescriptionMatches
&& preg_match($product_family_reg_exp, $product_family)
) || (
$productDescriptionMatches
&& preg_match($product_number_reg_exp, $product_number)
)
) {
Which is equivalent to:
if (
preg_match($regular_expression, $product_description)
&& (
preg_match($product_family_reg_exp, $product_family)
|| preg_match($product_number_reg_exp, $product_number)
)
) {
If I counted everything correctly, and assuming you made that mistake, I believe you want something like this:
function some($array, $callback)
{
foreach ($array as $item) {
if ($callback($item)) {
return $item;
}
}
return false;
}
function get_option_output($option, $products)
{
$found = some(
$products,
fn($product) =>
(
preg_match($option[5], $product[2])
&& (
preg_match($option[3], $product[0])
|| preg_match($option[4], $product[1])
|| (
empty($product[0])
&& empty($product[1])
)
)
)
);
return $found ? $option[7] : 'Not available';
}
$result = array_map(
fn($option) => [
'id' => $option[0],
'option_name' => $option[1],
'option_category' => $option[2],
'output' => get_option_output($option, $input_product_data),
],
$ct_product_options
);
In average, the execution time of that code was: 0.0000189903259277 seconds. I ran 10,000 iterations.
Variant A took in average: 0.0000316595554352 seconds.
Variant B took in average: 0.0000314178943634 seconds.
The code I provided doesn't have nested loops and doesn't have to remove repeated elements and sorting them twice. But it's possible to make it run faster:
$result = [];
foreach ($ct_product_options as $option) {
foreach ($input_product_data as $product) {
$output = null;
$isAvailable =
(
preg_match($option[5], $product[2])
&& (
preg_match($option[3], $product[0])
|| preg_match($option[4], $product[1])
|| (
empty($product[0])
&& empty($product[1])
)
)
);
if ($isAvailable) {
$output = $option[7];
break;
}
}
$result []= [
'id' => $option[0],
'option_name' => $option[1],
'option_category' => $option[2],
'output' => $output ?? 'Not available',
];
}
It took, in average, 0.0000132960796356 seconds. But it's harder to understand.
That answers the first question. Use an array_map.
It also helps to answer the special question: change the function get_option_output accordingly.
If priority is the regular expression that should be used (and all the others should be ignored), then do something like this (also check if the priority is valid):
function get_option_output($option, $products)
{
$priority = (int)$option[6];
$found = find(
$products,
fn($product) => preg_match(
$option[3 + $priority],
$product[$priority]
)
);
return $found ? $option[7] : 'Not available';
}
If the one with the highest priority should be checked first, and the others should also be checked:
function some($array, $callback)
{
foreach ($array as $index => $item) {
if ($callback($item, $index)) {
return true;
}
}
return false;
}
function get_option_output($option, $products)
{
$priority = (int)$option[6];
$found = some(
$products,
fn($product) =>
preg_match($option[3 + $priority], $product[$priority])
|| some(
$product,
fn($text, $index) =>
$index !== $priority
&& preg_match($option[3 + $index], $product[$index])
)
);
return $found ? $option[7] : 'Not available';
}
If I didn't understand the details and something is missing, nevertheless probably what was provided might help.
Added: unique_multidimensional_array reimplementation
function unique_multidimensional_array($array, $key) {
$valuesByKey = [];
foreach($array as $value) {
$elementsByKey[$value[$key]] = $value;
}
return array_values($valuesByKey);
}
I'm trying to modify a voting script(Thumbsup) I need this query to only return only array items that have their 'cat' => '1' in the subarray (proper term?)
<?php $items = ThumbsUp::items()->get() ?>
Here's an example of the live generated array from my database
array (
0 =>
array (
'id' => 1,
'name' => 'a',
'cat' => '1',
),
1 =>
array (
'id' => 2,
'name' => 'b',
'cat' => '2',
),
2 =>
array (
'id' => 3,
'name' => 'c',
'cat' => '2',
),
)
Is this possible by just modifying the query?
edit: heres function get()
public function get()
{
// Start building the query
$sql = 'SELECT id, name, url, cat, closed, date, votes_up, votes_down, ';
$sql .= 'votes_up - votes_down AS votes_balance, ';
$sql .= 'votes_up + votes_down AS votes_total, ';
$sql .= 'votes_up / (votes_up + votes_down) * 100 AS votes_pct_up, ';
$sql .= 'votes_down / (votes_up + votes_down) * 100 AS votes_pct_down ';
$sql .= 'FROM '.ThumbsUp::config('database_table_prefix').'items ';
// Select only either open or closed items
if ($this->closed !== NULL)
{
$where[] = 'closed = '.(int) $this->closed;
}
// Select only either open or closed items
if ($this->name !== NULL)
{
// Note: substr() is used to chop off the wrapping quotes
$where[] = 'name LIKE "%'.substr(ThumbsUp::db()->quote($this->name), 1, -1).'%"';
}
// Append all query conditions if any
if ( ! empty($where))
{
$sql .= ' WHERE '.implode(' AND ', $where);
}
// We need to order the results
if ($this->orderby)
{
$sql .= ' ORDER BY '.$this->orderby;
}
else
{
// Default order
$sql .= ' ORDER BY name ';
}
// A limit has been set
if ($this->limit)
{
$sql .= ' LIMIT '.(int) $this->limit;
}
// Wrap this in an try/catch block just in case something goes wrong
try
{
// Execute the query
$sth = ThumbsUp::db()->prepare($sql);
$sth->execute(array($this->name));
}
catch (PDOException $e)
{
// Rethrow the exception in debug mode
if (ThumbsUp::config('debug'))
throw $e;
// Otherwise, fail silently and just return an empty item array
return array();
}
// Initialize the items array that will be returned
$items = array();
// Fetch all results
while ($row = $sth->fetch(PDO::FETCH_OBJ))
{
// Return an item_id => item_name array
$items[] = array(
'id' => (int) $row->id,
'name' => $row->name,
'url' => $row->url,
'cat' => $row->cat,
'closed' => (bool) $row->closed,
'date' => (int) $row->date,
'votes_up' => (int) $row->votes_up,
'votes_down' => (int) $row->votes_down,
'votes_pct_up' => (float) $row->votes_pct_up,
'votes_pct_down' => (float) $row->votes_pct_down,
'votes_balance' => (int) $row->votes_balance,
'votes_total' => (int) $row->votes_total,
);
}
return $items;
}
thanks.
You can easily modify "get()" to add the desired functionality:
public function get($cat = null)
{
$where = array();
if ($cat !== null) {
$where[] = 'cat = '. (int) $cat;
}
// ... original code ...
}
Usage:
$items = ThumbsUp::items()->get(1);
I'm trying to create a list of categories with any number of sub categories, where sub categories can also has their own sub categories.
I have selected all categories from the Mysql db, the cats are in a standard associate array list, each category has an id, name, parentid where the parentid is 0 if it's top level.
I basically want to be able to take the single level array of cats and turn it into a multidimensional array structure where each category can have an element which will contain an array of subcats.
Now, I can easily achieve this by looping a query for each category but this is far from ideal, I'm trying to do it without any extra hits on the db.
I understand I need a recursive function for this. Can anyone point me in the right direction for this tree style structure?
Cheers
This does the job:
$items = array(
(object) array('id' => 42, 'parent_id' => 1),
(object) array('id' => 43, 'parent_id' => 42),
(object) array('id' => 1, 'parent_id' => 0),
);
$childs = array();
foreach($items as $item)
$childs[$item->parent_id][] = $item;
foreach($items as $item) if (isset($childs[$item->id]))
$item->childs = $childs[$item->id];
$tree = $childs[0];
print_r($tree);
This works by first indexing categories by parent_id. Then for each category, we just have to set category->childs to childs[category->id], and the tree is built !
So, now $tree is the categories tree. It contains an array of items with parent_id=0, which themselves contain an array of their childs, which themselves ...
Output of print_r($tree):
stdClass Object
(
[id] => 1
[parent_id] => 0
[childs] => Array
(
[0] => stdClass Object
(
[id] => 42
[parent_id] => 1
[childs] => Array
(
[0] => stdClass Object
(
[id] => 43
[parent_id] => 42
)
)
)
)
)
So here is the final function:
function buildTree($items) {
$childs = array();
foreach($items as $item)
$childs[$item->parent_id][] = $item;
foreach($items as $item) if (isset($childs[$item->id]))
$item->childs = $childs[$item->id];
return $childs[0];
}
$tree = buildTree($items);
Here is the same version, with arrays, which is a little tricky as we need to play with references (but works equally well):
$items = array(
array('id' => 42, 'parent_id' => 1),
array('id' => 43, 'parent_id' => 42),
array('id' => 1, 'parent_id' => 0),
);
$childs = array();
foreach($items as &$item) $childs[$item['parent_id']][] = &$item;
unset($item);
foreach($items as &$item) if (isset($childs[$item['id']]))
$item['childs'] = $childs[$item['id']];
unset($item);
$tree = $childs[0];
So the array version of the final function:
function buildTree($items) {
$childs = array();
foreach($items as &$item) $childs[(int)$item['parent_id']][] = &$item;
foreach($items as &$item) if (isset($childs[$item['id']]))
$item['childs'] = $childs[$item['id']];
return $childs[0]; // Root only.
}
$tree = buildTree($items);
You can fetch all categories at once.
Suppose you have a flat result from the database, like this:
$categories = array(
array('id' => 1, 'parent' => 0, 'name' => 'Category A'),
array('id' => 2, 'parent' => 0, 'name' => 'Category B'),
array('id' => 3, 'parent' => 0, 'name' => 'Category C'),
array('id' => 4, 'parent' => 0, 'name' => 'Category D'),
array('id' => 5, 'parent' => 0, 'name' => 'Category E'),
array('id' => 6, 'parent' => 2, 'name' => 'Subcategory F'),
array('id' => 7, 'parent' => 2, 'name' => 'Subcategory G'),
array('id' => 8, 'parent' => 3, 'name' => 'Subcategory H'),
array('id' => 9, 'parent' => 4, 'name' => 'Subcategory I'),
array('id' => 10, 'parent' => 9, 'name' => 'Subcategory J'),
);
You can create a simple function that turns that flat list into a structure, preferably inside a function. I use pass-by-reference so that there are only one array per category and not multiple copies of the array for one category.
function categoriesToTree(&$categories) {
A map is used to lookup categories quickly. Here, I also created a dummy array for the "root" level.
$map = array(
0 => array('subcategories' => array())
);
I added another field, subcategories, to each category array, and add it to the map.
foreach ($categories as &$category) {
$category['subcategories'] = array();
$map[$category['id']] = &$category;
}
Looping through each categories again, adding itself to its parent's subcategory list. The reference is important here, otherwise the categories already added will not be updated when there are more subcategories.
foreach ($categories as &$category) {
$map[$category['parent']]['subcategories'][] = &$category;
}
Finally, return the subcategories of that dummy category which refer to all top level categories._
return $map[0]['subcategories'];
}
Usage:
$tree = categoriesToTree($categories);
And here is the code in action on Codepad.
See the method :
function buildTree(array &$elements, $parentId = 0) {
$branch = array();
foreach ($elements as $element) {
if ($element['parent_id'] == $parentId) {
$children = buildTree($elements, $element['id']);
if ($children) {
$element['children'] = $children;
}
$branch[$element['id']] = $element;
}
}
return $branch;
}
I had the same problem and solved it this way: fetch cat rows from DB and for each root categories, build tree, starting with level (depth) 0. May not be the most efficient solution, but works for me.
$globalTree = array();
$fp = fopen("/tmp/taxonomy.csv", "w");
// I get categories from command line, but if you want all, you can fetch from table
$categories = $db->fetchCol("SELECT id FROM categories WHERE parentid = '0'");
foreach ($categories as $category) {
buildTree($category, 0);
printTree($category);
$globalTree = array();
}
fclose($file);
function buildTree($categoryId, $level)
{
global $db, $globalTree;
$rootNode = $db->fetchRow("SELECT id, name FROM categories WHERE id=?", $categoryId);
$childNodes = $db->fetchAll("SELECT * FROM categories WHERE parentid = ? AND id <> ? ORDER BY id", array($rootNode['id'], $rootNode['id']));
if(count($childNodes) < 1) {
return 0;
} else {
$childLvl = $level + 1;
foreach ($childNodes as $childNode) {
$id = $childNode['id'];
$childLevel = isset($globalTree[$id])? max($globalTree[$id]['depth'], $level): $level;
$globalTree[$id] = array_merge($childNode, array('depth' => $childLevel));
buildTree($id, $childLvl);
}
}
}
function printTree($categoryId) {
global $globalTree, $fp, $db;
$rootNode = $db->fetchRow("SELECT id, name FROM categories WHERE id=?", $categoryId);
fwrite($fp, $rootNode['id'] . " : " . $rootNode['name'] . "\n");
foreach ($globalTree as $node) {
for ($i=0; $i <= $node['depth']; $i++) {
fwrite($fp, ",");
}
fwrite($fp, $node['id'] " : " . $node['name'] . "\n");
}
}
ps. I am aware that OP is looking for a solution without DB queries, but this one involves recursion and will help anybody who stumbled across this question searching for recursive solution for this type of question and does not mind DB queries.
If the parent key is not passed from the class object then my code will create a root category and if the parent value is passed then child will create under the parent root.
class CategoryTree {
public $categories = array();
public function addCategory(string $category, string $parent=null) : void
{
if( $parent ) {
if ( array_key_exists($parent , $this->categories ) ) {
$this->categories[$parent][] = $category;
}
else {
$this->categories[$parent] = array();
$this->categories[$parent][] = $category;
}
}
else {
if ( ! array_key_exists($category , $this->categories ) ) {
$this->categories[$category] = array();
}
}
}
public function getChildren(string $parent = null) : array
{
$data = [];
if ( array_key_exists($parent , $this->categories ) ) {
$data = $this->categories[$parent];
}
return $data;
}
}
$c = new CategoryTree;
$c->addCategory('A', null);
$c->addCategory('B', 'A');
$c->addCategory('C', 'A');
$c->addCategory('C', 'E');
$c->addCategory('D', 'E');
$c->addCategory('D', null);
$c->addCategory('N', 'D');
$c->addCategory('A', null);
$c->addCategory('G', 'A');
echo implode(',', $c->getChildren('A'));
Ok, I am wanting to do something like this:
$which = !empty($param_id) ? "['groups'][$param_id]" : "['groups']";
And than I'd like it to be able to do something like so...
$all_groups . $which = array(
-1 => array(
'id' => '-1',
'name' => $txt['parent_guests_only'],
'checked' => in_array('-1', $checked) || in_array('-3', $checked),
'is_post_group' => false,
)
And I need it to build an array like so, if !empty($param_id)
$all_groups['groups'][$param_id] = array(the array info);
But if $param_id is empty it should do the this instead:
$all_groups['groups'] = array(the array info);
I don't think I can concatenate it or can I?
Can someone please help me here? This is happening many many times throughout a function, so I don't want to use if... else... statements every single time. Would be too many, thinking of a 1 fast approach for all of them.
Thanks :)
EDIT, here is the function in question:
function ListGroups($checked = array(), $unallowed = array(), $order = array(), $param_id = 0)
{
global $context, $smcFunc, $txt;
// We'll need this for loading up the names of each group.
if (!loadLanguage('ManageBoards'))
loadLanguage('ManageBoards');
if (empty($checked))
return array();
$all_groups['groups'][$param_id] = array();
if (!in_array('-1', $unallowed))
// Guests
$all_groups['groups'][$param_id] = array(
-1 => array(
'id' => '-1',
'name' => $txt['parent_guests_only'],
'checked' => in_array('-1', $checked) || in_array('-3', $checked),
'is_post_group' => false,
)
);
if (!in_array('0', $unallowed))
{
// Regular Members
if (!empty($all_groups['groups']))
$all_groups['groups'][$param_id] += array(
0 => array(
'id' => '0',
'name' => $txt['parent_members_only'],
'checked' => in_array('0', $checked) || in_array('-3', $checked),
'is_post_group' => false,
)
);
else
$all_groups['groups'][$param_id] = array(
0 => array(
'id' => '0',
'name' => $txt['parent_members_only'],
'checked' => in_array('0', $checked) || in_array('-3', $checked),
'is_post_group' => false,
)
);
}
// Load membergroups.
$request = $smcFunc['db_query']('', '
SELECT group_name, id_group, min_posts
FROM {db_prefix}membergroups
WHERE id_group > {int:is_zero}',
array(
'is_zero' => 0,
)
);
while ($row = $smcFunc['db_fetch_assoc']($request))
{
if (!in_array($row['id_group'], $unallowed))
{
$all_groups['groups'][(int) $param_id][(int) $row['id_group']] = array(
'id' => $row['id_group'],
'name' => trim($row['group_name']),
'checked' => in_array($row['id_group'], $checked) || in_array('-3', $checked),
'is_post_group' => $row['min_posts'] != -1,
);
}
}
$smcFunc['db_free_result']($request);
// Let's sort these arrays accordingly!
if (!empty($order))
{
$all_groups['groups'][$param_id] = sortGroups($all_groups['groups'][$param_id], $order);
$context['group_order' . $param_id] = implode(', ', $order);
}
else
{
$context['group_order' . $param_id] = '';
sort($all_groups['groups'][$param_id]);
$x = 0;
foreach ($all_groups['groups'][$param_id] as $key => $value)
{
$x++;
$context['group_order' . $param_id] .= $x < count($all_groups['groups'][$param_id]) ? $value['id'] . ', ' : $value['id'];
}
}
return $all_groups['groups'][$param_id];
}
I need to do a check for !empty($param_id), if so, it needs to build the $all_groups['groups'] array without the $param_id.
So will need to add in a check for if (!empty($params_id)) build the array like so: $all_groups['groups'][$params_id] else build it like this instead: $all_groups['groups']. I don't want a bunch of if... else... statements in here, just a 1 or 5 liner would be GREAT!
Thanks Guys :)
Don't overcomplicate it. :)
$array = array(
/* contents */
);
if (!empty($param_id)) {
$all_groups['groups'][$param_id] = $array;
} else {
$all_groups['groups'] = $array;
}
I don't know what $all_groups['groups'] looks like before this; if it was empty, I'd shorten this to:
$all_groups['groups'] = array(
/* contents */
);
if (!empty($param_id)) {
$all_groups['groups'] = array($param_id => $all_groups['groups']);
}
if (!empty($param_id)) {
$which = &$all_groups['groups'][$param_id]
} else {
$which = &$all_groups['groups'];
}
$which = array(
-1 => array(
'id' => '-1',
'name' => $txt['parent_guests_only'],
'checked' => in_array('-1', $checked) || in_array('-3', $checked),
'is_post_group' => false,
);
unset($which); // unset $which, if you want to use this variable
// in this scope once again
Generally speaking, references are the solution. See #zerkms' answer.
However, if at all possible, I would try to redesign your data structures such that you don't have to resort to this type of conditional behavior. For example, using default as the default missing key:
$which = !empty($param_id) ? $param_id : 'default';
$all_groups['groups'][$which] = array( ... );
I don't know if it's possible in your case, but this could be easier to manage.
Other potential solutions:
$tmp = array( ... );
if ($cond)
$foo = $tmp;
else
$bar = $tmp;
or:
function make_array( ... )
{
return array( ... );
}
if ($cond)
$foo = make_array( ... );
else
$bar = make_array( ... );
I have a deep and long array (matrix). I only know the product ID.
How found way to product?
Sample an array of (but as I said, it can be very long and deep):
Array(
[apple] => Array(
[new] => Array(
[0] => Array([id] => 1)
[1] => Array([id] => 2))
[old] => Array(
[0] => Array([id] => 3)
[1] => Array([id] => 4))
)
)
I have id: 3, and i wish get this:
apple, old, 0
Thanks
You can use this baby:
function getById($id,$array,&$keys){
foreach($array as $key => $value){
if(is_array( $value )){
$result = getById($id,$value,$keys);
if($result == true){
$keys[] = $key;
return true;
}
}
else if($key == 'id' && $value == $id){
$keys[] = $key; // Optional, adds id to the result array
return true;
}
}
return false;
}
// USAGE:
$result_array = array();
getById( 3, $products, $result_array);
// RESULT (= $result_array)
Array
(
[0] => id
[1] => 0
[2] => old
[3] => apple
)
The function itself will return true on success and false on error, the data you want to have will be stored in the 3rd parameter.
You can use array_reverse(), link, to reverse the order and array_pop(), link, to remove the last item ('id')
Recursion is the answer for this type of problem. Though, if we can make certain assumptions about the structure of the array (i.e., 'id' always be a leaf node with no children) there's further optimizations possible:
<?php
$a = array(
'apple'=> array(
'new'=> array(array('id' => 1), array('id' => 2), array('id' => 5)),
'old'=> array(array('id' => 3), array('id' => 4, 'keyname' => 'keyvalue'))
),
);
// When true the complete path has been found.
$complete = false;
function get_path($a, $key, $value, &$path = null) {
global $complete;
// Initialize path array for first call
if (is_null($path)) $path = array();
foreach ($a as $k => $v) {
// Build current path being tested
array_push($path, $k);
// Check for key / value match
if ($k == $key && $v == $value) {
// Complete path found!
$complete= true;
// Remove last path
array_pop($path);
break;
} else if (is_array($v)) {
// **RECURSION** Step down into the next array
get_path($v, $key, $value, $path);
}
// When the complete path is found no need to continue loop iteration
if ($complete) break;
// Teardown current test path
array_pop($path);
}
return $path;
}
var_dump( get_path($a, 'id', 3) );
$complete = false;
var_dump( get_path($a, 'id', 2) );
$complete = false;
var_dump( get_path($a, 'id', 5) );
$complete = false;
var_dump( get_path($a, 'keyname', 'keyvalue') );
I tried this for my programming exercise.
<?php
$data = array(
'apple'=> array(
'new'=> array(array('id' => 1), array('id' => 2), array('id' => 5)),
'old'=> array(array('id' => 3), array('id' => 4))
),
);
####print_r($data);
function deepfind($data,$findfor,$depth = array() ){
foreach( $data as $key => $moredata ){
if( is_scalar($moredata) && $moredata == $findfor ){
return $depth;
} elseif( is_array($moredata) ){
$moredepth = $depth;
$moredepth[] = $key;
$isok = deepfind( $moredata, $findfor, $moredepth );
if( $isok !== false ){
return $isok;
}
}
}
return false;
}
$aaa = deepfind($data,3);
print_r($aaa);
If you create the array once and use it multiple times i would do it another way...
When building the initial array create another one
$id_to_info=array();
$id_to_info[1]=&array['apple']['new'][0];
$id_to_info[2]=&array['apple']['new'][2];