How to add/remove elements from array based on array contents - javascript

I've been struggling with this piece for a few days and I can't seem to find what's wrong. I have an array with a few objects:
myMainPostObj.categories = [Object, Object]
This is for add/removing categories to a post. Imagine I'm editing an existing post which is already associated with a couple of categories (as per code above).
I also have an array which has all categories in the db (including the ones which are associated with the post). On my js controller I have the following code:
$scope.addCategory = function (cat) {
for (var i in $scope.post.category_ids) {
if (cat._id === $scope.post.category_ids[i]) {
$scope.post.category_ids.slice(i, 1);
} else if (cat._id !== $scope.post.category_ids[i]) {
$scope.post.category_ids.push(cat._id);
}
}
}
The above function is called every time the user click on a category. The idea is for the above function to loop through the categories within the post (associated with the post) and compares it with the category passed as argument. If it matches the function should remove the category. If it doesn't it should add.
In theory this seems straight forward enough, but for whatever reason if I tick on category that is not associated with the post, it adds two (not one as expected) category to the array. The same happens when I try to remove as well.
This is part of a Angular controller and the whole code can be found here

The error in your code is that for each iteration of the loop you either remove or add a category. This isn't right... You should remove if the current id matches but add only if there was no match at all. Something like this:
$scope.addCategory = function (cat) {
var found = false;
for (var i in $scope.post.category_ids) {
if (cat._id === $scope.post.category_ids[i]) {
$scope.post.category_ids.splice(i, 1); // splice, not slice
found = true;
}
}
if (!found) // add only if it wasn't found
$scope.post.category_ids.push(cat._id);
}

I guess the problem could be that you're altering the category_ids array while you're iterating over it with the for loop. You might be better off trying something like this:
$scope.addCategory = function (cat) {
var catIndex = $scope.post.category_ids.indexOf(cat._id);
if (catIndex > -1)
$scope.post.category_ids.splice(catIndex, 1);
else
$scope.post.category_ids.push(cat._id);
}
Note that indexOf doesn't seem to be supported in IE7-8.

Let's simplify this a bit:
const CATEGORIES = [1, 2, 3];
let addCategory = (postCategories, categoryId) => {
CATEGORIES.forEach((cId, index) => {
if (postCategories[index] === cId) console.log("Removing", categoryId);
else console.log("Adding", categoryId);
});
return postCategories;
};
Please ignore the fact that we actually are not mutating the array being passed in.
A is either equal or not equal to B - there is no third option (FILE_NOT_FOUND aside). So you are looping over all of your categories and every time you don't find the category ID in the array at the current index you add it to the postCategories array.
The proper solution to your problem is just to use a Set (or if you need more than bleeding edge ES6 support, an object with no prototype):
// Nicer, ES6-or-pollyfill option
var postCategories = new Set();
postCategories.add(categoryId);
// Or, in the no-prototype option:
var postCategories = Object.create(null);
postCategories[categoryId] = true;
// Then serialization needs an extra step if you need an array:
parentObject.postCategories = Object.keys(parentObject.postCategories);

Related

Why is array.map nested within array.forEach so much faster (in this case) than the other way around?

I have a page that gives the user the ability to filter store locations by clicking on links for Region and Individual Shops nested within those Regions. Every click updates an object containing which filters have been applied, hides all the elements and then shows only the filtered set of elements.
My code in this post takes the data from the filters object and creates a CSS selector string to which the jQuery .show() method can applied.
I tried doing it 2 different ways thinking there wouldn't be much of a difference in performance, but to my surprise, jsbench.me showed Solution #2 (below) to be ~90% slower, and I'm baffled as to why.
Here is the code that creates the CSS selector string for each solution. They both use the same object representing the current active filters. They can be found in this Codepen as well (variables are named slightly differently, but the code is the same as below).
Example Active Filters Object
const filts = {
levels: [],
locations: {
"new-york": ["monore", "chester", "cortalnd"],
colorado: ["denver"],
penn: [],
}
};
Solution 1:
const locs2 = filts.locations;
const activeRegions = Object.keys(locs2);
if(activeRegions.length) {
let sels2 = [];
activeRegions.forEach(region => {
const regionSelStr = `#region-holder__${region}`;
sels2.push(regionSelStr);
const shops2 = locs2[region];
if(shops2.length) {
const shopsSels = shops2.map(shop => `#shop-holder_${region}_${shop}`);
sels2.push(...shopsSels);
} else {
sels2.push(`${regionSelStr} .shop-holder`);
}
});
console.log(sels2.join(','));
}
Solution 2:
const locs = filts.locations;
const regions = Object.keys(locs);
const sels = regions.map(region => {
const regionPart = `#region-holder__${region}`;
let str = `${regionPart},`;
const shopPart = locs[region];
if(!shopPart.length) {
str += `${regionPart} .shop-holder`;
} else {
shopPart.forEach((shop, i, arr) => { arr[i] = `#shop-holder_${region}_${shop}` });
str += shopPart.join(',');
}
return str;
});
console.log(sels.join(','));
They each use a .forEach() and a .map() for iterating, but is it the order in which they're used/nested? Or is it because Solution 2 changes the value of the filts object when does its Object.keys(filts.locations).map(...)? Any ideas?
EDIT
I changed Solution 2 to use string interpolation (oversight on my part at first) - still ~88% slower according to jsbench.me
EDIT 2
I changed Solution 2 to NOT call array.join on each iteration, rather just start building up the string right in the forEach() and now it is the faster one by ~10%
const locs = filts.locations;
const regions = Object.keys(locs);
const sels = regions.map(region => {
const regionPart = `#region-holder__${region}`;
let str = `${regionPart},`;
const shopPart = locs[region];
if(!shopPart.length) {
str += `${regionPart} .shop-holder`;
} else {
shopPart.forEach((shop, i, arr) => {
str += `#shop-holder_${region}_${shop},`;
});
}
return str;
});
console.log(sels.join(','));
The difference between forEach and map won't really be significant, forEach will likely have a slight edge since it doesn't have to create a new array but that's just a guess. The reason why your code is slower in solution 2 with forEach is because 1. you're using a join which will execute an array join on every iteration (essentially creating a factorial loop) and also 2. you're using string concatenation which will be slower than interpolation, as the comment on your question have already mentioned. Solution 1 is just linear with a bit extra for adding new items to an array, which is negligible.

Javascript TypeError: Cannot read property 'indexOf' of undefined

In this code I want to remove an element from the cart_products array.
var cart_products = ["17^1", "19^1", "18^1"];
var product = 17;
$.each(cart_products,function(key, item) {
if(item.indexOf(product+"^") !== -1){
cart_products.splice(key, 1);
}
});
But I get this error in Google Chrome console:
Uncaught TypeError: Cannot read property 'indexOf' of undefined
Is there something wrong with the code?
Thanks for your help.
The problem is that you're modifying the array while jQuery's $.each is looping over it, so by the time it gets to the end, the entry that used to be at index 2 is no longer there. (I admit I'm a bit surprised $.each behaves that way, but I haven't used $.each in at least five years, so...)
If the goal is to remove matches from the array, the better choice is filter:
var cart_products = ["17^1", "19^1", "18^1"];
var product = 17;
cart_products = cart_products.filter(function(item) {
return item.indexOf(product+"^") === -1;
});
console.log(cart_products);
...or alternately if it's important to modify the array in-place rather than creating a new one use a boring for loop as Andreas points out looping backward through the array so it doesn't matter when you remove things:
var cart_products = ["17^1", "19^1", "18^1"];
var product = 17;
var target = product + "^";
for (var index = cart_products.length - 1; index >= 0; --index) {
if (cart_products[index].indexOf(target) !== -1) {
cart_products.splice(index, 1);
}
}
console.log(cart_products);
First of all, you don't need to use a jQuery each for this. Second, it's not a great idea to alter an array that you are operating on. If you're trying to remove elements from an array, use filter. Filter has the following signature:
someArray.filter(function(item, index, array) {
// return a value that is truthy to keep an item or falsey to remove it
})
Filter returns a new array with only the values that match what you want. That means you don't mess with your original array, which is a good idea anyways. In your case it would look like this:
var filteredProducst = cart_products.filter(function(item) {
return item.indexOf(product + "^")
})

How do I add elements to a dynamic array and exclude exsisting elements

function addNumifnotThere(numer){
var numCent = [];
numCent.forEach(function(){
if(numer in numCent)
console.log("you logged that");
else
numCent.push(numer);
});
return numCent;
}
This is my current code, what its attempting to do is read an array and if there is already an element exits the loop and says "you already logged that", obviously if it cannot find a similar element then it pushes it to the array.
I want this to work dynamically so we cannot know the size of the array beforehand, so the first element passed as an argument should be put into the array, (addNum(1) should have the array print out [1], calling addNum(1) again should print "you already logged that")
However there are two problems with this
1) Trying to push to a new array without any entries means everything is undefined and therefore trying to traverse the array just causes the program to print [].
2) Adding some random elements to the array just to make it work, in this case numCent=[1,2,3] has other issues, mainly that adding a number above 3 causes the code to print incorrect information. In this case addNum(5) should print [1,2,3,5] but instead prints [1,2,3,5,5,5]
I know this has to be a simple mistake but I've been dragging myself too long to not ask for help.
EDIT: Thanks to the many outstanding answers here I have now leanred about the indexOf method, thank you guys so much.
For every non-match you are pushing the number. Use something like this
var numCent = [];
function addNumifnotThere(numer)
{
var index = numCent.indexOf(number);
if(index >=0)
{
console.log("you logged that");
}
else
{
numCent.push(number);
}
return numCent;
}
Use Array.prototype.indexOf
var numCent = [];
function addNum(numer){
if (numCent.indexOf(numer) > -1)
{
console.log("Number already in array");
}
else
{
numCent.push(numer);
}
}
//DEMO CODE, not part of solution
document.querySelector("button").addEventListener("click", function(){
if (document.querySelector("input").value.length > 0)
{
addNum(document.querySelector("input").value);
document.querySelector("div").innerHTML = numCent.join(", ");
}
}, false);
Output
<div id="output"></div>
<input />
<button>Add number</button>
indexOf tests if an element is inside the array and returns its index. If not found it will return -1. You can test for this. You can try it for your self in this snippet. It will only allow you to add a number (or any string, in this example) once.
I also was confused by the newCent array declaration inside the function. I think, based upon the content of your question, you meant this.
If you want the array held in the instance, you can do it like this.
function AddIf(arr){
if( arr || !this.arr ) {
this.arr = arr || [];
}
return function(number) {
if( this.arr.indexOf(number) >= 0 ) {
console.log("Already Present!");
} else {
this.arr.push(number);
}
return this.arr;
}.bind(this);
}
// Usage would be like this:
// var addIf = new AddIf([1, 2, 3]);
// addIf(10); // returns [1, 2, 3, 10]
// addIf(10); // logs "Already Present!", doesn't add 10 to array
This basically returns a function, with this bound to the function being called. If you pass in an initial array, it will use that array to compare to when adding it to the array.
You can catch the return function and call it as you would want to. If you don't call new when invoking however, it will share the same array instance (and have a funky way of being called, AddIf()(10)).
I used fn.bind() to ensure the function gets called in the correct context every time, if you were wondering why I called it like that.
Do do this cleanly, I'd consider prototyping the global Array object and adding a method to push values but only if they're unique to the array. Something like this:
Array.prototype.pushUnique = function (item) {
if (this.indexOf(item) != -1) {
console.log("Item with value of " + item + " already exists in the array."
}
else {
this.push(item);
}
}
If you're not comfortable prototypeing global types like Array, you can build the same thing in a procedural pattern:
function arrayPushUnique (arr, item) {
if (arr.indexOf(item) != -1) {
console.log("Item with value of " + item + " already exists in the array."
}
else {
arr.push(item);
}
}
Then to use it, simply create a new empty array and start pushing things to it.
var numCent = [];
// The Array.prototype method
numCent.pushUnique(number);
// The procedural method
arrayPushUnique(numCent, number);

Is there a non loop-every-single-list-item approach to find unique list items?

I know I could use the loop-every-single-list-item approach to filter out unique elements in a given list, but I feel like there's probably a neat, quick way to do it.
How can I find unique list items in JavaScript, without looping through and filtering them manually?
Lately I was working on event handling patch and needed fast method for filtering out unique function handlers in a callback lists which got to be run quite frequently.
Here's what I'm trying to do:
Array.prototype.unique = (function () {
// main Array#unique method
var uni = function uni () {
return this.filter(uni.x);
};
// attach a helper for resolving unique elements
// if element is at current position, not before,
// it's unique one, pass `true` flag to .filter()
uni.x = function (node, pos, ls) {
return pos === ls.indexOf(node);
};
// save
return uniq;
})();
Implementation:
// sample list:
// generate ~1K long list of integers:
// get the keys of string object of length 32,
// map every item to key-list itself,
// flatten, shuffle..
var ls =
Array.prototype.concat.apply([],
Object.keys(new String('1'.repeat(32)))).
map(function (node, pos, list) { return list; }).
sort(function () { return Math.random() < Math.random(); });
// run each function 1K times fetching unique values
for (
var
it = -1,
l = 1000,
// record iteration start
tm = Date.now();
++it < l;
ls.unique()
);
No. If you have a list, you will need to look at least once at every single item to determine whether it is unique.
If you need something faster, don't use a list.
Btw, even on a list you can implement a unique-algorithm in less than the O(n²) that you currently have. See Easiest way to find duplicate values in a JavaScript array for some clever approaches.
I was working on event handling patch and needed fast method for filtering out unique function handlers in a callback list which got to be run quite frequently.
Then you don't want to put them in that list in the first place. Don't check the list for duplicates when you run it (which as you say is frequent), but when you insert a new handler.
If you think that using .indexOf to find a handler in the list is still too slow, you can mark every function object that it is already contained in the list. Choose a unique (per list) property name, and put a value on that property of each function that is in the list. You can then check in constant runtime for duplicates.
If you have a unique key, using a dictionary is a good option. However, if you have some logic that needs to be executed to perform your filtering, I'd go with UnderscoreJS. Check out the _.filter method. It's a great library with lots to offer in this area.
http://underscorejs.org/#filter
I don't think there is a way to get unique list of items without iterating through each item. If you're looking for a built-in library function, I don't think there is one in Angular.
It would be simple enough to create one:
function unique(array, fn) {
var hash = [];
var list = [];
for(var i = 0; i < array.length; ++i) {
var key = fn(array[i]);
if (key && !hash[key]) {
list.push(array[i]);
hash[key] = key;
}
}
return list;
}
Usage:
var myList = [ { id:1, name="oranges"},
{ id:2, name="apples" },
{ id:1, name="oranges"},
{ id:3, name="pears" },
{ id:3, name="pears" } ];
var uniqueList = unique(myList, function(item) { return item.id; });

change array passed to function

I pass 2 arrays to a function and want to move a specific entry from one array to another. The moveDatum function itself uses underscorejs' methods reject and filter. My Problem is, the original arrays are not changed, as if I was passing the arrays as value and not as reference. The specific entry is correctly moved, but as I said, the effect is only local. What do I have to change, to have the original arrays change as well?
Call the function:
this.moveDatum(sourceArr, targetArr, id)
Function itself:
function moveDatum(srcDS, trgDS, id) {
var ds = _(srcDS).filter(function(el) {
return el.uid === uid;
});
srcDS = _(srcDS).reject(function(el) {
return el.uid === uid;
});
trgDS.push(ds[0]);
return this;
}
Thanks for the help
As mentioned in the comments, you're assigning srcDS to reference a new array returned by .reject(), and thus losing the reference to the array originally passed in from outside the function.
You need to perform your array operations directly on the original array, perhaps something like this:
function moveDatum(srcDS, trgDS, id) {
var ds;
for (var i = srcDS.length - 1; i >= 0; i--) {
if (srcDS[i].uid === id) {
ds = srcDS[i];
srcDS.splice(i,1);
}
}
trgDS.push(ds);
return this;
}
I've set up the loop to go backwards so that you don't have to worry about the loop index i getting out of sync when .splice() removes items from the array. The backwards loop also means ds ends up referencing the first element in srcDS that matches, which is what I assume you intend since your original code had trgDS.push(ds[0]).
If you happen to know that the array will only ever contain exactly one match then of course it doesn't matter if you go forwards or backwards, and you can add a break inside the if since there's no point continuing the loop once you have a match.
(Also I think you had a typo, you were testing === uid instead of === id.)
Copy over every match before deleting it using methods which modify Arrays, e.g. splice.
function moveDatum(srcDS, trgDS, id) { // you pass an `id`, not `uid`?
var i;
for (i = 0; i < srcDS.length; ++i) {
if (srcDS[i].uid === uid) {
trgDS.push(srcDS[i]);
srcDS.splice(i, 1);
// optionally break here for just the first
i--; // remember; decrement `i` because we need to re-check the same
// index now that the length has changed
}
}
return this;
}

Categories