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
]
Related
I have an array with recipes from 3 categories, breakfast, lunch and dinner. Each of these categories, have 10 unique recipes.
$recipes = [
'breakfast' => [
0 => [
'title' => 'eggless waffles',
'calorie' => 210,
],
1 => [
'title' => 'blueberry oatmeal',
'calorie' => 161,
],
...
],
'lunch' => [9],
'dinner' => [9]
];
I'd like to sort and create a combination of 3 recipes for each day
$days = array_fill(0, 6, [1 => [], 2 => [], 3 => []]);
Each recipe has a calorie amount, and each final day should have a combination (consists of 1 breakfast, 1 lunch and 1 dinner) with recipes that was ordered by whichever 3 recipe combo hit closest to 500
For example, if day 1 combined recipes (breakfast, lunch and dinner) calorie totaled 660, and day 2 was 400. It's possible that switching breakfast from day 2, to day 1 might make both of them hit closest to 500, however it's possible that switching day 3 breakfast to day 1, and day 2 to day 3 might make all 3 hit closer to 500 as well.
So day 1, 2, 3, 4, 5, 6, and 7 should have 3 recipes (breakfast, lunch and dinner)
$final = [
0 => [
'breakfast' => [...],
'lunch' => [...],
'dinner' => [...],
],
1 => [
'breakfast' => [...],
'lunch' => [...],
'dinner' => [...],
],
2 => [
'breakfast' => [...],
'lunch' => [...],
'dinner' => [...],
],
...
];
It's been days since I've reached an impasse, and I cannot figure out how to go about sorting these arrays into a combination of 3 for each day. (I know I'm not providing a lot of code to go off of)
Edit 1:
This is what I've got so far:
class Combinations {
private $days;
public function __construct(){
$this->days = array_fill(1, 7, [1 => [], 2 => [], 3 => []]);
}
public function create(){
$median = 600;
foreach($this->days as $day => $categories){
while($this->dayIsIncomplete($day)){
$recipes = [];
foreach($categories as $category => $value){
$recipes[$category] = $this->getRandomRecipe($category);
}
// add random meals to first day
if($day === 1){
$this->days[$day] = $recipes;
continue;
}
foreach($recipes as $category => $recipe){
foreach($this->days as $dayKey => $mealsArray){
$originalMacros = $this->totalMacros($mealsArray);
// remove $recipe category from mealsArray, and merge it ($recipe)
$filteredMacros = $this->totalMacros(array_merge([$recipe], array_filter($mealsArray, function($key) use($category){
return $key !== $category;
}, ARRAY_FILTER_USE_KEY)));
// if original is not closer to median
if(($originalMacros - $median) * ($originalMacros - $median) < ($filteredMacros - $median) * ($filteredMacros - $median)){
// flip current recipes
// switch D2B ($recipe) with D1B
}
}
}
}
}
}
public function getRandomRecipe(int $category){
$recipes = []
if($category === 1){
$recipes[] = ['id' => 1, 'calorie' => 310];
$recipes[] = ['id' => 2, 'calorie' => 360];
$recipes[] = ['id' => 3, 'calorie' => 450];
$recipes[] = ['id' => 4, 'calorie' => 330];
$recipes[] = ['id' => 5, 'calorie' => 220];
$recipes[] = ['id' => 6, 'calorie' => 390];
$recipes[] = ['id' => 7, 'calorie' => 400];
$recipes[] = ['id' => 8, 'calorie' => 320];
$recipes[] = ['id' => 9, 'calorie' => 460];
}
if($category === 2){
$recipes[] = ['id' => 10, 'calorie' => 420];
$recipes[] = ['id' => 11, 'calorie' => 360];
$recipes[] = ['id' => 12, 'calorie' => 450];
$recipes[] = ['id' => 13, 'calorie' => 310];
$recipes[] = ['id' => 14, 'calorie' => 320];
$recipes[] = ['id' => 15, 'calorie' => 490];
$recipes[] = ['id' => 16, 'calorie' => 440];
$recipes[] = ['id' => 17, 'calorie' => 520];
$recipes[] = ['id' => 18, 'calorie' => 560];
}
if($category === 3){
$recipes[] = ['id' => 19, 'calorie' => 510];
$recipes[] = ['id' => 20, 'calorie' => 660];
$recipes[] = ['id' => 21, 'calorie' => 750];
$recipes[] = ['id' => 22, 'calorie' => 610];
$recipes[] = ['id' => 23, 'calorie' => 580];
$recipes[] = ['id' => 24, 'calorie' => 690];
$recipes[] = ['id' => 25, 'calorie' => 710];
$recipes[] = ['id' => 26, 'calorie' => 620];
$recipes[] = ['id' => 27, 'calorie' => 730];
}
return $recipes[array_rand($recipes)];
}
public function dayIsIncomplete($day){
return !empty($this->days[$day][1]) && !empty($this->days[$day][2]) && !empty($this->days[$day][3]);
}
public function totalMacros($array){
$total = 0;
foreach ($array as $key => $value) {
$total += $value['calorie'];
}
return $total / 2;
}
}
Edit 2:
I'm trying to figure out what algorithm best fits to sort this issue out. I think using a bipartite matching (maximum) algorithm might be what I need.
Edit 3:
Thank you all for taking the time to help, I haven't forgotten about the answers. I had to put this aside for the time being, however soon enough I'll get to it, and the accepted answer will get my remaining 300 bounty.
So I tested a genetic algorithm and it works. I used Jenetics, a Java library (it's not PHP, sorry, but PHP is not suited to heavy computations anyway).
I took 1400 calories as the daily target.
The function to be minimized is the mean squared error.
Here's the code:
import java.util.ArrayList;
import io.jenetics.*;
import io.jenetics.engine.*;
import io.jenetics.util.*;
public class Recipes
{
private static final int TARGET = 1400;
private static final int DAYS = 7;
private static class Recipe
{
public int id;
public int calories;
public Recipe(int id, int calories)
{
this.id = id;
this.calories = calories;
}
}
private static ISeq<Recipe> getSeq(int[] ids, int[] calories)
{
ArrayList<Recipe> list = new ArrayList<>();
for(int i=0;i<ids.length;i++)
list.add(new Recipe(ids[i], calories[i]));
return ISeq.of(list);
}
private static double meanSquareError(Genotype<EnumGene<Recipe>> gt)
{
int err = 0;
for(int d=0;d<DAYS;d++)
{
int calories = 0;
for(int m=0;m<3;m++)
calories += gt.get(m).get(d).allele().calories;
err += (calories-TARGET)*(calories-TARGET);
}
return err / (double)DAYS;
}
public static void main(String[] args)
{
ISeq<Recipe> recipes1 = getSeq(new int[]{ 1, 2, 3, 4, 5, 6, 7, 8, 9}, new int[]{310, 360, 450, 330, 220, 390, 400, 320, 460});
ISeq<Recipe> recipes2 = getSeq(new int[]{10, 11, 12, 13, 14, 15, 16, 17, 18}, new int[]{420, 360, 450, 310, 320, 490, 440, 520, 560});
ISeq<Recipe> recipes3 = getSeq(new int[]{19, 20, 21, 22, 23, 24, 25, 26, 27}, new int[]{510, 660, 750, 610, 580, 690, 710, 620, 730});
Factory<Genotype<EnumGene<Recipe>>> gtf = Genotype.of(
PermutationChromosome.of(recipes1, DAYS),
PermutationChromosome.of(recipes2, DAYS),
PermutationChromosome.of(recipes3, DAYS)
);
Engine<EnumGene<Recipe>, Double> engine = Engine
.builder(Recipes::meanSquareError, gtf)
.optimize(Optimize.MINIMUM)
.populationSize(50)
.alterers(new SwapMutator<>(0.2), new PartiallyMatchedCrossover<>(0.2), new Mutator<>(0.01))
.build();
Phenotype<EnumGene<Recipe>, Double> result = engine.stream()
.limit(20000)
.collect(EvolutionResult.toBestPhenotype());
for(int m=0;m<3;m++)
{
for(int d=0;d<DAYS;d++)
{
Recipe r = result.genotype().get(m).get(d).allele();
System.out.print(String.format("%2d (%d) ", r.id, r.calories));
}
System.out.println();
}
System.out.println("MSE = " + result.fitness());
}
}
A genetic algorithm is non-deterministic so it gives a different result each time. The best solution I could get is:
3 (450) 4 (330) 5 (220) 2 (360) 7 (400) 1 (310) 8 (320)
16 (440) 15 (490) 17 (520) 10 (420) 13 (310) 11 (360) 14 (320)
19 (510) 23 (580) 20 (660) 26 (620) 24 (690) 27 (730) 21 (750)
MSE = 14.285714
It's almost perfect (all days are at 1400 calories except Sunday which has 1390).
You have:
10 breakfasts
10 lunches
10 dinners
The combinations of these are 10x10x10 = 1000 recipes.
Part 1
Calculate these recipes and their total calories each.
From each recipes `s total calories, calculate the absolute difference from the daily calories goal:
AbsoluteDifference = Abs(calories - 500)
and which breakfast, lunch and dinner it consists of.
So now e.g. you have a list:
| Recipes | AbsDiff | Breakfast | Lunch | Dinner |
| recipe 1 | 140 | 1 | 7 | 4
| recipe 2 | 135 | 4 | 8 | 3
| recipe 3 | 210 | 7 | 9 | 10
...
| recipe 1000 | 170 | 5 | 1 | 9
Part 2
This is the difficult part, by calculating all combinations of 7 recipes is gonna take too long.
The best combination is the one that has total of absDiff of its recipes the minimum:
MIN(AbsDiff(recipe1) + AbsDiff(recipe2) + AbsDiff(recipe7))
Algorithm
The idea is to calculate only a few combinations of 7 different recipes.
Initial Step
Make a guess of "how much the minimum could be about", e.g. say you think it is likely less than 350 calories.
Using this guess, you can try to calculate all the combinations of 7 recipes that have TotalCaloriesDiff < 350.
Based on:
In a collection of n positive numbers that sum up to or less than S, at least one of them will be less than S divided by n (S/n).
In this case S=350 and n=7, then at least one recipe should have AbsDiff < 350/7 = 50.
So, you could try to calculate combinations of 7 recipes with lower total differences to the guess.
Steps
Get the recipes with AbsDiff(recipe1) < 350 / 7
For each recipe1 found above, get the next recipes that have AbsDiff(recipe2) < (350 - AbsDiff(recipe1)) / 6 and they do not share any breakfast, lunch or dinner with recipe1.
continue till you get combinations of 7 recipes.
select the combination with lowest TotalCaloriesDiff
If you do not find any results, based on your guess, you raise the guess, e.g. 350 + 50 = 400.
Here is my answer to a similar problem.
I think first you should make a combination of dishes, where calorie should be near to 500, to make them unice, like:
$arr = [
0 => [
'breakfast' => 160
'lunch' => 160
'dinner' => 180
],
...
You should rebuild array like $breakfast = ['1' => 130, '2' => 150, '3' => 160, '4' => 170, '5' => 120, '6' => 100, '7' => 130] and etc.
Maybe try to compare arrays like
$final = ['1' => 152, '2' => 235, '3' => 521, '4' => 343, ... ];
And then you can grab each value from $arr ->
$final = ['1' => ['breakfast' => '1', 'lunch' => '5', 'dinner' => '2'], ...];
I think you can modify this logic as you want. Best of luck
I'm a complete beginner at PHP and I'd like to create a simple 3 for 2 campaign with the help of built in functions (such as i.e array_pop(), array_sum() or something else) and I've unfortunately hit a wall.
I've created an array with completely made up pizzas:
$pizzaArray = [
"Hackaton" => [
"price" => 135,
"ingredients" => "tomato sauce and magic",
"randomComment" => "tastes like unicorns"
],
"Plug-in" => [
"price" => 145,
"ingredients" => "sardeler och oliver",
"randomComment" => "Ripasso"
],
"Lulz" => [
"price" => 150,
"ingredients" => "tomato sauce and old socks",
"randomComment" => "tastes like old socks"
]
];
Please don't mind the weird values. The only thing that is "important" are the price arrays.
What I basically want to achieve is that if a customer were to purchase 3 pizzas, then the $totalSum will equal the total amount minus the cheapest pizza.
I'm trying to think that it'd be achieveable if I were to be able to code this:
$numberOfPizzas = count($pizzaarray);
$totalSum = 0;
if ($numberOfPizzas == 3) {
//array_pop() the cheapest pizza
// $totalSum =array_sum() price of remaining pizza
} else {
//$totalSum = array_sum() prices of all pizzas
But since I can't really think like a programmer yet my head simply won't output something that is logical. Could this be achieved in a simple scenario?
You may use uasort to sort the pizzas by price, then array_slice to take all but the cheapest, then array_reduce to compute the final price:
$pizzas = [
'Hackaton' => ['price' => 135, 'ingredients' => 'tomato sauce and magic', 'randomComment' => 'tastes like unicorns'],
'Plug-in' => ['price' => 145, 'ingredients' => 'sardeler och oliver', 'randomComment' => 'Ripasso'],
'Lulz' => ['price' => 150, 'ingredients' => 'tomato sauce and old socks', 'randomComment' => 'tastes like old socks']
];
uasort($pizzas, static function (array $pizza1, array $pizza2): int {
return $pizza1['price'] <=> $pizza2['price'];
});
$pizzasMinusCheapest = array_slice($pizzas, 1);
// or, if you want the discount to apply for every 3 pizzas,
// $pizzasMinusCheapest = array_slice($pizzas, (int)(count($pizzas) / 3))
$finalPrice = array_reduce($pizzasMinusCheapest, static function (int $total, array $pizza): int {
return $total + $pizza['price'];
}, 0);
echo $finalPrice;
Demo: https://3v4l.org/gYKdY
Bonus - using PHP 7.4's short closures:
uasort($pizzas, fn(array $pizza1, array $pizza2): int => $pizza1['price'] <=> $pizza2['price']);
$pizzasMinusCheapest = array_slice($pizzas, 1);
$finalPrice = array_reduce($pizzasMinusCheapest, fn(int $total, array $pizza): int => $total + $pizza['price'], 0);
I have two arrays which I can get after form submit:
$product_id = $request->get('product_id');
// [1, 3, 4]
$quantity = $request->get('quantity');
// [5, 1, 2]
Now I want to submit this arrays into database where I want to pick the purchase_price from product database. I'm not sure how to assign product_id to product_quantity (index 0 to 0, 1 to 1, 2 to 2) and store into database.
Sample data to store into carts:
[1 5 120 ],
[3 1 230 ],
[4 2 340 ],
foreach ($product_id as $product)
{
}
DB::table('carts')->insert(
['product_id' => '',
'quantity' => 0,
'purchase_price' =>
]
);
Just for clarification:
product_id and quantity come from dynamic input box means number of product_id and quantity are same but it could be n times as user wanted. So I store it as arrays.
Now from this array I wanted to store it in database where I want to store with product_id with quantity.
Lets give you some suggetions:
If you have below array - if not then make it array like below:
$dataset = [
0 => [
'product_id' => 1,
'quantity' => 5,
'purchase_price' => 120,
],
1 => [
'product_id' => 3,
'quantity' => 1,
'purchase_price' => 230,
],
2 => [
'product_id' => 4,
'quantity' => 2,
'purchase_price' => 340,
]
];
Now you have to write INSERT query for this:
$result = Cart::insert($dataSet);
if ($result)
return true;
else
return false;
You will get an idea how to do it after seeing above code...good luck
Please check out this sample.
you can parse 2d array and convert it to json to store in the database then decode back:
$product_id = [1,2,3];
// [1, 3, 4]
$quantity = [5,1,2];
// [5, 1, 2]
$output=[120,230,340];
$out=[];
for ($i=0; $i < count($product_id); $i++) {
$out[$i]=[$product_id[$i],$quantity[$i],$output[$i]];
}
dd(json_encode($out));
output:
"[[1,5,120],[2,1,230],[3,2,340]]"
You can use
foreach ($product_id as $key=>$product)
{
//select purchase price from table by using the $product value
$purchase_price = *your select code here*
DB::table('carts')->insert([
'product_id' => $product,
'quantity' => $quantity[$key],
'purchase_price' => $purchase_price
]);
}
Let me know if not works
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;
}
}
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.