Finding value between ranges without going through the entire ranges - php

I want to grant bonuses to my players based on how many friends they have.
I have breakpoints (for example 0, 1, 5, 10, 25)
For 0 friends he gets 0 bonus.
For 1 friend he gets 1000, for 5 or above 2000 etc...
What I do right now is this:
public function getFriendsBonusByFriendsAmount($amount)
{
switch (true) {
case ($amount < 1):
return 0;
case ($amount < 5):
return 1000;
case ($amount < 10):
return 2000;
case ($amount < 25):
return 3000;
case ($amount >= 25):
return 5000;
}
}
I'm looking for a different way to find the bonus without foreach/switch
Perhaps think of an array or arrays that I could play with?
$bonusBreakpoints = [
0 => 0,
1 => 1000,
5 => 2000,
10 => 3000,
25 => 5000
]
Or perhaps two arrays with respective indexs?
I thought of a way to do it but it's a memory waste:
$bonusPerFriends = [
0 => 0,
1 => 1000,
2 => 1000,
3 => 1000,
4 => 1000,
5 => 2000,
6 => 2000,
...
25 => 5000
]
I rather not to use that way.

Well, sometimes foreach/switch will be the best solution :)
/**
* Calculates bonus based on how many
* friends the player have from predefined breakpoints
**/
function getBonus($friends) {
$bonuses = [0, 1000, 2000, 3000, 5000];
$stops = [[PHP_INT_MIN, 0], [1, 4], [5, 14], [15, 24], [25, PHP_INT_MAX]];
// replace the stops by bonus if match, otherwise return empty - O(n)
$bonus = array_map(function ($stop, $bonus) use ($friends) {
if ($friends >= $stop[0] && $friends <= $stop[1]) {
return $bonus;
}
}, $stops, $bonuses);
// clean up the array from empty values - O(n)
$bonus = array_filter($bonus , 'is_numeric');
// from array(3 => 3000) to 3000 - O(1)
return array_pop($bonus);
}
results:
echo getBonus(0).PHP_EOL; // 0
echo getBonus(4).PHP_EOL; // 1000
echo getBonus(12).PHP_EOL; // 2000
echo getBonus(20).PHP_EOL; // 3000
echo getBonus(39).PHP_EOL; // 5000
P.S. $bonuses and $stops here must be equal length.

#andrey-mischenko 's answer is technically right, but does not solve the problem without foreach, as you said in your question. (Edit: answer was removed) Try this:
$bonusBreakpoints = [
0 => 0,
1 => 1000,
5 => 2000,
10 => 3000,
25 => 5000
];
$justTheKeys = array_keys($bonusBreakpoints);
public function getFriendsBonusByFriendsAmount($amount)
{
$bonus = array_reduce($justTheKeys, function($carryOver, $item) use ($amount)
{
if ($amount >= $item) return $bonusBreakpoints($item);
return $carryOver;
}
return $bonus;
}
(I'm aware that this is not what array_reduce is originally intended for. I understood the question as a mind-game. Like "find creative ways to solve this problem apart from the obvious ones, like loops or switches." If I had to code this for work, I'd probabely use a loop, too. :) )

After reading the answer and more research I concludes that binary search is my best option.
The data should look somewhat like this:
$bonuses = [
[ 'min' => 0, 'max' => 0, 'amount' => 0 ]
[ 'min' => 1, 'max' => 4, 'amount' => 1000 ]
[ 'min' => 5, 'max' => 14, 'amount' => 2000 ]
...
[ 'min' => 25, 'max' => PHP_INT_MAX, 'amount' => 5000 ]
]
You start at the count($bonuses)/2 and from there check whether you are below the min, is so goes halfway, otherwise check if you are above max, and go halfway there. else... just return the bonus coz you are between the right range.
Since most of my users don't have any friends or more than 25 I would probably check the first and last cell first.

Related

How to find the closest matching array

Customers of a furniture store website can select products and add them to a "style book". Each product belongs to a "style".
The furniture store has some stylists that each have made their own style book that represents their style and expertise.
I want to be able to find the stylist that best matches a customer's stylebook.
For each style book I have a count of the number of products per style.
$stylists = [
'Nanda' => [
'Design' => 20,
'Retro' => 0,
'Rustiek' => 0,
],
'Angelique' => [
'Design' => 0,
'Retro' => 20,
'Rustiek' => 0,
],
'Lissy' => [
'Design' => 10,
'Retro' => 10,
'Rustiek' => 0,
],
];
The same for the customer's style book:
$customer = [
'Design' => 15,
'Retro' => 10,
'Rustiek' => 0,
];
In this case Lissy should be the best match.
The number of products isn't important since this depends on how active the stylist is.
More important is that the stylist matches most of the customer's styles.
For example:
'Stylist' => [
'Design' => 10,
'Retro' => 10,
'Rustiek' => 0,
]
Should still be a better match than
'Stylist' => [
'Design' => 300,
'Retro' => 0,
'Rustiek' => 180,
]
I have tried giving the stylists' style books scores and percentages based on the order of importance of the customer's style book but still I don't get the best match a 100% of the times.
Google also didn't get me anywhere.
As we have already discussed, the problem with your model is, that it relies on the number of products. But what we need is an indicator of the style the stylist is using. In other words we eliminate the count and replace it with a relatively weighted indicator (percentages in this case). For example a stylist with a product portfolio of:
[
style1 => 30,
style2 => 10,
style3 => 5
]
The product count is 45 = 30 + 10 + 5 this will result in a style-profile like this:
[
style1 => 0.66,
style2 => 0.22,
style3 => 0.11
]
To match the stylist-style-profile with the client-style-profile we need to do the same thing for the client-stylebook [15, 10, 0]:
[
style1 => 0.60
style2 => 0.40
style3 => 0.00
]
The idea behind this is, that we rate how a stylist is influenced by a certain style and the outcome will probably be quite similar for the product that we want to find the best fitting stylist to.
If the stylist made products in a style that is not really what we need for the match, we rate this fact with the weighted relative factor e.g. 0.11. It is not that important, but we still acknowledge the fact that the design might be somewhat biased.
Therefore, if a stylist has a lot of products with a certain style that we are not looking for, it won't change the outcome as much.
Please let me know, if this helps and if you want to change anything. From here we could also implement other options and rules.
Below you find my RatingModel.
<?php
class RatingModel {
private $name;
private $preferences;
private $preferencesWeighted;
public function RatingModel($name, array $preferences) {
$this->name = $name;
$this->preferences = $preferences;
$this->init();
}
private function init() {
$total = 0;
foreach ($this->preferences as $value) {
$total += $value;
}
if ($total > 0) {
foreach ($this->preferences as $value) {
$this->preferencesWeighted[] = $value / $total;
}
} else {
$this->preferencesWeighted = array_fill(0, sizeof($this->preferences), 0);
}
}
public function getName() {
return $this->name;
}
public function getPreferences() {
return $this->preferences;
}
public function getPreferencesWeighted() {
return $this->preferencesWeighted;
}
public function distanceToModel($ratingModel) {
$delta = [];
for ($i = 0; $i < sizeof($this->preferencesWeighted); $i++) {
$delta[] = abs($this->preferencesWeighted[$i] - $ratingModel->getPreferencesWeighted()[$i]);
}
return $delta;
}
public function scoreToModel($ratingModel) {
$distanceToModel = $this->distanceToModel($ratingModel);
$score = [];
foreach ($distanceToModel as $value) {
$score[] = $value * $value;
}
return sqrt(array_sum($score));
}
}
$customer = new RatingModel('Customer', [15, 10, 0]);
$nanda = new RatingModel('Nanda', [20, 0, 0]);
$angelique = new RatingModel('Angelique', [0, 20, 0]);
$lissy = new RatingModel('Lissy', [10, 0, 0]);
$mary = new RatingModel('Mary', [0, 0, 0]);
$max = new RatingModel('Max', [12, 0, 5]);
$simon = new RatingModel('Simon', [17, 2, 5]);
$manuel = new RatingModel('Manuel', [17, 8, 10]);
$betty = new RatingModel('Betty', [16, 9, 5]);
$sally = new RatingModel('Sally', [15, 10, 4]);
$peter = new RatingModel('Peter', [16, 9, 1]);
$stylists = [$nanda, $angelique, $lissy, $mary, $max, $simon, $manuel, $betty, $peter, $sally];
$relativeToClient = [];
foreach ($stylists as $stylist) {
$relativeToClient[] = [
'stylist' => $stylist->getName(),
'distance' => $stylist->distanceToModel($customer),
'score' => $stylist->scoreToModel($customer)
];
}
echo '<pre>';
print_r($stylists);
echo '<hr>';
print_r($customer);
echo '<hr>';
print_r($relativeToClient);
echo '<hr>from best fit to worst (low score means low delta)<hr>';
$results = array_column($relativeToClient, 'score', 'stylist');
asort($results);
print_r($results);
echo '</pre>';
Right below are the results (lower values are better):
Array
(
[Peter] => 0.067936622048676
[Sally] => 0.1700528000819
[Betty] => 0.20548046676563
[Manuel] => 0.35225222874108
[Simon] => 0.3942292057505
[Max] => 0.50765762377392
[Nanda] => 0.56568542494924
[Lissy] => 0.56568542494924
[Mary] => 0.7211102550928
[Angelique] => 0.84852813742386
)
If we look at the two best fitting stylists we notice, that Peter wins over Sally, because Sally has more Products with a different style.
Sally: [15, 10, 4]
Peter: [16, 9, 1]
You may also notice, that Nanda and Lissy have the same score:
Nanda: [20, 0, 0]
Lissy: [10, 0, 0]
// relatively, for both => [1.00, 0.00, 0.00]
They are both regarded equally fitting. Nanda has 5 products more and Lissy has 5 products less of the first style, but it does not matter, because they both only supply one style and this it what matters: How far they are away from the ideal which is the customer-style.
You could also implement the logic so that you have no bias factor and be more strict when it comes to the comparison. In this case you may want to exclude some of the params.
E.g. just comparing [15, 10] and [16, 9] - in this case Sally would actually win, because she has no delta to the customer when it comes to preferences:
Sally:
[
style1 => 0.60,
style2 => 0.40
]
Peter:
[
style1 => 0.64,
style2 => 0.36
]

Why is this array_search returning 0

Consider the following:
$characterStats = [
['strength' => 500],
['dexterity' => 200],
['agility' => 1000],
['intelligence' => 1200],
['health' => 675],
];
$stat = array_search(max($characterStats), $characterStats);
echo $stat;
What I expect: ['intelligence' => 1200]
What I get: 0
Can some one help me out to achieve what I want?
Try the following:
$characterStats = array(
'strength' => 500,
'dexterity' => 200,
'agility' => 1000,
'intelligence' => 1200,
'health' => 675,
);
$stat = array_search(max($characterStats), $characterStats);
echo $stat;
I changed the way the array is declared. I believe you may need to indicate the field name you would like to search if using nested arrays with the following call:
$stat = array_search(max($characterStats), array_column($characterStats, 'KEYNAME'));
However, since each sub array has only 1 element with different "key" it may not be the best approach. For your scenario, you may need to use another approach, where you loop through each element and store the max value found.
With the array as you have it at the moment, the easiest way I can think of doing it as a standard foreach() and keep the maximum value as well as the element where it's found (save doing another search to get the full entry)...
$characterStats = [
['strength' => 500],
['dexterity' => 200],
['agility' => 1000],
['intelligence' => 1200],
['health' => 675],
];
$maxStat = null;
$max = null;
foreach ( $characterStats as $stat ){
if ( current($stat) > $max ) {
$max = current($stat);
$maxStat = $stat;
}
}
print_r( $maxStat);

tried to bind parameter number 2101. sql server supports a maximum of 2100 parameters

I am trying to run some migrations through phinx to MSSQL, when I execute my query turns out getting the following message "tried to bind parameter number 2101. sql server supports a maximum of 2100 parameters"
I have been looking for a solution for like 3 days, does anyone know, what can be the problem and how can I fix it? could be a problem related to my php migration file? or something about my phinx configuration?
This is part of my migration file:
$rows =
[
[
'COLUMNA' => 111,
'COLUMNB' => 101,
'COLUMNC' => '-1',
'COLUMND' => '',
'COLUMNE' => 'ERROR',
'COLUMNF' => NULL,
'COLUMNG' => 1,
'COLUMNH' =>0,
'COLUMNI' => 10002,
'COLUMNJ' => '2017-11-12 00:00:00.000',
'COLUMNK' => -1,
'COLUMNM' => -1
],
[ 'COLUMNA' => 112,
'COLUMNB' => 101,
'COLUMNC' => '-1',
'COLUMND' => '',
'COLUMNE' => 'ERROR',
'COLUMNF' => NULL,
'COLUMNG' => 1,
'COLUMNH' =>0,
'COLUMNI' => 10002,
'COLUMNJ' => '2017-11-12 00:00:00.000',
'COLUMNK' => -1,
'COLUMNM' => -1
],
]
$X = $this->table('MYTABLE');
$X->insert($rows);
$X->saveData();
Thanks a lot
You can chunk your data with php easily. If you want you can also determine a good chunksize by checking your data size.
$chunk_size = floor(2100 / count($rows[0]));
foreach (array_chunk($rows, $chunk_size) as $data_chunk ) {
$X->insert($data_chunk)->saveData();
}
Obviously you are trying to bind too many parameters, solution is to do the migrations in batches of lets say 1000 rows ~ 12000 parames according to your data.
$X = $this->table('MYTABLE');
$data = [];
for ($i = 0; $i < count($rows); i++)
{
$data[] = $rows[i];
if(i % 1000 === 0)
{
$X->insert($data);
$X->saveData();
$data = [];
}
}

Avoid multiple if statements in PHP (use array or some other option)

I am creating a conversion table using PHP and I have to check the user's input against A LOT of scenarios and I started by using if statements but this doesn't seem to be efficient whatsoever and I was hoping for an easier way to go through all the scenarios.
I looked into ternary and switch options but those don't seem to do what I need it to do and I also considered array's (the option I think I need to use)
What I'm trying to do:
The user enters in a grade level and scores for a category. Based on the sum of those scores and the grade level, I need to compare them to get two other scores
Example code:
if ($grade == 1 && $sumScore <= 5)
{
$textscore = 'Beginning';
}
if ($grade ==1 && ($sumScore>5 && $sumScore <=8))
{
$textScore = 'Intermediate';
}
etc....
There are 13 grades (K-12) and 4 categories I need to go through all with their own "raw scores" to consider to get these other scores. How can I avoid using a ton of If/Else if statements?
Thanks!!
You could use a two-dimensional array that's 13x4. Then you can use a nested for loop to go through each possibility and just have one statement that gets run a bunch of times because of the for loops.
For example, the array might look like this:
$textscores = array (
1 => array(5 => 'Beginning', 8 => 'Intermediate', ...),
...
3 => array(5 => 'Intermediate', ...),
...
);
The nested for loop might look like this:
foreach($textscores as $grade => $scores) {
foreach($scores as $sumScore => $textScore) {
if($userGrade == $grade && $userSumScore <= $sumScore) {
$userTextScore = $textScore;
break 2;
}
}
}
I haven't tested this (sorry), but I think something like this
function getTextScore($grade, $sum) {
$rules = array( array("grade" => 1, "minSum" => null, "maxSum" => 5, "textScore" => "Beginning"),
array("grade" => 1, "minSum" => 6, "maxSum" => 8, "textScore" => "Intermediate" ),
/* ... */
);
for ($ruleIdx=0; $ruleIdx<count($rules); $ruleIdx++) {
$currentRule = $rules[$ruleIdx];
if (($currentRule['grade'] == $grade) &&
((is_null($currentRule['minSum'])) || ($currentRule['minSum'] <= $sum)) &&
((is_null($currentRule['maxSum'])) || ($currentRule['maxSum'] >= $sum))) {
return $currentRule['textScore'];
}
}
// got to the end without finding a match - need to decide what to do
}
The rules have optional min and max values. It will stop as soon as it finds a match, so the order is important. You will need to decide if no rules are matched. You should be able to just drop extra rules in or change the existing ones without changing the logic.
From your example I would suggest the following
Multidimensional array, but a bit different from the way you construct the array
// Grade => [Text => [Min,Max]]
$textScores = [
1 => [
'Beginning' => [0, 5],
'Intermediate' => [5, 8],
'Master' => [8, 10]
],
2 => [
'Beginning' => [0, 7],
'Intermediate' => [7, 8],
'Master' => [8, 10]
],
3 => [
'Beginning' => [0, 3],
'Intermediate' => [3, 6],
'Master' => [6, 10]
]
];
// Random input to test
$grade = rand(1, 3);
$sumScore = rand(0, 10);
foreach ($textScores[$grade] as $Text => $MinMax) {
if ($MinMax[0] <= $sumScore && $MinMax[1] >= $sumScore) {
$textScore = $Grade;
break;
}
}

Calculating dimensions for a shipment/stack of items

I am looking into writing a PHP function that uses a convenient method/formula to calculate the dimensions of i.e. a parcel/pallet loaded with items.
Here is an example of an array with items. Note: Some items are flagged to be sent as separate parcels. Some items may not be tilted.
$items = array(
1 => array(
'quantity' => 1,
'weight' = 1,
'dimensions' => array(80, 50, 50), // Length, Width, Height
'separate' => true, // If the item should be sent as a separate package
'tiltable' => false, // False if the item has a 'this side up' sticker
),
2 => array(
'quantity' => 3,
'weight' = 1,
'dimensions' => array(21, 15, 10),
'separate' => false,
'tiltable' => true,
),
3 => array(
'quantity' => 2,
'weight' = 1,
'dimensions' => array(18, 19, 20),
'separate' => false,
'tiltable' => true,
),
// ... and so on ...
);
Does anyone have the slightest bit of knowledge or experience from doing this? I don't want to reinvent the wheel.
The function I have in mind is something like this:
* (Syntax errors may occur) *
function build_packages($items, $max_weight=0, $max_length=0, $max_width=0, $max_height=0) {
$packages = array();
// Step through each item
foreach ($items as $item) {
// Twist and turn item. Longest side first ([0]=length, [1]=width, [2]=height)
if (!empty($item['tiltable'])) {
rsort($item['dimensions'], SORT_NUMERIC);
} else {
if ($item['dimensions'][0] < $item['dimensions'][1]) {
$item['dimensions'] = array($item['dimensions'][1], $item['dimensions'][0], $item['dimensions'][2]);
}
}
// Validate item
if (!empty($max_weight) && $item['weight'] > $max_weight) return false;
if (!empty($max_length) && $item[0] > $max_length) return false;
if (!empty($max_width) && $item[1] > $max_width) return false;
if (!empty($max_height) && $item[2] > $max_height) return false;
// Step through quantities
for ($i=0; $i<$item['quantity']; $i++) {
// Step through packages
$package_found = false;
foreach (array_keys($packages) as $key) {
// Skip to next package on certain conditions
if ($packages[$key]['separate']) continue;
// ...
// Do some logic
// ...
// Modify package
$package_found = true;
$packages[$key]['num_items']++;
$packages[$key]['weight'] += $item['weight'];
$packages[$key]['dimensions'] = array(0, 0, 0); // <--- Replace with new dimensions
// Twist and turn package. Longest side first ([0]=length, [1]=width, [2]=height)
if (!empty($item['tiltable'])) {
rsort($packages[$key]['dimensions'], SORT_NUMERIC);
} else {
if ($packages[$key]['dimensions'][0] < $packages[$key]['dimensions'][1]) {
$packages[$key]['dimensions'] = array($packages[$key]['dimensions'][1], $packages[$key]['dimensions'][0], $packages[$key]['dimensions'][2]);
}
}
break;
}
if ($package_found) continue;
// Add to a new package
$packages[] = array(
'num_items' => 1,
'weight' => $item['weight'],
'dimensions' => $item['dimensions'],
'separate' => $item['separate'],
'tiltable' => $item['tiltable'],
);
}
}
return $packages;
}
Care to help out with some code?
What you are after is a solution to the Bin Packing problem. The solution is NP Hard so you may want to find a "good enough" solution, rather than the optimal solution. A google search turned up this: One Dimensional Bin Packing class. I did not look at the code, so I am not sure how good it is, but I suggest it as a place to at least start your investigation. If it turns out to be good enough for one dimensional problems, perhaps you can create a solution creating multiple 1 dimensional layouts and keep track of each row's max height, therefor knowing whether you have exceeded the total package height.
Hope this helps.
I found this Package https://github.com/dvdoug/BoxPacker and this service https://3dbinpacking.com/ so far that does this for you.

Categories