Choose the highest combo - javascript

I am looking for a neat way to do the following but have a brain freeze:
User can add (and subsequently remove) a type to a shopping basket.
For each add or remove, I need to see if there is a type higher in the hierarchy that can be suggested instead. The highest type is preferable (note it has the same price as the penultimate type but b6 should be preferred over b5)
Examples:
2 x type1 (100+100) -> 1 x type2
type1 + type3 (100+300) -> 1 x type4
type3 + type4 (300+400) -> 1 x type6 + 1 x type2
1 type6 + 1 type2 + 1 type1 -> 1 x type6 + 1 x type3
and so on.
So if the total is 0, just add whatever is clicked.
If not, see if the total plus the new click can be resolved in a better combination starting from the top. If user declines the suggestion, ignore the add.
Code so far (tagged jQuery since the code is using it - map and filter are welcome)
I have not posted my attempts of the recalc, since I want to avoid the X/Y problem.
jsFiddle
var types= {
b1: {price:100,quantity:0},
b2: {price:200,quantity:0},
b3: {price:300,quantity:0},
b4: {price:400,quantity:0},
b5: {price:500,quantity:0},
b6: {price:500,quantity:0}
}
function getTotal() {
var total = 0;
$.each(types,function(typeId,type) {
total+=type.quantity*type.price;
});
return total
}
$(function() {
var cont = $("#container");
$.each(types,function(typeId,type) {
$('<button class="add" id="'+typeId+'add">Add to '+typeId+'<button><span id="'+typeId+'val">0</button><button class="remove" id="'+typeId+'remove">Remove from '+typeId+'</button><span id="'+typeId+'total">0</span><br/>').appendTo(cont);
});
$(".add").on("click",function() {
var id = this.id.replace("add",""),type=types[id];
type.quantity++;
var subTotal = type.quantity*type.price;
$("#"+id+"val").text(type.quantity);
$("#"+id+"total").text(subTotal);
$("#total").text(getTotal());
});
$(".remove").on("click",function() {
var id = this.id.replace("remove",""),type=types[id];
if (type.quantity>0) type.quantity--;
var subTotal = type.quantity*type.price;
$("#"+id+"val").text(type.quantity);
$("#"+id+"total").text(subTotal);
$("#total").text(getTotal());
});
});

It's a little confusing what you are after. Try this:
//Sort the keys by price descending, and get the current total
var remaining = getTotal(),
keys = Object.keys(types).sort(function(a,b){
return b.price > a.price ? -1 : 1;
});
// Loop through the keys (sorted by price).
// Set quantity based on how many times the remainder goes into the price
// and update the remainder
keys.forEach(
function(k){
var x = Math.floor(remaining/types[k].price);
types[k].quantity = x;
remaining -= x * types[k].price;
});
http://jsfiddle.net/GC3mW/

The problem as you have described it sounds remarkably similar to the coins-change algorithm.
It can be made to work with a rather simple greedy loop, going somewhere along these lines:
while(total > 0) {
total = total - [get largest price less than total]
[increment amount for above largest price]
}
Here's your fiddle with that update: http://jsfiddle.net/DnRG9/1/
This doesn't handle any corner cases, so it's possible I've missed something. Let me know if it helps

Related

How to divide a value and distribute it on my html page?

I have two values on my html page, that I got from a form.
The person completes the amount of money they have on one input, and on the other input, the number of people.
So, my question is:
How do i divide the amount.value by the people.value, and distribute it, in a way it appears as shown in the example below?
Amount: 150 / Number of People: 3
-Person 1 - 50
-Person 2 - 50
-Person 3 - 50
What i'm actually struggling with, is to create a function that will add to the HMTL another person + the result, depending on the number of people added previously.
The code down below just finds the total share of all people and distributes the amount among them dependin on their shares.
/*----- Define your variables in here ------*/
var amount = 150;
var people = {
1: 0.75,
2: 0.15,
3: 0.10
}
var totalSharePerc = 0;
/*----- Loop thruogh people to calculate total sum ------*/
for (const [key, value] of Object.entries(people)) {
totalSharePerc = totalSharePerc + value;
}
/*----- Loop thruogh people and split the amount ------*/
for (const [key, value] of Object.entries(people)) {
/*------ Calculate share of person -----*/
var share = (amount*value/totalSharePerc);
/*----- Do whatever you want ------*/
console.log("Person"+key+" => "+share);
}
You can use Array#reduce() to calculate the total share for every person involved.
This assumes you have a mapping defined of which person has to cover which share (which you will need to have if you want to distribute in non-equal shares).
const amount = 150;
// define mapping of percentages to persons
const sharesPerPersonPct = {
1: 0.75,
2: 0.15,
3: 0.1,
};
// create a mapping of total values to persons
const sharesPerPersonTotal = Object.entries(sharesPerPersonPct).reduce(
(allShares, [personId, share]) => {
allShares[personId] = {
pct: share, // add percentage (optional)
total: amount * share // add total share
}
return allShares;
},
{}
);
console.log("Resulting JS object:")
console.log(sharesPerPersonTotal);
Object.entries(sharesPerPersonTotal).forEach(([personId, share]) => console.log(`Person ${personId} has to cover ${(share.pct * 100).toFixed(2)}% which amounts to ${share.total}$.`))
Updated answer to reflect your edit
The following is for an equal distribution of an amount to a number of people. The challenge is that e.g 10$ cannot be distributed 3.33$ for each of 3 persons as then penny would be missing. This is the sort of stuff you get when using floating point arithmetic. To prevent that use integer arithmetic instead. So multiply 10$ by 100 so you get 1000p and you can then assign each person their floored share (Math.floor(1000 / 3) = 333) use modulo to get the remainder (10 % 3 = 1) and distribute that remainder among the persons involved. The current implementation isn't quite fair either though because it always assigns that one penny more starting from the front, but you could use something like this to account for that.
The rest is just input validation using RegEx and displaying the results doing some DOM manipulation.
function handleUpdateDistribution() {
const amountMoney = document.getElementById("amount-money");
const noPersons = document.getElementById("no-persons");
if (!isMoneyValid(amountMoney.value)) {
console.log("Money value can only have two decimal places!");
return;
}
if (!isNoPersonValid(amountMoney.value)) {
console.log("Number of persons must be an integer greater than one!");
return;
}
const distribution = updateDistribution(
Number.parseInt(noPersons.value),
Number.parseFloat(amountMoney.value)
);
showDistribution(distribution);
}
function isMoneyValid(money) {
const matches = money.match(/^[0-9]+(\.[0-9]{1,2})?$/g);
if (matches === null) return null;
else return matches[0];
}
function isNoPersonValid(noPersons) {
const matches = noPersons.match(/[1-9]*/g);
if (matches === null) return null;
else return matches[0];
}
function showDistribution(distribution) {
const list = document.createElement("ul");
const listItems = Object.entries(distribution).map(([personId, share]) => {
const item = document.createElement("li");
item.textContent = `Person ${personId} has to cover ${share}$.`;
return item;
});
list.append(...listItems);
document.getElementById("result").replaceChildren(list);
}
/**
*
* #param {number} noPersons number of persons to split between
* #param {number} amountMoney amount of money to split
*/
function updateDistribution(noPersons, amountMoney) {
// use integer arithmetic as floating point arithmetic is not very suitable for task at hand
amountMoney *= 100;
const share = Math.floor(amountMoney / noPersons);
let remainder = amountMoney % noPersons;
const persons = {};
for (let i = 1; i <= noPersons; i++) {
const shareInInts = share + (remainder > 0 ? 1 : 0);
remainder--;
persons[i] = (shareInInts / 100).toFixed(2);
}
return persons;
}
window.addEventListener("DOMContentLoaded", (e) => {
const amountMoney = document.getElementById("amount-money");
const noPersons = document.getElementById("no-persons");
amountMoney.addEventListener("input", handleUpdateDistribution);
noPersons.addEventListener("input", handleUpdateDistribution);
});
input:invalid {
border: red solid 3px;
}
<form>
<input id="no-persons" placeholder="Number of persons" type="number" required />
<input id="amount-money" placeholder="Amount of money" type="text" pattern="^[0-9]+(\.[0-9]{1,2})?$" required />
</form>
<div id="result"></div>
Please note you can and should do more to give the user a nice user experience (Show popovers instead of console.log(), nicer styling etc). See MDN docs on form validation. You should also probably restrict the number of persons as rendering thousands of list items will seriously impact the performance.

Having trouble with a Javascript adding system (simple but I'm slow)

So I am trying to make a program here where I press a button and once its pressed I input a number (from 1-3) and then it outputs the inputted value.
Then you can press the button again and add another value (again from 1-3) and it adds the second input to the first input and so on.
This is the code I've got so far and it doesn't work, it just outputs my inputted value and that's it. Nothing gets added and updated.
<script type="text/javascript">
function addone()
{
x=parseInt(prompt("What goal importance did you complete?"));
var sum = 0;
if (x === 1)
{
sum = sum + x;
}
else if (x=== 2)
{
sum = sum + x;
}
else if (x=== 3)
{
sum = sum + x;
}
document.getElementById("myBtn").innerHTML = x;
}
</script>
The button and the ouput are with:
<button onclick="addone()">Coins</button>
<h1>Coins:</h1>
<h4 id='myBtn'>0</h4>
As pointed out by #Aplet123, each time you execute the addOne function, you restart the sum as 0.
The simplest way is to initialize it outside the function.
By the way, there is no need for the elseif conditions in your code, regarding the feature you want to achieve, the best, for readability should be to use a AND condition :
<script type="text/javascript">
var sum = 0;
function addone()
{
x=parseInt(prompt("What goal importance did you complete?"));
if (x >= 1 && x <=3)
{
sum = sum + x;
}
document.getElementById("myBtn").innerHTML = sum;
}
</script>
You have to fix one thing before moving on. You are instantiating the sum as 0 whenever you run the function. To fix this, declare the sum outside of the function. You can reset it if you want later in the program if you like.
Next, you want to accept only 1, 2 or 3 as the input. You can shorten the if else condition so that it checks that input is more than or equal to 1, but less than or equal to 3.
Finally - You are adding the values of input to the sum variable. The variable x is not changing itself. So, you will always see the number you input. Change the innerhtml value to sum.
EDIT - I have also included comments in the code below -
var sum = 0; //Declare sum outside the function
function addone(){
let x=parseInt(prompt("What goal importance did you complete?"));
if (x >= 1 && x <=3){ //Shorten the if else condition
sum += x;
}
document.getElementById("myBtn").innerHTML = sum; //Display the sum
}

Function only removes items from array but does not change values

I have created a class that takes computer parts and their costs and calculates them based on what parts are chosen. At the moment I have two functions, one to add more parts to the quote and one to remove parts. It works correctly in the sense of removing or adding items, but does not change the total cost. When I remove parts, the price remains the same, likewise if I add more parts. What could I do to get this working as expected. Here is the code:
class PriceCalc {
Motherboard = 520.99;
RAM = 250.4;
SSD = 500.8;
HDD = 400.66;
Case = 375.5;
Monitor = 600.75;
Keyboard = 100.99;
Mouse = 25.5;
constructor(Obj) {
this.parts = Obj;
this.cost = "$" + Obj.reduce((a, b) => a + this[b], 0).toFixed(2);
this.retail ="$" +(Obj.reduce((a, b) => a + this[b], 0) +Obj.reduce((a, b) => a + this[b], 0) * 1.75).toFixed(2);
this.quote = "Your quote is " + this.retail;
}
add(item) {
this.parts = [...this.parts, item];
}
remove(item) {
this.parts = this.parts.filter((x) => x !== item);
}
}
quote4 = new PriceCalc(["RAM", "SSD", "Case", "Mouse"]);
console.log(quote4.parts);//Returns ["RAM", "SSD", "Case", "Mouse"]
console.log(quote4.cost);//Returns $1152.20
console.log(quote4.retail);//Returns $3168.55
console.log(quote4.quote);//Returns "Your quote is $3168.55"
quote4.remove("Case")
console.log(quote4.parts);//Returns ["RAM", "SSD", "Mouse"]
console.log(quote4.cost);//Returns $1152.20
console.log(quote4.retail);//Returns $3168.55
console.log(quote4.quote);//Returns "Your quote is $3168.55"
At the moment this.cost/retail/quote doesnt change when things are added or removed, whereas they should be modified if items are added or removed. The only way I can change the values at the moment is by manually changing the parts within the called array. How could I fix this?
You need to recalculate them whenever a new item gets added or an item is removed. Since they need to be recalculated from different places (the constructor, add and remove), a new method (called calculate or update for example) is perfect for this as reusable code should be grouped in a function/method so you don't repeat yourself.
Also, cost and retail should be numbers instead of strings. You are using reduce to calculate cost, then using the same reduce twice to calculate retail which should be avoided. Just calculate cost and use cost to calculate retail.
Finally, add and remove should not create new arrays each time an item is removed or added.
class PriceCalc {
Motherboard = 520.99;
RAM = 250.4;
SSD = 500.8;
HDD = 400.66;
Case = 375.5;
Monitor = 600.75;
Keyboard = 100.99;
Mouse = 25.5;
constructor(initialParts) { // always choose good names for your variables. initialParts makes more sense than Obj
this.parts = initialParts; // store the initial parts
calculate(); // calculate cost, retail and quote
}
add(item) {
this.parts.push(item); // use push instead of creating a new array (which is an overkill)
calculate(); // recalculate cost, retail and quote
}
remove(item) {
this.parts = this.parts.filter((x) => x !== item); // I'd rather use this https://stackoverflow.com/a/5767357 over filter but filter is shorter so OK
calculate(); // recalculate cost, retail and quote
}
calculate() { // the method that calculates cost, retail and quote
this.cost = this.parts.reduce((a, b) => a + this[b], 0); // calculate cost and store as a number instead of a string
this.retail = this.cost + this.cost * 1.75; // calculate retail using the value of this.cost and also store as a number (you can use this.retail = 2.75 * this.cost; which is the same)
this.quote = "Your quote is $" + this.retail.toFixed(2); // generate the quote (notice the added $ sign and the call to toFixed(2))
}
}

How do I return the total of this object array?

I need to take the price and tax of these and return the total of everything. I'm learning so I apologize for the simple question.
const orders = [{"price":15,"tax":0.09},{"price":42,"tax":0.07},{"price":56,"tax":0.11},
{"price":80,"tax":0.11},{"price":69,"tax":0.06},{"price":68,"tax":0.14},
{"price":72,"tax":0.14},{"price":51,"tax":0.09},{"price":89,"tax":0.15},
{"price":48,"tax":0.13}];
// Do not edit code above.
/*
Use a higher-order method to get the sum of all the order totals after adding in the sales tax
*/
var ordersTotal = orders.reduce(function(total, num) {
return total + num;
})
ordersTotal;
Simply use Array.reduce() and Object destructing. And please make sure to pass 0 as the initial value to your reduce function.
const orders = [{"price":15,"tax":0.09},{"price":42,"tax":0.07},{"price":56,"tax":0.11},{"price":80,"tax":0.11},{"price":69,"tax":0.06},{"price":68,"tax":0.14},{"price":72,"tax":0.14},{"price":51,"tax":0.09},{"price":89,"tax":0.15},{"price":48,"tax":0.13}];
const result = orders.reduce((a,{price,tax})=>a+price+tax,0);
console.log(result);
You need to give reduce something to start with, in this example 0 is probably a good start. Then each num passed to reduce will be an object. Currently you're just adding the object like total = total + {"price":15,"tax":0.09} and that doesn't work. You need to look at each property you want to add. It's not clear if tax is a percentage or a total amount. Below we'll just add it, but it should be clear how to add as a percentage if you want.
const orders = [{"price":15,"tax":0.09},{"price":42,"tax":0.07},{"price":56,"tax":0.11},{"price":80,"tax":0.11},{"price":69,"tax":0.06},{"price":68,"tax":0.14},{"price":72,"tax":0.14},{"price":51,"tax":0.09},{"price":89,"tax":0.15},{"price":48,"tax":0.13}];
var ordersTotal = orders.reduce(function(total, num) {
return total + num.price + num.tax; // add properties
}, 0) // start with 0
console.log(ordersTotal);
Make sure you start with a zero so it doesn't try to coerce the result into a string.
const orders = [{"price":15,"tax":0.09},{"price":42,"tax":0.07},{"price":56,"tax":0.11},
{"price":80,"tax":0.11},{"price":69,"tax":0.06},{"price":68,"tax":0.14},
{"price":72,"tax":0.14},{"price":51,"tax":0.09},{"price":89,"tax":0.15},
{"price":48,"tax":0.13}];
// Do not edit code above.
var ordersTotal = orders.reduce(function(total, order) {
return total + order.price + order.tax;
},0)
console.log(ordersTotal,ordersTotal.toFixed(2))

Most efficient way to pick unique strings from weighted categories

The setup:
n categories
each category has a weight (an artificial number that indicates the importance of that category)
Each string is globally unique
Example:
Categories: a, b, c
Strings:
a_001, a_002, a_003
b_001, b_002, b_003
c_001, c_002, c_003
Weights:
a: 1
b: 2
c: 1
The task:
Get an array of unique strings, 1 from category a, 2 from category b and 1 from category c. The number of picked strings from a category doesn't have to exactly line up with the weightedNumber. The weights should just be considered (strongly though) while picking the strings. However, if it's not possible, then it's not the end of the world. The number of strings picked total however, MUST be correct.
The problems:
The weight could be 10, but there are only 3 unique strings in that category (in that case, it should be filled up with strings from the other categories based on their weights)
I'm working with firestore, so I can only pick one random string at a time and I don't have access to the number of strings in a given category
My attempt:
function pickStrings(numberOfStrings, arrCategories) {
// Calculates the weight of each category
// Sets initial weight and stringsleft to weightTotal and numberOfStrings
// Iterates over the categories:
// selectedStrings.push(...pickStringsFromCategory(calculatedNumberBasedOnWeight, categoryId))
// returns selectedStrings
}
function pickStringsFromCategory(numberOfStrings, categoryid) {
// Create a map of picked strings
// Randomly pick a string from that category
// Checks if the string was picked already
// Tries again (up to 10 times) if the string was already picked
}
However, that just doesn't feel right. Trying 10 times is an artificial number and if the category with only 1 string and weightedNumber of 10 is the last one, the number of strings picked is less than the numberOfStrings.
Any ideas on how to improve this algorithm?
Here a possible approach:
var arr = [
["a_001", "a_002", "a_003"],
["b_001", "b_002", "b_003"],
["c_001", "c_002", "c_003"]
];
var weights = [7, 2, 1];
var str = "";
weights.map((o, i) => {
let curr = i;
let p = 0;
for (let j = 0; j < o; j++) {
if (arr[curr][p]) {//this could be and ajax, function, whatever
str += arr[curr][p] + " ";//this is an assumption
p++;
} else { //this happens when we didn't find a string into such category
curr = curr + 1; //we move one category
p = 0;//firs element in the next category
j--;//move back because we didn't finish
}
}
})
console.log(str);

Categories