New array based on one property in existing object array - javascript

I'm trying to figure out the cleanest way of using the string-similarity library in NodeJS with the 2 arrays used in my project.
The first is an array of objects that look something like this:
{
eventName: "Some event name",
tournamentName: "US Open",
city: "New York"
}
The second array contains objects that looks slightly different, for example:
{
eventName: "Some event name",
temperature: "28",
spectators: "15000"
}
What I'm trying to do is build something that iterates through the first array and finds the closest matching event name in the second array, based of course ONLY on the eventName property using the "string-similarity" NodeJS library.
The below method works really well:
stringSimilarity.findBestMatch(eventName, arrayOfEventNames)
But of course the 2nd parameter requires an array consisting only of event names. I don't have that. I have an array consisting of objects. It's true that one of the properties of these objects is the event name, so what I'm trying to figure out is the best way to pass that in to this function. I built the below function (calling it inside forEach on first array) which basically takes in the name of the event I want to search for and the second array of objects and then creates a new temporary array inside it of ONLY the event names. Then I have the 2 inputs I need to call the stringSimilarity.findBestMatch method.
function findIndexOfMatchingEvent(eventName, arrayToCompareAgainst) {
let onlyEventNames = [];
arrayToCompareAgainst.forEach(e => {
onlyEventNames.push(e.eventName);
});
if (arrayToCompareAgainst.length !== onlyEventNames.length) {
throw new Error("List of events array length doesn't match event names array length!");
}
const bestMatch = stringSimilarity.findBestMatch(eventName, onlyEventNames);
const bestMatchEventName = bestMatch.bestMatch.target;
const bestMatchAccuracyRating = bestMatch.bestMatch.rating;
const index = arrayToCompareAgainst.findIndex(e => {
return e.eventName === bestMatchEventName;
});
if (index === -1) {
throw new Error("Could not find matched event in original event list array");
} else if (bestMatchAccuracyRating >= 0.40) {
return index;
}
}
This works but it feels very wrong to me. I'm creating this new temporary array so many times. If my first array has 200 objects, then for each of those I'm calling my custom function which is then creating this temporary array (onlyEventNames) 200 times as well. And even worse, it's not really connected to the original array in any way, which is why I'm then using .findIndex to go back and find which object inside the array the found event refers to.
Would really appreciate some feedback/advice on this one. Thanks in advance!

In my earlier answer I misunderstood the question.
There's no need to recreate the array of event names for each entry in the other array you want to compare. Create the array of event names once, then reuse that array when looping through the other array's entries. You can create the array of event names the way you did in findIndexOfMatchingEvent, but the more idiomatic way is with map.
Assuming these arrays:
const firstArray = [
{
eventName: "Some event name",
tournamentName: "US Open",
city: "New York"
},
// ...
];
const secondArray = [
{
eventName: "Some event name",
temperature: "28",
spectators: "15000"
},
// ...
];
Then you can do this:
const onlyEventNames = secondArray.map(e => e.eventName);
let bestResult;
let bestRating = 0;
for (const {eventName} of firstArray) {
const result = stringSimilarity.findBestMatch(eventName, onlyEventNames)
if (!bestResult || bestRating < result.rating) {
// Better match
bestResult = secondArray[result.bestMatchIndex];
bestRating = result.rating;
}
}
if (bestRating >= 0.4) {
// Use `bestResult`
}
When done with the loop, bestResult will be the object from the second array that is the best match for the events in the first array, and bestRating will be the rating of that object. (That assumes there are entries in the arrays. If there are no entries in firstArray, bestResult will be undefined and bestRating will be 0; if there aren't any in the second array, I don't know what findBestMatch returns [or if it throws].)
About your specific concerns:
I'm creating this new temporary array so many times.
Yes, that's definitely not ideal (though with 200 elements, it's really not a big problem). That's why in the above I create it only once and reuse it.
...it's not really connected to the original array in any way...
It is: by index. You know for sure that if the match was found at index 2 of onlyEventNames, that match is for index 2 of secondArray. In the code above I grab the entry using the index returned by findBestMatch.

Related

Determine if sets of key and value pairs exists in another array of objects

I've got 2 sets of javascript objects, one containing a new item and another containing an array of items.
I'd like to be able to check the existing array of items to find out whether the new item falls into any of the existing items, based on two pairs of keys and values (these being dateFrom and dateTo, and ideally even if the new item falls in-between any of the dates in the existing items).
const newItem = [{
id: null,
dateFrom: '2019-02-01',
dateTo: '2019-02-28'
}]
const existingItems = [{
id: 1,
dateFrom: '2019-01-01',
dateTo: '2019-01-31'
},{
id: 2,
dateFrom: '2019-02-01',
dateTo: '2019-02-28'
},{
id: 3,
dateFrom: '2019-03-01',
dateTo: '2019-03-31'
}]
What I'd like is to be able to pass both items to a function that will return a boolean answer that states whether the new item doesn't exist in the existing collection of items.
Thanks in advance!
I'm guessing something along the lines of:
checkDateRanges(newItem, existingItems) {
forEach.existingItems(item => {
item.dateFrom = newItem.dateFrom ||
item.dateTo = newItem.dateTo
})
}
You're very close with what you've got there. First, the array function find might be a good choice. It will only find the first match, which is often more efficient than looping through the whole array. It would look something like this:
function checkDateRanges(newItem, existingItems) {
const match = existingItems.find(item=>
item.dateFrom == newItem.dateFrom &&
item.dateTo == newItem.dateTo
);
return match!==undefined;
}
As you say this will return a boolean. But of course you could also return the actual object match, or if you wanted the array index of the match, there is also findIndex.
The other thing to note is that your newItem above is an array containing a single object. I'm guessing you might want to remove the array brackets from around that object. But with newItem and existingItems exactly as you have them above, you would need to invoke the function like this (to get the first element of that array):
checkDateRanges(newItem[0], existingItems);
The best way I can think of solving this is by using Javascripts Array.some method, which returns a boolean if the callback returns true for any of the value within the array. As a bonus, it stops as soon as it sees a true value.
So you are comparing 2 arrays, my solution would look like this, which could be interpreted to the newItem is contained within the existingItems array based on the logic you provided.
function checkDateRanges(newItem, existingItems) {
return existingItems.some((item) => {
return item.dateFrom === newItem.dateFrom ||
item.dateTo === newItem.dateTo;
});
}

Is it bad to have "number strings" as object keys?

I have two APIs to work with and they can't be changed. One of them returns type like this:
{
type: 25
}
and to other API I should send type like this:
{
type: 'Computers'
}
where 25 == 'Computers'. What I want to have is a map of numeric indices to the string value like this:
{
'1': 'Food',
'2': 'Something',
....
'25': 'Computers'
....
}
I am not sure why, but it doesn't feel right to have such map with numeric value to string, but maybe it is completely fine? I tried to Google the answer, but couldn't find anything specific. In one place it says that it is fine, in another some people say that it's better not to have numeric values as object keys. So, who is right and why? Could somebody help me with this question?
Thanks :)
There's nothing wrong with it, but I can understand how it might look a little hinky. One alternative is to have an array of objects each with their own id that you can then filter/find on:
const arr = [ { id: 1, label: 'Food' }, { id: 2, label: 'Something' }, { id: 25, label: 'Computers' } ];
const id = 25;
function getLabel(arr, id) {
return arr.find(obj => obj.id === id).label;
}
console.log(getLabel(arr, id));
You can use the Map object for this if using regular object feels "weird".
const map = new Map()
map.set(25, 'Computers');
map.set(1, 'Food');
// then later
const computers = map.get(25);
// or loop over the map with
map.forEach((id, category) => {
console.log(id, category);
});
Quick Update:
As mentioned by others, using objects with key=value pairs is OK.
In the end, everything in javascript is an object(including arrays)
Using key-value pairs or Map has 1 big advantage( in some cases it makes a huge difference ), and that is having an "indexed" data structure. You don't have to search the entire array to find what you are looking for.
const a = data[id];
is nearly instant, whereas if you search for an id in an array of objects...it all depends on your search algorithm and the size of the array.
Using an "indexed" object over an array gives much better performance if dealing with large arrays that are constantly being updated/searched by some render-loop function.
Map has the advantage of maintaining the insertion order of key-value pairs and it also only iterates over the properties that you have set. When looping over object properties, you have to check that the property belongs to that object and is not "inherited" through prototype chain( hasOwnProperty)
m = new Map()
m.set(5, 'five');
m.set(1, 'one');
m.set(2, 'two');
// some other function altered the same object
m.__proto__.test = "test";
m.forEach((id, category) => {
console.log(id, category);
});
/*
outputs:
five 5
one 1
two 2
*/
o = {};
o[5] = 'five';
o[1] = 'one';
o[2] = 'two';
// something else in the code used the same object and added a new property
// which you are not aware of.
o.__proto__.someUnexpectedFunction = () => {}
for (key in o) {
console.log(key, o[key]);
}
/*
Output:
1 one
2 two
5 five
someUnexpectedFunction () => {}
*/
Map and objects also have 1 very important advantage(sometimes disadvantage - depending on your needs ). Maps/objects/Sets guarantee that your indexed values are unique. This will automatically remove any duplicates from your result set.
With arrays you would need to check every time if an element is already in the array or not.

can I use forEach to make every element of an array a new object?

I created an array with many elements with a loop:
myArray = [c1, c2, c3...]
now I want to make each element into an object and assign different key values:
c1 = {image = path, value = number)
I tried to run forEach() but can't figure out the correct way to do so and I have not succeeded in finding the answer to it.
My guess was:
myArray.forEach(function() {
let name = {
image = path,
value = number,
}
return name;
});
but there's no change in the elements in the log.
Any help or link to an answer that can help me here. First time coding here.
UPDATE: an easier solution was to .push all the keys and values of the objects when I created the array with the loop in the first place.
array.push({image: pathx, value: numberx})
You can, but you'd be better off with a simple for loop:
for (let i = 0; i < myArray.length; ++i) {
let entry = myArray[i];
myArray[i] = {image: entry.path, value: entry.number};
}
Or making a new array with map.
newArray = myArray.map(entry => ({image: entry.path, value: entry.number}));
Or if you prefer non-arrow functions:
newArray = myArray.map(function(entry) {
return {image: entry.path, value: entry.number};
});
You could theoretically push to a new array but this is the exact usecase for Array#map. Array#map maps old values to new values. The returned object from the callback is the new object and the returned array is the new array containing the new objects.
Semantically, Array#forEach is to plainly iterate over each element in an array, and possibly execute something with side-effects (which may include pushing to another array). But with Array#map, it's specifically used to transform old array values to new ones. Use the one that is specifically designed because it conveys a clear message to anyone else who reads your code.
const newArray = myArray.map(({ path, number }) => ({
image: path,
value: number
}));
Array#map maps old values to new values. You may need to use the follwing instead of arrow functions as it is not supported in IE.
I just added dummy data in the object, you can change it.
myArray = ["c1", "c2", "c3"]
myArray = myArray.map(function(elem) {
return {"image":"path"+elem,"value":"value"+elem};
});
console.log(myArray);

How to mutate original array in Javascript .map() function?

For eg:
var persons = [{ "name":"A", "salary":1200 }, { "name":"B", "salary":"1500" }];
And you want to change the value of "salary" of each person in an original array.
If you want to mutate the original array, you can use Array#forEach function.
const persons = [{ "name":"A", "salary":1200 }, { "name":"B", "salary": 1500 }];
persons.forEach(item => item.salary += 1000);
console.log(persons)
Array#map creates a new array of the created items and returns that. After you need to assign the returned result.
let persons = [{ "name":"A", "salary":1200 }, { "name":"B", "salary": 1500 }];
persons = persons.map(item => {
item.salary += 1000;
return item;
});
console.log(persons);
You can mutate the objects directly iterating with map. If I understood you correctly.
persons.map(i => { i.salary = i.salary * 1.25; return i; });
console.log(persons);
// [{ "name":"A", "salary": 1875 }, { "name":"B", "salary": 2343.75 }]
Though it's anti-pattern and you should avoid performing mutations of your original array in map().
Use forEach. If your array is full of objects, and you simply want to mutate those objects — like in the original question — then you can simply have your callback function mutate the item passed to it in its first argument.
If your array is full of primitives, which can only be changed by assigning something else to the array slots, thereby mutating the array — or if for some other reason you want to reassign things to the array slots — you can still use forEach. The second parameter and third parameters passed to the callback are the array index of the element currently being operated on, and a reference to the array itself. So within the callback function, you can assign to the relevant array slot.
you can use a simple for loop for that
var persons = [{ "name":"A", "salary":1200 }, { "name":"B", "salary":"1500" }];
for(let element of persons){
element.salary*=2;
}
console.log(persons);
.map() function takes third parameter in its callback thats the instance of original array.
You could do something like this also:
var persons = [{ "name":"A", "salary":1200 }, { "name":"B", "salary":1500 }];
persons.map(function(person, key, array) {
array[key].salary *= 2;
});
console.log(persons);
I read other answers, you can use any of them, but I see some problems there.
I will mention 2 methodologies I have used in many different languages, map and forEach. map is a functional way of traversing a collection and creating some new collection with new (different or same) elements, independent of languages. With map, it is expected to create a new collection that is created by some mapping from initial collection. On the other hand, forEach is a method that eases traversing a collection by not using usual for loop syntax for collections, and mutating (or changing) each item if desired.
If you use map on a collection that contains objects, and change those objects in the mapper function, you might face with unexpected behavior. Beacuse you are changing directly the object you are operating on, and do not mapping it to another object. This object might can be considered as a state and computers works based on the state transfers. If you want to change that object, i.e. some state, it is absolutely ok, but based on the description, you should not use map for such a case. Because you are not creating a new array with some new values, but instead, mutating provided elements. Use forEach for such a case.
I have added an example here. You can click the link and take a look at the console, and see my what I mean in a more clear way.
As far as I know, based on my experience, mutations in map method is considered as bad practice and discouraged.
These two are added for different purposes and it would be better to use them as expected.
For more, see Mozilla Web Docs page for Array.
JavaScript has an inbuilt Array method map that iterate the values of an Array
persons.map(person => person["salary"] + 1000)
var persons = [{ "name":"A", "salary":1200 }, { "name":"B", "salary":"1500" }];
var mutatedPersons = persons.map(function(obj){
return {name:obj.name,salary:parseInt(obj.salary) + 100};
})
console.log(mutatedPersons);
try:
let persons = persons.map((person) => {person['salary'] = parseInt(person['salary']) + 1; return person})
If you have an array of primitives, you can use this function:
function mapInplace<T>(arr: T[], callback: (v: T, i: number) => T) {
for(const [i, v] of arr.entries()) {
arr[i] = callback(v, i)
}
}
Example usage:
mapInplace(weights, w => w / total)
There's no return value since it's mutating the array.

Merge array into a single array

If this were .NET, I'd ask how to convert List<List<MyClass> to List<MyClass>. However, I'm not very good with javascript and don't know how to ask that as a question using Javascript terminology!
My javascript object comes through like
And is created as:
js_datasets.push({
"DataItem0": {
lzabel: "This",
data: [[1408710276000, null],[1408710276000, 15]]
},
"DataItem1": {
lzabel: "That",
data: [[1408710276000, null],[1408710276000, 15]]
},
});
js_datasets.push({
"DataItem22": {
lzabel: "And other",
data: [[1408710276000, null],[1408710276000, 5]]
},
"DataItem23": {
lzabel: "And lastly",
data: [[1408710276000, null],[1408710276000, 1]]
},
});
Each object is the same "type" (if it matters).
I'd like to create a single list but I am failing to do so. My efforts are
var myDataSet = []; //this is the results of what I want, ideally as a single list
for (var i = 0; i < js_datasets.length; i++) {
if (i==0) {
myDataSet.push(js_datasets[i]);
}
else {
myDataSet.concat(js_datasets[i]);//does nothing
myDataSet.join(js_datasets[i]);//does nothing
}
...more logic
As you can see with the above, I've tried using push, concat and join.
If I update the code to only use push (and never use concat and join) then I get all the values I want, but again, as an array within an array.
Using concat and join do not add to the list.
So, if we can assume the 12 items in the array (pictured) all contain 10 items, I'd like to have a single list of the 120 items!
How can I simply convert this multidimension array (is it multidimension) to a single dimension array.
This will be a bit complicated, as the items in your Array js_datasets are not Arrays, but a more generic Object. This means you can't assume the keys will be in order if you try to read them
Lets write some helper functions to account for this;
function dataItemCollectionToArray(o) {
var keys = Object.keys(o);
// assuming no non-DataItem keys, so next line commented out
// keys = keys.filter(function (e) {return e.indexOf("DataItem") === 0;});
keys.sort(function (a, b) { // ensure you will get the desired order
return +a.slice(8) - +b.slice(8);
});
return keys.map(function (e) {return o[e];});
}
Now you can loop over js_datasets performing this conversion
var myDataSet = [], i;
for (i = 0; i < js_datasets.length; ++i) {
// assuming no gaps, if you need to add gaps, also find min, max indices
// in `dataItemCollectionToArray`, and check them in each iteration here
myDataSet.push.apply(myDataSet, dataItemCollectionToArray(js_datasets[i]));
}
Please note that Object.keys and Array.prototype.map may require polifills if you wish to support old browsers, i.e. IE<=8
An easier solution however, may be to re-write how js_datasets is constructed so that the Objects you are pushing are more Array-like or indeed pushing true Arrays, perhaps with a couple extra properties so you know the offset for the first index. This would mean you can use flatten methods that you'll find around the internet

Categories