I know indexOf cannot be used in an array of objects, e.g. below.
const objs = [
{name: 'darling', age: 28},
{name: 'eliot', age: 29}
]
console.log(objs.indexOf({ name: 'eliot', age: 29 })) // print -1
However, why DOM element array index can be traced by indexOf??
e.g. below
document.getElementById('slides').addEventListener('click', function (e) {
const nodes = document.querySelectorAll('#slides > .slide')
const newNodes = [...nodes]
console.log(newNodes);
console.log(newNodes.indexOf(e.target)) // can print out the index
})
Every time the interpreter comes across an object literal when it runs a line - for example:
const objs = [
{name: 'darling', age: 28},
{name: 'eliot', age: 29} // this line
]
or
console.log(objs.indexOf(
{ name: 'eliot', age: 29 } // this line
))
It creates a new object with those properties. They aren't the same.
console.log(
// two separate objects, which happen to contain the same value
{ foo: 'foo' } === { foo: 'foo' }
);
In contrast, when looking up nodes in the DOM, the nodes are generally static - they don't change unless JavaScript runs which explicitly does so. The e.target refers to one such static node, and querySelectorAll returns a collection of such static nodes.
So they're ===, and .indexOf works.
Consider the following behavior:
const element = { foo: 1 };
const arr = [{ foo: 1 }, { foo: 2 }, element];
console.log(arr.indexOf({ foo: 1 })); // -1
console.log(arr.indexOf(element)); // 2
console.log(arr.indexOf(arr[0])); // 0
It clearly demonstrates how indexOf matches the reference to an object, rather than an object with the same keys and values.
For one { name: 'eliot', age: 29 } is a new object and hence not found in your array
So you can find an object if it is the actual same object
Otherwise look for it by the value of one or more of the entries
const objs = [
{name: 'darling', age: 28},
{name: 'eliot', age: 29}
]
const objToFind = objs[1]
console.log(objs.indexOf(objToFind))
// find the index by name
const idxOfEliot = objs.findIndex(({name}) => name==="eliot")
console.log(idxOfEliot)
Related
I need to check that an Array contains an Object that matches a given structure.
I already tried this:
const myArray = [{ name: 'Mete', age: 19, phone: '123456' }];
expect(myArray).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'Mete',
age: 19
})
])
)
// Throws no error
It should not match because the object in the array has an additional property "phone".
I need something like toStrictEqual() combined with arrayContaining().
You can use Array.prototype.some() with .toEqual(value) to test each object in the array, which will stop iteration and return true on the first assertion that passes the expectation. Otherwise, false will be returned and the final expect statement will fail.
The technique here is to catch the exceptions thrown from the assertions and convert them to false results:
const expected = {name: 'Mete', age: 19};
const myArray = [{ name: 'Mete', age: 19, phone: '123456' }];
const found = myArray.some(obj => {
try {
expect(obj).toEqual(expected);
return true;
}
catch {
return false;
}
});
expect(found).toBeTruthy();
With every array function, you can get the result:
const strictlyEquals = (arr, props) => arr.every(o=>Object.keys(o).every(i=>props.includes(i)));
console.log(strictlyEquals([{name:'Test', age: 20}], ['name', 'age']));
console.log(strictlyEquals([{name:'Test', age: 20, phone:'Test'}], ['name', 'age']));
If you have to match complete object then:
const strictlyEquals = (arr, props) => arr.every(o=> Object.keys(o).length===Object.keys(props).length && Object.keys(o).every(i=>o[i]===props[i]));
console.log(strictlyEquals([{name:'Test', age: 20},{name:'Test', age: 20}], {name:'Test', age: 20}));
let arr = [{ age: 3 }, { age: 5 }, { age: 6 }, { age: 7 }];
let exists = arr.find(x => x.age < 4);
exists.age += 1;
console.log(arr);
//output is [{age:4},{age:5},{age:6},{age:7}];
In the above example, I'm updating the result returned by the find method but it also changes the value of the original array why so?
It's because Objects in JavaScript are passed by reference, you got that object ( {age : 3} ) in exists then added 1 to it's "age" key , so the original object also changed .
let obj1 = {age: 3 , name: 'jack' }
let obj2 = obj1
console.log(obj1 === obj2 ) // true
// if you change the second object , the first one will change too :
obj2.age = 15
console.log(obj1 , obj2 )
// obj1 = { age: 15 , name: 'jack' }
// obj2 = { age: 15 , name: 'jack' }
Array.prototype.find will return the first element that satisfy the condition that you've passed as a callback function.
Since you are looking for an object whose age property is less than 4. So It will return first object(whose age is 3). So if you gonna check for equality of exists and arr[0] then It will return object that satisfy the condition
let arr = [{ age: 3 }, { age: 5 }, { age: 6 }, { age: 7 }];
let exists = arr.find((x) => x.age < 4);
console.log(exists === arr[0])
So, If you are going to do any kind of mutation with the object that is returned by the find method then the original object will reflect the changes.
Because both are same object just different references.
If you don't want to mutate the original object then you should clone it before doing any kind of changes to that object.
Note: Both of the following method does shallow copy
1) Using spread syntax
let arr = [{ age: 3 }, { age: 5 }, { age: 6 }, { age: 7 }];
let exists = arr.find((x) => x.age < 4);
const clone = { ...exists };
clone.age += 1;
console.log(arr);
/* This is not a part of answer. It is just to give the output full height. So IGNORE IT */
.as-console-wrapper { max-height: 100% !important; top: 0; }
2) Using Object.assign
let arr = [{ age: 3 }, { age: 5 }, { age: 6 }, { age: 7 }];
let exists = arr.find((x) => x.age < 4);
const clone = Object.assign({}, exists);
clone.age += 1;
console.log(arr);
Because you have an array of object references. In JavaScript, objects are actually stored elsewhere (called the "heap") and object variables actually only contain the references to object. So the reason is because you're updating the same object.
If you want to do a shallow clone of an object, you can use Object.assign({}, obj).
Further, not directly relevant to your question, if your object properties themselves were to contain other object references, including arrays, and you want those to be copies as well, you'll have to deep-clone them. This is not automatically done by a stock JavaScript function or method. You'll have to find code that does that for you or write it yourself. Last time I did this, I used randa's clone function because a different developer on my team had already imported the ramda library into our project. What makes the most sense for your project may be different.
I found unexpected result when try to merge with lodash object with flat array inside.
Here the example:
var people = { name: 'Andrew', age: '30', values: ["PT", "PORTA 31"] };
const person = { age: '31', values: ["PT"] };
var people2 = { name: 'Andrew', age: '30', values: [{ pippo : 1}] };
const person2 = { age: '31', values: [{ pippo : 2}] };
// Now merge person back into people array
console.log(_.merge({}, people, person));
console.log(_.merge({}, people2, person2));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
The result of first console.log is
{
age: "31",
name: "Andrew",
values: ["PT", "PORTA 31"]
}
And not as expected
{
age: "31",
name: "Andrew",
values: ["PT"]
}
Someone can explain me why and give me a solution to make sure that with a flat array it takes me the correct value
I think assign is better in this case than merge
This method is like _.assign except that it recursively merges own and inherited enumerable string keyed properties of source objects into the destination object. Source properties that resolve to undefined are skipped if a destination value exists. Array and plain object properties are merged recursively. Other objects and value types are overridden by assignment. Source objects are applied from left to right. Subsequent sources overwrite property assignments of previous sources.
var people = { name: 'Andrew', age: '30', values: ["PT", "PORTA 31"] };
const person = { age: '31', values: ["PT"] };
console.log(_.assign({}, people, person));
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.20/lodash.min.js"></script>
I believe _.assign(people, person) would produce the desired outcome in this case https://lodash.com/docs/4.17.15#assign
This functionality is also native and can be used like this Object.assign(target, source)
Here's a list of parents and I want to sort the parents by their 2nd's child's age with ramda:
[
{
name: "Alicia",
age: "43",
children: [{
name: "Billy",
age: "3"
},
{
name: "Mary",
age: "8"
},
]
},
{
name: "Felicia",
age: "60",
children: [{
name: "Adrian",
age: "4"
},
{
name: "Joseph",
age: "5"
},
]
}
]
How do I do on about it? I tried doing something along the lines of
parents.sort(
sortBy("-children.age"))
);
Use R.sortBy and extract the value with a function create with R.pipe. The function gets the children array of the object with R.prop, takes the last child (R.last), gets the age with R.propOr (returns 0 if no children), and converts to a Number. You can use R.negate if you want to reverse the order.
const { sortBy, pipe, prop, last, propOr } = R
const fn = sortBy(pipe(
prop('children'),
last,
propOr(0, 'age'),
Number,
// negate - if you want to reverse the order
))
const parents = [{"name":"Alicia","age":"43","children":[{"name":"Billy","age":"3"},{"name":"Mary","age":"8"}]},{"name":"Felicia","age":"60","children":[{"name":"Adrian","age":"4"},{"name":"Joseph","age":"5"}]}]
const result = fn(parents)
console.log(result)
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
In vanilla JavaScript (making some assumptions about the relatively poorly formatted input) using the Array.prototype.sort method:
let parents = [ .... ]; // What you have above
parents = parents.sort((a, b) => {
return a.children[1].age - b.children[1].age; // Change - to + for ascending / descending
});
Be careful though - what would happen if a parent had fewer than 2 children?
Assuming your JSON above was hand generated, including the syntax errors, then assuming your real data is just fine (an array of parents, with each parent having a children array of objects) then a normal JS sort will work just fine:
const compareC2(parent1, parent2) {
let c1 = parent1.children;
let c2 = parent2.children;
if (!c1 || !c2) {
// what happens if someone has no children?
}
let l1 = c1.length;
let l2 = c2.length;
if (l1 === 0 || l2 === 0) {
// different symptom, but same question as above
}
if (l1 !== l2) {
// what happens when the child counts differ?
}
if (l1 !== 2) {
// what happens when there are fewer, or more than, 2 children?
}
// after a WHOLE LOT of assumptions, sort based on
// the ages of the 2nd child for each parent.
return c1[1].age - c2[1].age;
}
let sorted = parents.sort(compareC2);
I would use sortWith with ascend functions. Using sortWith allows you to define a first sort order function, a second sort order function, etc.
const people = [
{
name: "Alicia",
age: "43",
children: [{
name: "Billy",
age: "3"
},
{
name: "Mary",
age: "8"
},
]
},
{
name: "Felicia",
age: "60",
children: [{
name: "Adrian",
age: "4"
},
{
name: "Joseph",
age: "5"
},
]
}
];
const by2ndChildAge = ascend(pathOr(0, ['children', 1, 'age']));
const by1stChildAge = ascend(pathOr(0, ['children', 0, 'age']));
console.log(sortWith([by2ndChildAge, by1stChildAge], people));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {sortWith, ascend, pathOr} = R;</script>
The simplest solution is, I think, just to combine sortBy with path:
const sortBy2ndChildAge = sortBy(path(['children', 1, 'age']))
const people = [{name: "Alicia", age: "43", children: [{name: "Billy", age: "3"}, {name: "Mary", age: "8"}]}, {name: "Felicia", age: "60", children: [{name: "Adrian", age: "4"}, {name: "Joseph", age: "5"}]}]
console.log(sortBy2ndChildAge(people))
<script src="https://bundle.run/ramda#0.26.1"></script><script>
const {sortBy, path} = ramda </script>
There are several potential flaws with this, that others have noted. Are parents always guaranteed to have at least two children? Do we really want a lexicographic sort -- i.e. '11' < '2' -- or do you want to convert these values to numbers?
It would be easy enough to fix both of these problems: sortBy(compose(Number, pathOr(0, ['children', 1, 'age']))), but that depends upon what you're trying to do. If you're just using this to learn about Ramda, then sortBy and path are both useful functions to know. sortBy is useful when you can convert the items to be sorted to some ordered type -- Strings, numbers, dates, or anything with a numeric valueOf method. You supply that conversion function and a list of values and it will sort by that. path is simply a null-safe read for a list of nested properties in an object.
I have this Array and Object representing the same data:
arrayExample = [
{name: "max", age: 21},
{name: "max.David", age: 27},
{name: "max.Sylvia"},
{name: "max.David.Jeff"},
{name: "max.Sylvia.Anna", age: 20},
{name: "max.David.Buffy"},
{name: "max.Sylvia.Craig"},
{name: "max.Sylvia.Robin"}
];
ObjectExample = {
name: "max",
age: 21,
children: [
{
name: "Sylvia",
children: [
{name: "Craig"},
{name: "Robin"},
{name: "Anna", age: 20}
]
},
{
name: "David",
age: 27,
children: [
{name: "Jeff"},
{name: "Buffy"}
]
}
]
};
my objective is to extend the Array class to have 2 functions flatten which transform the objectExample into the arrayExample and uneven which do the opposite, I'm thinking maybe lodash would help here but I still didn't find the correct way to do this here's where I'm now:
to flatten from objectExample to arrayExample first the objectExample structure must be specific meaning the parents must share a property with all their children sure the parents and children could have other property that should be ported to the proper item in the new arrayExample, also for the uneven function it should create an object that all the parents share the same property with their children and other property should be copied respectively.
To give my use case for this I'm trying to make a d3js tree layout of angular ui router in my application that will be generated from the routes JSON file since I make the routes in a JSON file.
update:
my specific problem is that I need to create a d3js tree layout for angular-ui-router configurations states object which I can extract into a json file as I said before, the structure for the ui-router is like the arrayExample, and the required structure for the d3js tree layout is like the objectExample, one way to go about this is to manually rewrite it and it wont take too much time but that solution is not what I want I need to make a build task for this for generic routes that will always have the name attribute in their config object that could be used to find children of each route or state, for more information check ui-router for routes config object and this d3 videos for d3 tre layout:
part 1.
part 2.
correction: extending the Object class with a flatten function to flatten an object into an array and the Array class with unEven function to unEven an array into an object not like I wrote before:
my objective is to extend the Array class to have 2 functions.
update 2:
To make this more clear, both flatten and unEven are like the map function except flatten is for an object not an array and it return an array, and the unEven function is for an array but return an object.
Here's a function that will produce the flattened output:
Working demo: http://jsfiddle.net/jfriend00/w134L7c6/
var ObjectExample = {
name: "max",
age: 35,
status: "single",
hometown: "Scottsdale",
children: [
{
name: "Sylvia",
children: [
{name: "Craig", age: 16},
{name: "Robin"},
{name: "Anna"}
]
},
{
name: "David",
age: 54,
children: [
{name: "Jeff"},
{name: "Buffy"}
]
}
]
};
// call this on an object with a name property
// and an optional children property (which would be an array of objects)
function flatten(obj, key, outputArray, rootName) {
var name, item;
outputArray = outputArray || [];
rootName = rootName || "";
if (rootName) {
rootName += ".";
}
if (obj.hasOwnProperty(key)) {
name = rootName + obj[key];
item = {};
item[key] = name;
for (var prop in obj) {
if (obj.hasOwnProperty(prop) && prop !== "children") {
item[prop] = obj[prop];
}
}
outputArray.push(item)
if (obj.children) {
for (var i = 0; i < obj.children.length; i++) {
flatten(obj.children[i], key, outputArray, name);
}
}
}
return outputArray;
}
var result = flatten(ObjectExample, "name");
Produces this output:
[{"name":"max","age":35,"status":"single","hometown":"Scottsdale"},
{"name":"max.Sylvia"},
{"name":"max.Sylvia.Craig","age":16},
{"name":"max.Sylvia.Robin"},
{"name":"max.Sylvia.Anna"},
{"name":"max.David","age":54},
{"name":"max.David.Jeff"},
{"name":"max.David.Buffy"}]
You could adapt this function to be a method on the Array prototype if you really want to (not something I would recommend, particularly since the input isn't even an array).
I do not know what you mean when you say "the rootName could have more then one". ObjectExample is an object and thus cannot have more than one name at the top level. If you started with an array of ObjectExample like structures, then you could just loop over the array calling flatten() on each object in the top level array and it would accumulate the results.