Simpler version to filter object? - javascript

I have some trouble trying to filter that kind of nested object, i must be wrong but there must be an easier way to do it.
theObject = {
"a": [{"val":"","date":2},{"val":20,"date":2}],
"b": [{"val":"","date":2}],
"c": [{"val":"10","date":1},{"val":20,"date":2},{"val":"30","date":3}]
}
myFilteredObject = {}
What i want to do is to remove from theObject unnecessary data depending on conditions, for example : val != "" or date < 2 plus, i only want the last object. And keep all the good data in myFilteredObject
Example of filtered tab with those two conditions :
myFilteredObject = {
"a": [{"val":20,"date":2}],
"c": [{"val":"30","date":3}]
}
My question : is there a simpler way to write it, here i'm doing two loops of "For... of object.entries"
let theObject = {
"a": [{"val":"","date":2},{"val":20,"date":2}],
"b": [{"val":"","date":2}],
"c": [{"val":"10","date":2},{"val":20,"date":2},{"val":"30","date":1}]
}
let myFilteredObject = {}
const filterFunction = function(){
// i remove all data that do not match my conditions //
for(let [key, value] of Object.entries(tab)){
if(value[value.length-1].val !== "" || value[value.length-1].date < 1){
myFilteredObject[key] = tab[key]
}
// i keep only the last object of each table //
for(let [key, value] of Object.entries(myFilteredObject)){
myFilteredObject[key].splice(0, myFilteredObject[key].length -1)
}
}
}
filterFunction()
console.log("result",myFilteredObject)

Take the initial object's entries, map them to an entry array of just the key and the final item in the array, then filter by whether that final item has a value and a good date:
const theObject = {
"a": [{"val":"","date":2},{"val":20,"date":2}],
"b": [{"val":"","date":2}],
"c": [{"val":"10","date":1},{"val":20,"date":2},{"val":"30","date":3}]
}
const filteredObject = Object.fromEntries(
Object.entries(theObject)
.map(([key, subarr]) => [key, [subarr[subarr.length - 1]]])
.filter(([key, subarr]) => subarr[0].val && subarr[0].date >= 2)
);
console.log(filteredObject);

You could reduce the entries.
const
data = { a: [{ val: "", date: 2 }, { val: 20, date: 2 }], b: [{ val: "", date: 2 }], c: [{ val: "10", date: 1 }, { val: 20, date: 2 }, { val: "30", date: 3 }] },
result = Object.assign({}, ...Object
.entries(data)
.map(([k, v]) => v.reduce((r, o) => {
if (!o.val) return r;
if (o.date > 2) return { [k]: [o] };
(r[k] ??= []).push(o);
return r;
}, {}))
);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

You can make a configurable function that receives an object containing information on how to validate each field.
If that object doesn't contain a certain field it will consider it valid by default (the || (() => true)) part)
This way when the rules change, you only change/add items in the validators object, leaving the function body intact. So it means there is less coupling between the code and the shape of the object.
const theObject = {
"a": [{"val":"","date":2},{"val":20,"date":2}],
"b": [{"val":"","date":2}],
"c": [{"val":"10","date":1},{"val":20,"date":2},{"val":"30","date":3}]
}
// put the validation rules here for each field
// if a field has no validator it will be considered valid
const validators = {
val: x => x !== "", // for the `val` field to be valid it has to be !== ""
date: x => x >= 2, // for the `date` field to be valid it has to be >= 2
}
// Function that filters
const filterObject = (obj, validators) =>
Object.entries(obj).reduce((acc, [key, array]) => {
const filteredArray = array.filter((arrayItem) =>
Object.entries(arrayItem).every(([k, v]) =>
(validators[k] || (() => true))(v)));
return { ...acc, ...(filteredArray.length && { [key]: filteredArray }) };
}, {});
// enjoy!
console.log(filterObject(theObject, validators));

Using two for loops is fine. The data is 2 dimensional, so its not bad. Plus, using javascript means that your performance doesnt matter much anyway, unless its awful.
In terms of "simple" below is my style.
Just change "true" to your condition.
One thing I'd like to point out is to use IIFE without polluting namespace, meaning that this filtering function should not directly access variables, but receive them as arguements. This is also good for code reusing and maintaining
((obj,result)=>{
for (i in Object.keys(obj)){
let a = {}
for (item in obj[i]){
if (true){
a = item;
}
}
result[i] = [a]
}
})(theObject, myFilteredObject)

Related

Remove random parts of an object (Chaos Monkey Style)

I have a JavaScript object e.g.:
const testFixture = {
a: [
{b:1},
{b:2},
{b:3},
],
b: {c: {d: 44, e: "foo", f: [1,2,3]}}
c: 3,
d: false,
f: "Blah",
}
I'd like to have a function I could pass this object to that would mutate it to remove random properties from it, so that I can test whether the thing that uses this object displays an error state, rather than silently erroring.
Edit:
To be clear, I mean any deeply nested property. e.g. it might remove a.b.c.d.e.f[1] or a[2].b
Edit 2:
Here's a buggy solution I'm working on based on ideas from Eureka and mkaatman's answers.
It seems to be changing key names to "undefined" which I wasn't expecting. It's also changing numbers to {} which I wasn't expecting. Not sure why.
var testFixture2 = {
a: [{
b: 1, c: 2
},
{
b: 2, c: 2
},
{
b: 3, c: 2, d: "bar"
},
],
b: {
c: {
d: 44,
e: "foo",
f: [1, 2, 3]
}
},
c: 3,
d: false,
f: "Blah"
};
function getRandomIndex(max) {
return Math.floor(Math.random() * max);
}
function chaosMonkey(thing) {
if (typeof thing === "object") {
console.log("object", Object.keys(thing).length, thing);
const newlyDeformedObject = { ...thing};
// Make a list of all the keys
const keys = Object.keys(thing);
// Choose one at random
const iKey = getRandomIndex(keys.length);
let target = newlyDeformedObject[keys[iKey]];
const shouldDelete = getRandomIndex(3) === 0;
if (shouldDelete) {
delete target;
console.log("Object deleted", keys[iKey]);
} else {
console.log("+++ Going deeper", thing);
newlyDeformedObject[keys[iKey]] = chaosMonkey({ ...newlyDeformedObject[keys[iKey]] });
}
return newlyDeformedObject;
} else if (typeof thing === "array") {
console.log(array);
const iKey = getRandomIndex(thing.length);
const shouldDelete = getRandomIndex(3) === 0;
if (shouldDelete) {
delete array[iKey];
console.log("Array deleted", iKey);
} else {
array[iKey] = chaosMonkey(array[iKey]);
return array;
}
} else {
//#todo do something bad based on type e.g. number -> NaN, string -> '', but these are less likely to break something
delete thing;
return;
}
}
console.log(JSON.stringify(chaosMonkey(testFixture2), null, 2));
NB: the chances of any object key or array item being recursed into are equal, in order to make modifications equally likely anywhere in the object.
Edit 3:
Additional Requirement:
It MUST always remove at least one thing.
Bonus points for:
ways to control the number of things that get deleted
any way to limit which properties get deleted or recursed into.
i.e. allow/deny lists, where:
allowRemovalList = properties that it's ok to remove
denyRemovalList = properties that it's not ok to remove
(It could be that you have some properties that it's ok to remove entirely, but they should not be recursed into and inner parts of them removed.)
NB: Originally I asked for whitelist/blacklist but this caused confusion (and I wouldn't want anyone copying this code to be surprised when they use it) and some answers have implemented it so that blacklist = properties to always remove. I won't penalise any answer for that (and it's trivial to change anyway).
I took a stab at it because I thought the question was interesting and unique. This is a bit sloppy but maybe it's a start if someone else is wondering how to do this in the future.
const testFixture = {
a: [{
b: 1
},
{
b: 2
},
{
b: 3
},
],
b: {
c: {
d: 44,
e: "foo",
f: [1, 2, 3]
}
},
c: 3,
d: false,
f: "Blah"
};
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
function chaosMonkey(object) {
console.log(Object.keys(testFixture).length, object);
const newlyDeformedObject = { ...object
};
Object.keys(testFixture).forEach((item, index) => {
const shouldDelete = getRandomInt(2);
console.log(index, shouldDelete);
if (shouldDelete) {
delete newlyDeformedObject[item];
} else {
if (typeof newlyDeformedObject[item] === "object") {
console.log("+++ Going deeper", { ...newlyDeformedObject[item]
});
newlyDeformedObject[item] = chaosMonkey({ ...newlyDeformedObject[item]
});
}
}
});
return newlyDeformedObject;
}
console.log(chaosMonkey(testFixture));
Assuming you mean random properties of the root of the object (not properties of properties or properties of array elements)
const testFixture = {
a: [{
b: 1
},
{
b: 2
},
{
b: 3
},
],
b: {
c: {
d: 44,
e: "foo",
f: [1, 2, 3]
}
},
c: 3,
d: false,
f: "Blah",
}
// Make a list of all the keys
const keys = Object.keys(testFixture);
// Choose one at random
const iKey = Math.floor(Math.random() * keys.length);
// (For simplicity we are making the assumption that there will always be at least one key)
const deleteKey = keys[iKey]
// Build a new object, that has the all the properties of the old one, except the property selected for deletion.
const out = {};
keys.forEach(key => {
if (key !== deleteKey) {
out[key] = testFixture[key]
}
})
console.log(out)
Modifying the OP's code, to achieve deep deletion
Great that you have joined in the coding and shown a nearly-complete answer! That is much more likely to engage people's curiosity. I think your code is nearly there: just make sure to do your deletion directly from the target object, otherwise you only delete a temporary variable "target".
Does the below do what you want? (Only one line changed)
var testFixture2 = {
a: [{
b: 1,
c: 2
},
{
b: 2,
c: 2
},
{
b: 3,
c: 2,
d: "bar"
},
],
b: {
c: {
d: 44,
e: "foo",
f: [1, 2, 3]
}
},
c: 3,
d: false,
f: "Blah"
};
function getRandomIndex(max) {
return Math.floor(Math.random() * max);
}
function chaosMonkey(thing) {
if (typeof thing === "object") {
console.log("object", Object.keys(thing).length, thing);
const newlyDeformedObject = { ...thing
};
// Make a list of all the keys
const keys = Object.keys(thing);
// Choose one at random
const iKey = getRandomIndex(keys.length);
let target = newlyDeformedObject[keys[iKey]];
const shouldDelete = getRandomIndex(3) === 0;
if (shouldDelete) {
// In this line below, we delete the property from "newlyDeformedObject", not just delete the variable "target"
delete newlyDeformedObject[keys[iKey]];
console.log("Object deleted", keys[iKey]);
} else {
console.log("+++ Going deeper", thing);
newlyDeformedObject[keys[iKey]] = chaosMonkey({ ...newlyDeformedObject[keys[iKey]]
});
}
return newlyDeformedObject;
} else if (typeof thing === "array") {
console.log(array);
const iKey = getRandomIndex(thing.length);
const shouldDelete = getRandomIndex(3) === 0;
if (shouldDelete) {
delete array[iKey];
console.log("Array deleted", iKey);
} else {
array[iKey] = chaosMonkey(array[iKey]);
return array;
}
} else {
//#todo do something bad based on type e.g. number -> NaN, string -> '', but these are less likely to break something
delete thing;
return;
}
}
console.log(JSON.stringify(chaosMonkey(testFixture2), null, 2));
Here is a solution that features whitelisting and blacklisting, considering blacklisting takes priority over whitelisting:
const testFixture = {
a: [{ b: 1 }, { b: 2 }, { b: 3 }],
b: { c: { d: 44, e: "foo", f: [1, 2, 3] } },
c: 3,
d: false,
f: "Blah",
};
const whiteList = [
["a", "2", "b"],
["b", "c", "e"],
];
const blackList = [
["a", "1", "b"],
["b", "c", "d"],
];
// Partial match because if a sub-property is whitelisted, the full path has to remain untouched
const isInWhiteList = (input) =>
whiteList.some((x) =>
input.reduce((acc, cur, i) => cur === x[i] && acc, true)
);
// Exact match
const isInBlackList = (input) =>
blackList.some(
(x) =>
x.length === input.length &&
input.reduce((acc, cur, i) => cur === x[i] && acc, true)
);
const chaosMonkey = (
input,
chanceOfBeingDeleted = 0.2, // Probability of property deletion
deep = true, // Remove only the deepest properties? If set to false, removes intermediate ones as well
path = []
) => {
if (typeof input !== "object") return;
const propsToDelete = [];
const itemsDeletedArr = [];
// Calculate properties to delete
for (const item in input) {
const currentPath = [...path, item];
if (
(isInBlackList(currentPath) ||
(!isInWhiteList(currentPath) &&
Math.random() < chanceOfBeingDeleted)) &&
(!deep || typeof input[item] !== "object")
) {
propsToDelete.push(item);
} else {
const itemsDeleted = chaosMonkey(
input[item],
chanceOfBeingDeleted,
deep,
currentPath
);
itemsDeletedArr.push(itemsDeleted);
}
}
// Delete properties
if (input instanceof Array) {
// Delete indexes in reverse direction to prevent indexes shifting
propsToDelete.reverse().forEach((x) => input.splice(x, 1));
} else {
propsToDelete.forEach((x) => delete input[x]);
}
// Has deleted at least one property?
return (
!!propsToDelete.length ||
itemsDeletedArr.reduce((acc, cur) => acc || cur, false)
);
};
// Optionally pass a chance of being deleted as second parameter
while (!chaosMonkey(testFixture)) {
console.log("chaosMonkey didn't change anything, retrying");
}
console.log(testFixture);
I have a very different solution that depending on your needs might be a clever one or not very useful.
Javascript can define a Proxy to be the interface between an object and whatever script is using that object. For example:
const obj = { a: 2 }
const proxy = new Proxy(obj, {
get: () => 3
})
console.log(obj.a) // 2
console.log(proxy.a) // 3
So I'm proposing a russian doll of proxies so that every time something tries to access any property of your object, deeply nested or not, there is some likelihood to receive undefined instead.
const obj = {
a: [
{ b: 1 },
{ b: 2 },
{ b: 3 },
],
b: {
c: {
d: 44,
e: "foo",
f: [1, 2, 3],
},
},
c: 3,
d: false,
f: "Blah",
}
function makeRandomlyInaccessible(object, likelihood) {
const handler = {
get(target, key, receiver) {
if (
key in target
&& Math.random() < likelihood
) {
return undefined
}
const result = Reflect.get(target, key, receiver)
if (typeof result === "object") {
return new Proxy(result, handler)
}
return result
},
}
return new Proxy(object, handler)
}
const monkeyObj = makeRandomlyInaccessible(obj, .1)
console.log(monkeyObj.a[1].b) // 2
console.log(monkeyObj.a[1].b) // 2
console.log(monkeyObj.a[1].b) // undefined
console.log(monkeyObj.a[1].b) // 2
console.log(monkeyObj.a[1].b) // Uncaught TypeError: Cannot read property '1' of undefined
The property get I'm defining in this proxy is called a "trap". This one traps every call to get a property value. But there are many other traps you could play with depending on what you need / want to try.
If you need the object to be stable (meaning that random properties are missing, but if they are found they are always found, and if they are undefined they are always undefined), you could just memoize the results of the traps like this:
function makeRandomlyInaccessible(object, likelihood) {
const trapMap = new Map()
const handler = {
get(target, key) {
if (!trapMap.has(target)) {
trapMap.set(target, {})
}
const traps = trapMap.get(target)
if (key in traps) {
return traps[key]
}
if (
!(key in traps)
&& Math.random() < likelihood
) {
traps[key] = undefined
return undefined
}
const result = target[key]
if (typeof result === "object" && result !== null) {
traps[key] = new Proxy(result, handler)
} else {
traps[key] = result
}
return traps[key]
},
}
return new Proxy(object, handler)
}
This approach allows you to define lists to customize the behavior, where each property is defined as a "path to the property" (for example "b.c.f[1]"), as well as the exact deleteCount.
doNotRemove: properties specified here must not be removed, which implies that all of their parents must not be removed either
mustRemove: properties specified here must be removed
canRemoveButNotModify: properties specified here can be removed, but all of their children must not be removed
This approach could easily be tweaked to use a "delete ratio" instead of an absolute count.
The basic principle is:
we recursively go over the entire object (breadth first) and construct a list of "path to a property", with a JS-like grammar (for example "b.c.f[1]")
we match the obtained list with doNotRemove, mustRemove, and canRemoveButNotModify (thanks to a few very basic utility functions) to get a list of properties to delete
we delete the properties in the list
const obj = {
a: [
{ b: 1 },
{ b: 2 },
{ b: 3 },
],
b: {
c: {
d: 44,
e: "foo",
f: [1, 2, 3],
},
},
c: 3,
d: false,
f: "Blah",
}
deleteRandomProps(obj, {
deleteCount: 5,
doNotRemove: [
'a[1]',
'b.c',
],
mustRemove: [
'a[2]',
'b.c.e',
],
canRemoveButNotModify: [
"b.c.f"
],
})
console.log('result', obj)
function deleteRandomProps(obj, {
deleteCount = 0, // number of deletion
doNotRemove = [], // prevent deletion of property and all its children, of the shape "a[0].b"
mustRemove = [], // force deletion of property, of the shape "a[0].b"
canRemoveButNotModify = [], // can delete property entirely but not modify its children, of the shape "a[0].b"
}) {
// list all possible property paths
const listOfAllPaths = listPaths(obj)
// prevent deletion of doNotRemove items
doNotRemove
.flatMap(path => allPathsThatLeadToPath(path))
.forEach(path => {
// remove all of doNotRemove from full list
removeItemFromList(path, listOfAllPaths)
// remove all of doNotRemove from mustRemove
removeItemFromList(path, mustRemove)
})
// prevent deletion of items that are children of canRemoveButNotModify items
canRemoveButNotModify
.forEach(path => {
// remove from full list
removeChildPaths(path, listOfAllPaths)
// remove from mustRemove
removeChildPaths(path, mustRemove)
})
// remove from list all properties that are children of a property in mustRemove
mustRemove.forEach(path => {
removeItemFromList(path, listOfAllPaths)
removeChildPaths(path, listOfAllPaths)
})
// start from mustRemove and add until deleteCount is reached
const deletions = [...mustRemove]
while (deletions.length < deleteCount && listOfAllPaths.length > 0) {
const path = removeRandomItemFromList(listOfAllPaths)
// remove from list all properties that are children of the one we're deleting
removeItemFromList(path, listOfAllPaths)
removeChildPaths(path, listOfAllPaths)
// remove from deletions all properties that are children of the new one
removeItemFromList(path, deletions)
removeChildPaths(path, deletions)
deletions.push(path)
}
// delete properties from object
console.log('deleting props', deletions)
deletions.forEach(path => {
deleteFromPath(obj, path)
})
return obj
}
// create a list of all possible property paths, of the shape "a[0].b"
function listPaths(obj, prefix = '') {
if (typeof obj !== 'object') {
return []
}
const pureKeys = Object.keys(obj)
const keys = prefix
? pureKeys.map(key => `${prefix}.${key}`)
: pureKeys
const values = Object.values(obj)
const more = []
values.forEach((value, index) => {
if (Array.isArray(value)) {
value.forEach((item, i) => {
const newKey = `${keys[index]}[${i}]`
more.push(newKey)
more.push(...listPaths(item, newKey))
})
} else if (typeof value === 'object') {
more.push(...listPaths(value, keys[index]))
}
})
return [...keys, ...more]
}
// recursively find property based on list of keys, of the shape ["a", "0", "b"]
function findFromArray(obj, array) {
if (array.length === 0) {
return obj
}
const [key, ...rest] = array
if (key in obj) {
return findFromArray(obj[key], rest)
}
}
// turn path into list of keys ("a[0].b" => ["a", "0", "b"])
function pathToParticles(path) {
const regex = /([a-z0-9])/gi
const particles = path.match(regex)
return particles || []
}
// deletes a property based on property path, of the shape "a[0].b"
function deleteFromPath(obj, path) {
const particles = pathToParticles(path)
const last = particles.pop()
const lastObject = findFromArray(obj, particles)
if (lastObject) {
delete lastObject[last]
}
}
// turn path into list of path that lead to it ("a[0].b" => ["a", "a[0]", "a[0].b"])
function allPathsThatLeadToPath(path) {
const regex = /[\[\.]/gi
const list = [path]
let match
while ((match = regex.exec(path)) !== null) {
list.push(path.substring(0, match.index))
}
return list
}
// remove item from an array
function removeItemFromList(item, list) {
const index = list.indexOf(item)
if (index !== -1) {
list.splice(index, 1)
}
return list
}
// remove from list all property paths that are children of a property path
// for example "a[0]" will remove "a[0].b", "a[0].b.c", ... but not "a[0]" itself
function removeChildPaths(path, list) {
for(let i = list.length; i > 0; i--) {
const item = list[i-1]
if (item !== path && item.startsWith(path)) {
list.splice(i-1, 1)
}
}
}
// remove a random prop from a list
function removeRandomItemFromList(list) {
const index = Math.floor(Math.random() * list.length)
const [item] = list.splice(index, 1)
return item
}
I went for a more simplistic approach. My function builds out a list of all key-value pairs and selects one at random each time it runs. For the sake of this example, I log the name of the removed key each time the function runs along with the stringified object, then run it again, until all keys are successfully removed.
Solution #1
const testFixture = {
a: [{b:1},{b:2},{b:3},],
b: {c: {d: 44, e: "foo", f: [1,2,3]}},
c: 3,
d: false,
f: "Blah",
};
const chaosMonkey = obj => {
const getObjKeysPairs = (obj, parent = false) => {
const objKeysPairs = [];
for (const [key, value] of Object.entries(obj)) {
const parentKeyPair = parent ? parent + "." + key : key;
objKeysPairs.push([obj, key, parentKeyPair]);
if (typeof value === 'object' && value !== null) {
objKeysPairs.push(...getObjKeysPairs(value, parentKeyPair));
}
}
return objKeysPairs;
};
const objKeyPairs = getObjKeysPairs(obj),
[object, key, path] = objKeyPairs[Math.floor(Math.random() * objKeyPairs.length)];
console.log("Deleting " + path + " === " + object[key]);
delete object[key];
};
while (Object.keys(testFixture).length) {
chaosMonkey(testFixture);
}
console.log("testFixture:", testFixture);
if (Object.keys(testFixture).length === 0) console.log("testFixture emptied successfully");
This solution works as-is and deletes keys recursively for both arrays and objects. Using the delete operator however, array items are deleted but not entirely removed, leaving an empty value remaining.
This does not throw any errors, even silently, because empty values are not recognized as valid key-value pairs and therefore are not "re-attempted" as values once deleted initially.
NOTE: The below explanation is for the first version of this solution. It is still valuable and so I am not removing it, but please see the older version of this solution to see its relevance.
One JS concept worthy of a deeper explanation is this:
if ([Object, Array].includes(value.constructor)) { /* ... */ }
Every data type in JavaScript is built upon classes. Up until recently, classes were defined using a functional syntax. ES6+ introduced a new class declaration, which makes creating new classes arguably much easier and essentially decorates the classical syntax.
Each class's constructor can be referenced by its name like a variable (without quotes) or by its name as a string. Here is an example of this:
class CustomObject extends Object {
constructor() {
super();
}
}
const myVar = new CustomObject();
myVar.constructor === CustomObject; // true
myVar.constructor.name === "CustomObject"; // true
Another important concept here is the prototypal chain. Every object and type in JavaScript is a prototyped instance of another object, so if you traverse through the prototypal chain you can evaluate any ancestor prototype of any given object, even simple strings or numbers, until you reach the most primitive object which has no prototype and is the ultimate prototype inherited by all types in JavaScript. Here are a few examples of that:
(5).constructor === (5).__proto__?.constructor; // true
(5).__proto__.constructor; // => Number
(5).__proto__.__proto__?.constructor; // => Object
(5).__proto__.__proto__.__proto__?.constructor; // => undefined
"string".constructor === "string".__proto__?.constructor; // true
"string".__proto__.constructor; // => String
"string".__proto__.__proto__?.constructor; // => Object
"string".__proto__.__proto__.__proto__?.constructor; // => undefined
class CustomObject extends Object { constructor() { super(); }}
const myVar = new CustomObject();
myVar.__proto__.constructor; // => CustomObject
myVar.__proto__.__proto__?.constructor; // => Object
myVar.__proto__.__proto__.__proto__?.constructor; // => undefined
In your example, this is all very useful for us for the simple purpose of checking whether the value of one of the object's values, regardless of depth, is a nested object or array. Like the examples above, arrays and objects also have unique constructors. The constructor for arrays is Array and the constructor for objects is Object. I am simply checking to see if the value being evaluated is either one of those types and if so evaluate its own child properties/elements via recursion.
Recursion, if you aren't already familiar with it is the process by which a function calls itself, often passing information allowing the function to reach whatever depth necessary to gather all the information available. This is exactly what we need in your case to build the complete list of all properties in the main object.
Lastly, my purpose in using [Object, Array].includes(...) is simply because is a bit easier than using value.constructor === Object || value.constructor === Array. Using the includes Array.prototype method allows us in this case to check whether the value we are checking, value.constructor is equal to either Object or Array.
There is a spec floating around out there for use of the bitwise OR operator | to do something like this so we can write these more naturally like this: value.constructor === Object | Array. However, this is purely conceptual at this point and may only work in specific use-cases such as XHTTP requests and fetch.
Solution #2
In my second solution below, I add an additional check at the end of the function to test whether object.constructor === Array and if so, I use the splice Array.prototype method to completely remove the array element rather than only deleting it, which leaves an empty value behind at the deleted index.
If however, you would prefer to remove the array items altogether as the function runs, you can do so using the splice array method and simply run the splice conditionally based on the constructor.
Here is how that would work:
const testFixture = {
a: [{b:1},{b:2},{b:3},],
b: {c: {d: 44, e: "foo", f: [1,2,3]}},
c: 3,
d: false,
f: "Blah",
};
const chaosMonkey = obj => {
const getObjKeysPairs = (obj, parent = false) => {
const objKeysPairs = [];
for (const [key, value] of Object.entries(obj)) {
const parentKeyPair = parent ? parent + "." + key : key;
objKeysPairs.push([obj, key, parentKeyPair]);
if (typeof value === 'object' && value !== null) {
objKeysPairs.push(...getObjKeysPairs(value, parentKeyPair));
}
}
return objKeysPairs;
};
const objKeyPairs = getObjKeysPairs(obj),
[object, key, path] = objKeyPairs[Math.floor(Math.random() * objKeyPairs.length)];
console.log("Deleting " + path + " === " + object[key]);
delete object[key];
if (Array.isArray(object)) object.splice(key, 1);
};
while (Object.keys(testFixture).length) {
chaosMonkey(testFixture);
}
console.log("testFixture:", testFixture);
if (Object.keys(testFixture).length === 0) console.log("testFixture emptied successfully");
Here is an answer I'm not entirely happy with. It does randomly remove leaf properties from an object, and it does that fairly well.
But -- and while the question did not specify this, I think it's an interesting possible extension -- this version only removes leaf nodes; it might be preferable to also remove other branches. While I did think through some possibilities here, nothing really seemed to gel. It's not a matter of implementation, but of coming up with good requirements. Do we treat all paths equally? What does it mean to remove a branch when we want to keep some of its subbranches? Should we weight the chances of removal higher for leaves than for heavy-weight branches? And so on.
But, for what this does do, I'm pretty happy with this solution. It basically converts an object to an array of pairs representing paths to a node and that node's value. Then we randomly delete some entries from that array and reconstitute an object from the remaining values.
It looks like this:
// utility functions
const pathEntries = (obj) =>
Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, x]) => pathEntries (x) .map (([p, v]) => [[Array.isArray(obj) ? Number(k) : k, ... p], v])
)
: [[[], obj]]
const setPath = ([p, ...ps]) => (v) => (o) =>
p == undefined ? v : Object .assign (
Array .isArray (o) || Number.isInteger (p) ? [] : {},
{...o, [p]: setPath (ps) (v) ((o || {}) [p])}
)
const hydrate = (xs) =>
xs .reduce ((a, [p, v]) => setPath (p) (v) (a), {})
// helper function
const randomRemove = (xs, count = 1, i = Math .floor (Math .random () * xs .length)) =>
count <= 0
? [...xs]
: randomRemove ([... xs .slice (0, i), ... xs .slice (i + 1)], count - 1)
// main function
const chaosMonkey = (o, count = 1) =>
hydrate (
randomRemove (pathEntries (o), count)
)
// sample data
const testFixture = {a: [{b: 1}, {b: 2}, {b: 3}], b: {c: {d: 44, e: "foo", f: [1, 2, 3]}}, c: 3, d: false, f: "Blah"}
// demo
console .log (chaosMonkey (testFixture, 3))
.as-console-wrapper {max-height: 100% !important; top: 0}
This is built on three utility functions that I've used often on StackOverflow:
pathEntries creates an array of items such as [['a', 2, 'b'], 3], which describes the property b of the element with index 2 in the array stored in the a property of the root object, and notes that it has value 3.
setPath takes a path and value like that and a base object and sets the corresponding value along that path, creating new nodes as needed.
hydrate simply runs setPath for each of an array of such entries, building a new minimal object with those values.
We also create a simple helper function randomRemove which removes random elements from an array. This is a simple recursion on the number of items to remove, returning the original array if the count is zero, otherwise removing one random element and recurring with one less than count.
On top of those, we write our chaosMonkey function, that simply calls pathEntries on the object, calls randomRemove on the result and then hydrate to build a new object.
Besides the large question mentioned above, there are two possible changes I imagine we might consider. Most importantly, this will allow us to remove entries in the middle of our arrays, leaving us with sparse arrays. We may want to avoid that. This is a naive fix for that problem:
const chaosMonkey = (o, count = 1) =>
hydrate (
randomRemove (pathEntries (o) .filter (xs => !Number .isInteger (xs [xs .length])), count)
)
which avoids removing any array elements at all. You can see it in this snippet:
// utility functions
const pathEntries = (obj) =>
Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, x]) => pathEntries (x) .map (([p, v]) => [[Array.isArray(obj) ? Number(k) : k, ... p], v])
)
: [[[], obj]]
const setPath = ([p, ...ps]) => (v) => (o) =>
p == undefined ? v : Object .assign (
Array .isArray (o) || Number.isInteger (p) ? [] : {},
{...o, [p]: setPath (ps) (v) ((o || {}) [p])}
)
const hydrate = (xs) =>
xs .reduce ((a, [p, v]) => setPath (p) (v) (a), {})
// helper function
const randomRemove = (xs, count = 1, i = Math .floor (Math .random () * xs .length)) =>
count <= 0 ? [...xs] : randomRemove ([... xs .slice (0, i), ... xs .slice (i + 1)], count - 1)
// main function
const chaosMonkey = (o, count = 1) =>
hydrate (
randomRemove (pathEntries (o) .filter (xs => !Number .isInteger (xs [xs .length])), count)
)
// sample data
const testFixture = {a: [{b: 1}, {b: 2}, {b: 3}], b: {c: {d: 44, e: "foo", f: [1, 2, 3]}}, c: 3, d: false, f: "Blah"}
// demo
console .log (chaosMonkey (testFixture, 3))
.as-console-wrapper {max-height: 100% !important; top: 0}
We might consider a more sophisticated version of this that actually does delete array entries but doesn't leave the holes. While we could extend this technique to handle that, it might be more effort than it's worth, and perhaps we would instead write a more sophisticated hydrate.
Second, this requires us to specify the number of paths to delete. It seems more in the spirit of the problem to make that number also random, or to consider a fraction of the paths involved, or do something still more interesting. I think these would mostly be simple variants of the above, and I leave that to the reader.

Aggregate same key values into an array and avoid undefined

I am trying to aggregate the same key values into an array by value.
so for example I have an array of objects, like so
const data = [{foo: true},{foo: false},{bar: true},{buzz: false}]
when they get aggregated the array transforms into
[
foo: {true: [{foo: true}], false: [{foo: false}]},
bar: {true: [{bar: true}]},
buzz: {false: [{buzz: false}]}
]
the array entries is the original object.
Now I know the keys that I want to group by..
they are foo, bar, buzz and fizz.
But fizz is not part of the original array, so the return is undefined, like so
[
foo: {true:[{foo: true}], false: [{foo: false}]},
bar: {true: [{bar: true}]},
buzz: {false: A[{buzz: false}]}
fizz: {undefined: [{foo: true},{foo: false},{bar: true},{buzz: false}]}
],
how do I reduce the original array without including the fizz value that is undefined?
code here:
let v = [];
let types = ['foo', 'bar', 'buzz', 'fizz' ]
for (let x = 0; x < types.length; x++) {
let data = data.reduce((acc, i) => {
if (!acc[i[types[x]]]) {
acc[i[types[x]]] = [i]
}
else if (Array.isArray(acc[i[types[x]]])) {
acc[i[types[x]]].push(i);
}
else if (typeof acc[i[types[x]]] === 'object') {
acc[i[types[x]]] = [acc[i[types[x]]]]
acc[i[types[x]]].push(i)
}
return acc;
}, {})
v.push({ [types[x]]: data });
}
return v;
You were close, you just need to check if the property you were adding was undefined before adding. You can also check if the reduced object has any properties before adding to the result object.
Note that this may not be the most efficient way of doing it, but sometimes it's better to understand the code than it is to have highly efficient code.
const data = [{
foo: true
}, {
foo: false
}, {
bar: true
}, {
buzz: false
}];
let v = [];
let types = ['foo', 'bar', 'buzz', 'fizz']
for (let x = 0; x < types.length; x++) {
let reduced = data.reduce((acc, i) => {
// /* Added this type check */
if (!acc[i[types[x]]] && typeof i[types[x]] !== 'undefined') {
acc[i[types[x]]] = [i]
} else if (Array.isArray(acc[i[types[x]]])) {
acc[i[types[x]]].push(i);
} else if (typeof acc[i[types[x]]] === 'object') {
acc[i[types[x]]] = [acc[i[types[x]]]]
acc[i[types[x]]].push(i)
}
return acc;
}, {});
// Doesn't add a property for the type if there are no data
if (Object.keys(reduced).length) {
v.push({
[types[x]]: reduced
});
}
}
console.log(v);
Have a look at how Array.prototype.reduce works. It might be the right method to build your approach upon.
A generic way of solving the OP's problem was to iterate the provided data array. For each item one would extract its key and value. In case the item's key is listed (included) in another provided types array, one would continue creating a new data structure and collecting the currently processed item within the latter.
One does not want to iterate the types array for it will cause a unnecessarily complex lookup for the data items, each time a type item is going to be processed.
Thus a generically working (better code reuse) reduce method might be the best solution to the OP's problem ...
const sampleDataList = [
{ foo: true },
{ foo: false },
{ bar: true },
{ baz: false },
{ buzz: false },
{ baz: false },
{ bar: true }
];
// foo: {true: [{foo: true}], false: [{foo: false}]},
// bar: {true: [{bar: true}]},
// buzz: {false: [{buzz: false}]}
function collectItemIntoInclusiveKeyValueGroup(collector, item) {
const { inclusiveKeyList, index } = collector;
const firstItemEntry = Object.entries(item)[0];
const key = firstItemEntry[0];
const isProceedCollecting = ( // proceed with collecting ...
//
!Array.isArray(inclusiveKeyList) // - either for no given list
|| inclusiveKeyList.includes(key) // - or if item key is listed.
);
if (isProceedCollecting) {
let keyGroup = index[key]; // access the group identified
if (!keyGroup) { // by an item's key, ... or ...
// ...create it in case ...
keyGroup = index[key] = {}; // ...it did not yet exist.
}
const valueLabel = String(firstItemEntry[1]); // item value as key.
let valueGroupList = keyGroup[valueLabel]; // acces the group list
if (!valueGroupList) { // identified by an item's
// value, ...or create it in
valueGroupList = keyGroup[valueLabel] = []; // case it did not yet exist.
}
// push original reference into a grouped
// key value list, as required by the OP.
valueGroupList.push(item);
}
return collector;
}
console.log(
"'foo', 'bar', 'buzz' and 'fizz' only :",
sampleDataList.reduce(collectItemIntoInclusiveKeyValueGroup, {
inclusiveKeyList: ['foo', 'bar', 'buzz', 'fizz'],
index: {}
}).index
);
console.log(
"'foo', 'bar' and 'baz' only :",
sampleDataList.reduce(collectItemIntoInclusiveKeyValueGroup, {
inclusiveKeyList: ['foo', 'bar', 'baz'],
index: {}
}).index
);
console.log(
"all available keys :",
sampleDataList.reduce(collectItemIntoInclusiveKeyValueGroup, {
index: {}
}).index
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Try something like:
const data = [{foo: true},{foo: false},{bar: true},{buzz: false}];
let v = [];
let types = ['foo', 'bar', 'buzz', 'fizz' ];
for (let x = 0; x < types.length; x++) {
let filteredlist = data.filter(function (d) {
return Object.keys(d)[0] == types[x];
});
let isTrue = 0;
let isFalse = 0;
if (filteredlist.length > 0) {
for (let i = 0; i < filteredlist.length; i++) {
let trueOrfalse = eval("filteredlist[i]." + types[x]);
if (trueOrfalse) {
isTrue++;
} else {
isFalse++;
}
}
v.push(types[x], {true: isTrue, false: isFalse});
}
}
console.log(v);
Assuming you only want to count the number of each key (e.g. true or false) you can use the following code.
I've written this as a function named 'aggregate' so that it can be called multiple times with different arguments.
const initialData = [{foo: true},{foo: true},{foo: false},{bar: true},{buzz: false}];
const types = ['foo', 'bar', 'buzz', 'fizz'];
const aggregate = (data, types) => {
const result = {};
data.forEach(item => {
// Extract key & value from object
// Note: use index 0 because each object in your example only has a single key
const [key, value] = Object.entries(item)[0];
// Check if result already contains this key
if (result[key]) {
if (result[key][value]) {
// If value already exists, append one
result[key][value]++;
} else {
// Create new key and instantiate with value 1
result[key][value] = 1;
}
} else {
// If result doesn't contain key, instantiate with value 1
result[key] = { [value]: 1 };
}
});
return result;
};
console.log(aggregate(initialData, types));
This will output the following (note I've added another {foo: true} to your initialData array for testing).
The output should also be an object (not array) so that each key directly relates to its corresponding value, as opposed to an Array which will simply place the value as the next item in the Array (without explicitly linking the two).
{
foo: { true: 2, false: 1 },
bar: { true: 1 },
buzz: { false: 1 }
}

Grouping dynamic keys in array of objects

I have an object that i am trying to transform
var data = {
"A": {"XY1" : 1},
"B": {"XY2": 12},
"C": {"XY3": 10},
"D": {"XY1": 2}
am trying to transform this to
[
{ "XY1": { 1:"A", 2:"D"}},
{ "XY2": { 12:"B"}},
{ "XY3": { 8:"A", 10:"C"}},
]
(we can ignore the ordering of XY1, XY2 etc)
Here is what i have done so far -
var result = Object.keys(data).flatMap(alphabet => {
return Object.keys(data[alphabet]).map(group => {
return Object.assign({}, {
[group]: { [data[alphabet][group]]: alphabet }
})
});
});
console.log(result);
which prints
[
{"XY1":{"1":"A"}},
{"XY3":{"8":"A"}},
{"XY2":{"12":"B"}},
{"XY3":{"10":"C"}},
{"XY1":{"2":"D"}}
]
However, i want it to be grouped by using reduce(chaining), such as -
var result = Object.keys(data).flatMap(alphabet => {
return Object.keys(data[alphabet]).map(group => {
return Object.assign({}, {
[group]: { [data[alphabet][group]]: alphabet }
})
});
}).reduce((obj, item) => {
});
Is this possible ? How do i group by these dynamic keys?
Help much appreciated !
I'd group first using a hashtable:
const hash = {};
for(const [key, obj] of Object.entries(data)) {
for(const [key2, values] of Object.entries(obj)) {
if(!hash[key2]) hash[key2] = {};
for(const value of [values].flat())
hash[key2][value] = key;
}
}
To then get an array you can use Object.entries:
const result = Object.entries(hash).map(([key, value]) => ({ key, value }));
This is not exactly what you wanted, but to be honest I don't see a sense of having an array of objects with just one key each.

How can an array be grouped by two properties?

Ex:
const arr = [{
group: 1,
question: {
templateId: 100
}
}, {
group: 2,
question: {
templateId: 200
}
}, {
group: 1,
question: {
templateId: 100
}
}, {
group: 1,
question: {
templateId: 300
}
}];
Expected Result: const result = groupBy(arr, 'group', 'question.templateId');
const result = [
[{
group: 1,
question: {
templateId: 100
}
}, {
group: 1,
question: {
templateId: 100
}
}],
[{
group: 1,
question: {
templateId: 300
}
}],
[{
group: 2,
question: {
templateId: 200
}
}]
];
So far: I am able to group the result by a single property using Array.prototype.reduce().
function groupBy(arr, key) {
return [...arr.reduce((accumulator, currentValue) => {
const propVal = currentValue[key],
group = accumulator.get(propVal) || [];
group.push(currentValue);
return accumulator.set(propVal, group);
}, new Map()).values()];
}
const arr = [{
group: 1,
question: {
templateId: 100
}
}, {
group: 2,
question: {
templateId: 200
}
}, {
group: 1,
question: {
templateId: 100
}
}, {
group: 1,
question: {
templateId: 300
}
}];
const result = groupBy(arr, 'group');
console.log(result);
I would recommend to pass a callback function instead of a property name, this allows you to do the two-level-access easily:
function groupBy(arr, key) {
return Array.from(arr.reduce((accumulator, currentValue) => {
const propVal = key(currentValue),
// ^^^^ ^
group = accumulator.get(propVal) || [];
group.push(currentValue);
return accumulator.set(propVal, group);
}, new Map()).values());
}
Now you can do groupBy(arr, o => o.group) and groupBy(arr, o => o.question.templateId).
All you need to do for getting to your expected result is group by the first property and then group each result by the second property:
function concatMap(arr, fn) {
return [].concat(...arr.map(fn));
}
const result = concatMap(groupBy(arr, o => o.group), res =>
groupBy(res, o => o.question.templateId)
);
#Bergi's answer is really practical but I'll show you how building a multi-value "key" can be possible using JavaScript primitives – don't take this to mean Bergi's answer is bad in anyway; in fact, it's actually a lot better because of it's practicality. If anything, this answer exists to show you how much work is saved by using an approach like his.
I'm going to go over the code bit-by-bit and then I'll have a complete runnable demo at the end.
compound data equality
Comparing compound data in JavaScript is a little tricky, so we're gonna need to figure out a way around this first:
console.log([1,2] === [1,2]) // false
I want to cover a solution for the multi-value key because our entire answer will be based upon it - here I'm calling it a CollationKey. Our key holds some value and defines its own equality function which is used for comparing keys
const CollationKey = eq => x => ({
x,
eq: ({x: y}) => eq(x, y)
})
const myKey = CollationKey (([x1, x2], [y1, y2]) =>
x1 === y1 && x2 === y2)
const k1 = myKey([1, 2])
const k2 = myKey([1, 2])
console.log(k1.eq(k2)) // true
console.log(k2.eq(k1)) // true
const k3 = myKey([3, 4])
console.log(k1.eq(k3)) // false
wishful thinking
Now that we have a way to compare compound data, I want to make a custom reducing function that uses our multi-value key to group values. I'll call this function collateBy
// key = some function that makes our key
// reducer = some function that does our reducing
// xs = some input array
const collateBy = key => reducer => xs => {
// ...?
}
// our custom key;
// equality comparison of `group` and `question.templateId` properties
const myKey = CollationKey ((x, y) =>
x.group === y.group
&& x.question.templateId === y.question.templateId)
const result =
collateBy (myKey) // multi-value key
((group=[], x) => [...group, x]) // reducing function: (accumulator, elem)
(arr) // input array
So now that we know how we want collateBy to work, let's implement it
const collateBy = key => reducer => xs => {
return xs.reduce((acc, x) => {
const k = key(x)
return acc.set(k, reducer(acc.get(k), x))
}, Collation())
}
Collation data container
Ok, so we were being a little optimistic there too using Collation() as the starting value for the xs.reduce call. What should Collation be?
What we know:
someCollation.set accepts a CollationKey and some value, and returns a new Collation
someCollation.get accepts a CollationKey and returns some value
Well let's get to work!
const Collation = (pairs=[]) => ({
has (key) {
return pairs.some(([k, v]) => key.eq(k))
},
get (key) {
return (([k, v]=[]) => v)(
pairs.find(([k, v]) => k.eq(key))
)
},
set (key, value) {
return this.has(key)
? Collation(pairs.map(([k, v]) => k.eq(key) ? [key, value] : [k, v]))
: Collation([...pairs, [key, value]])
},
})
finishing up
So far our collateBy function returns a Collation data container which is internally implemented with an array of [key, value] pairs, but what we really want back (according to your question) is just an array of values
Let's modify collateBy in the slightest way that extracts the values – changes in bold
const collateBy = key => reducer => xs => {
return xs.reduce((acc, x) => {
let k = key(x)
return acc.set(k, reducer(acc.get(k), x))
}, Collation()).values()
}
So now we will add the values method to our Collation container
values () {
return pairs.map(([k, v]) => v)
}
runnable demo
That's everything, so let's see it all work now – I used JSON.stringify in the output so that the deeply nested objects would display all content
// data containers
const CollationKey = eq => x => ({
x,
eq: ({x: y}) => eq(x, y)
})
const Collation = (pairs=[]) => ({
has (key) {
return pairs.some(([k, v]) => key.eq(k))
},
get (key) {
return (([k, v]=[]) => v)(
pairs.find(([k, v]) => k.eq(key))
)
},
set (key, value) {
return this.has(key)
? Collation(pairs.map(([k, v]) => k.eq(key) ? [key, value] : [k, v]))
: Collation([...pairs, [key, value]])
},
values () {
return pairs.map(([k, v]) => v)
}
})
// collateBy
const collateBy = key => reducer => xs => {
return xs.reduce((acc, x) => {
const k = key(x)
return acc.set(k, reducer(acc.get(k), x))
}, Collation()).values()
}
// custom key used for your specific collation
const myKey =
CollationKey ((x, y) =>
x.group === y.group
&& x.question.templateId === y.question.templateId)
// your data
const arr = [ { group: 1, question: { templateId: 100 } }, { group: 2, question: { templateId: 200 } }, { group: 1, question: { templateId: 100 } }, { group: 1, question: { templateId: 300 } } ]
// your answer
const result =
collateBy (myKey) ((group=[], x) => [...group, x]) (arr)
console.log(result)
// [
// [
// {group:1,question:{templateId:100}},
// {group:1,question:{templateId:100}}
// ],
// [
// {group:2,question:{templateId:200}}
// ],
// [
// {group:1,question:{templateId:300}}
// ]
// ]
summary
We made a custom collation function which uses a multi-value key for grouping our collated values. This was done using nothing but JavaScript primitives and higher-order functions. We now have a way to iterate thru a data set and collate it in an arbitrary way using keys of any complexity.
If you have any questions about this, I'm happy to answer them ^_^
#Bergi's answer is great if you can hard-code the inputs.
If you want to use string inputs instead, you can use the sort() method, and walk the objects as needed.
This solution will handle any number of arguments:
function groupBy(arr) {
var arg = arguments;
return arr.sort((a, b) => {
var i, key, aval, bval;
for(i = 1 ; i < arguments.length ; i++) {
key = arguments[i].split('.');
aval = a[key[0]];
bval = b[key[0]];
key.shift();
while(key.length) { //walk the objects
aval = aval[key[0]];
bval = bval[key[0]];
key.shift();
};
if (aval < bval) return -1;
else if(aval > bval) return 1;
}
return 0;
});
}
const arr = [{
group: 1,
question: {
templateId: 100
}
}, {
group: 2,
question: {
templateId: 200
}
}, {
group: 1,
question: {
templateId: 100
}
}, {
group: 1,
question: {
templateId: 300
}
}];
const result = groupBy(arr, 'group', 'question.templateId');
console.log(result);

JavaScript: filter() for Objects

ECMAScript 5 has the filter() prototype for Array types, but not Object types, if I understand correctly.
How would I implement a filter() for Objects in JavaScript?
Let's say I have this object:
var foo = {
bar: "Yes"
};
And I want to write a filter() that works on Objects:
Object.prototype.filter = function(predicate) {
var result = {};
for (key in this) {
if (this.hasOwnProperty(key) && !predicate(this[key])) {
result[key] = this[key];
}
}
return result;
};
This works when I use it in the following demo, but when I add it to my site that uses jQuery 1.5 and jQuery UI 1.8.9, I get JavaScript errors in FireBug.
Object.prototype.filter = function(predicate) {
var result = {};
for (key in this) {
if (this.hasOwnProperty(key) && !predicate(this[key])) {
console.log("copying");
result[key] = this[key];
}
}
return result;
};
var foo = {
bar: "Yes",
moo: undefined
};
foo = foo.filter(function(property) {
return typeof property === "undefined";
});
document.getElementById('disp').innerHTML = JSON.stringify(foo, undefined, ' ');
console.log(foo);
#disp {
white-space: pre;
font-family: monospace
}
<div id="disp"></div>
First of all, it's considered bad practice to extend Object.prototype. Instead, provide your feature as stand-alone function, or if you really want to extend a global, provide it as utility function on Object, just like there already are Object.keys, Object.assign, Object.is, ...etc.
I provide here several solutions:
Using reduce and Object.keys
As (1), in combination with Object.assign
Using map and spread syntax instead of reduce
Using Object.entries and Object.fromEntries
1. Using reduce and Object.keys
With reduce and Object.keys to implement the desired filter (using ES6 arrow syntax):
Object.filter = (obj, predicate) =>
Object.keys(obj)
.filter( key => predicate(obj[key]) )
.reduce( (res, key) => (res[key] = obj[key], res), {} );
// Example use:
var scores = {
John: 2, Sarah: 3, Janet: 1
};
var filtered = Object.filter(scores, score => score > 1);
console.log(filtered);
Note that in the above code predicate must be an inclusion condition (contrary to the exclusion condition the OP used), so that it is in line with how Array.prototype.filter works.
2. As (1), in combination with Object.assign
In the above solution the comma operator is used in the reduce part to return the mutated res object. This could of course be written as two statements instead of one expression, but the latter is more concise. To do it without the comma operator, you could use Object.assign instead, which does return the mutated object:
Object.filter = (obj, predicate) =>
Object.keys(obj)
.filter( key => predicate(obj[key]) )
.reduce( (res, key) => Object.assign(res, { [key]: obj[key] }), {} );
// Example use:
var scores = {
John: 2, Sarah: 3, Janet: 1
};
var filtered = Object.filter(scores, score => score > 1);
console.log(filtered);
3. Using map and spread syntax instead of reduce
Here we move the Object.assign call out of the loop, so it is only made once, and pass it the individual keys as separate arguments (using the spread syntax):
Object.filter = (obj, predicate) =>
Object.assign(...Object.keys(obj)
.filter( key => predicate(obj[key]) )
.map( key => ({ [key]: obj[key] }) ) );
// Example use:
var scores = {
John: 2, Sarah: 3, Janet: 1
};
var filtered = Object.filter(scores, score => score > 1);
console.log(filtered);
4. Using Object.entries and Object.fromEntries
As the solution translates the object to an intermediate array and then converts that back to a plain object, it would be useful to make use of Object.entries (ES2017) and the opposite (i.e. create an object from an array of key/value pairs) with Object.fromEntries (ES2019).
It leads to this "one-liner" method on Object:
Object.filter = (obj, predicate) =>
Object.fromEntries(Object.entries(obj).filter(predicate));
// Example use:
var scores = {
John: 2, Sarah: 3, Janet: 1
};
var filtered = Object.filter(scores, ([name, score]) => score > 1);
console.log(filtered);
The predicate function gets a key/value pair as argument here, which is a bit different, but allows for more possibilities in the predicate function's logic.
Never ever extend Object.prototype.
Horrible things will happen to your code. Things will break. You're extending all object types, including object literals.
Here's a quick example you can try:
// Extend Object.prototype
Object.prototype.extended = "I'm everywhere!";
// See the result
alert( {}.extended ); // "I'm everywhere!"
alert( [].extended ); // "I'm everywhere!"
alert( new Date().extended ); // "I'm everywhere!"
alert( 3..extended ); // "I'm everywhere!"
alert( true.extended ); // "I'm everywhere!"
alert( "here?".extended ); // "I'm everywhere!"
Instead create a function that you pass the object.
Object.filter = function( obj, predicate) {
let result = {}, key;
for (key in obj) {
if (obj.hasOwnProperty(key) && !predicate(obj[key])) {
result[key] = obj[key];
}
}
return result;
};
Solution in Vanilla JS from year 2020.
let romNumbers={'I':1,'V':5,'X':10,'L':50,'C':100,'D':500,'M':1000}
You can filter romNumbers object by key:
const filteredByKey = Object.fromEntries(
Object.entries(romNumbers).filter(([key, value]) => key === 'I') )
// filteredByKey = {I: 1}
Or filter romNumbers object by value:
const filteredByValue = Object.fromEntries(
Object.entries(romNumbers).filter(([key, value]) => value === 5) )
// filteredByValue = {V: 5}
If you're willing to use underscore or lodash, you can use pick (or its opposite, omit).
Examples from underscore's docs:
_.pick({name: 'moe', age: 50, userid: 'moe1'}, 'name', 'age');
// {name: 'moe', age: 50}
Or with a callback (for lodash, use pickBy):
_.pick({name: 'moe', age: 50, userid: 'moe1'}, function(value, key, object) {
return _.isNumber(value);
});
// {age: 50}
ES6 approach...
Imagine you have this object below:
const developers = {
1: {
id: 1,
name: "Brendan",
family: "Eich"
},
2: {
id: 2,
name: "John",
family: "Resig"
},
3: {
id: 3,
name: "Alireza",
family: "Dezfoolian"
}
};
Create a function:
const filterObject = (obj, filter, filterValue) =>
Object.keys(obj).reduce((acc, val) =>
(obj[val][filter] === filterValue ? acc : {
...acc,
[val]: obj[val]
}
), {});
And call it:
filterObject(developers, "name", "Alireza");
and will return:
{
1: {
id: 1,
name: "Brendan",
family: "Eich"
},
2: {
id: 2,
name: "John",
family: "Resig"
}
}
As patrick already stated this is a bad idea, as it will almost certainly break any 3rd party code you could ever wish to use.
All libraries like jquery or prototype will break if you extend Object.prototype, the reason being that lazy iteration over objects (without hasOwnProperty checks) will break since the functions you add will be part of the iteration.
Given
object = {firstname: 'abd', lastname:'tm', age:16, school:'insat'};
keys = ['firstname', 'age'];
then :
keys.reduce((result, key) => ({ ...result, [key]: object[key] }), {});
// {firstname:'abd', age: 16}
// Helper
function filter(object, ...keys) {
return keys.reduce((result, key) => ({ ...result, [key]: object[key] }), {});
};
//Example
const person = {firstname: 'abd', lastname:'tm', age:16, school:'insat'};
// Expected to pick only firstname and age keys
console.log(
filter(person, 'firstname', 'age')
)
Plain ES6:
var foo = {
bar: "Yes"
};
const res = Object.keys(foo).filter(i => foo[i] === 'Yes')
console.log(res)
// ["bar"]
How about:
function filterObj(keys, obj) {
const newObj = {};
for (let key in obj) {
if (keys.includes(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
Or...
function filterObj(keys, obj) {
const newObj = {};
Object.keys(obj).forEach(key => {
if (keys.includes(key)) {
newObj[key] = obj[key];
}
});
return newObj;
}
I have created an Object.filter() which does not only filter by a function, but also accepts an array of keys to include. The optional third parameter will allow you to invert the filter.
Given:
var foo = {
x: 1,
y: 0,
z: -1,
a: 'Hello',
b: 'World'
}
Array:
Object.filter(foo, ['z', 'a', 'b'], true);
Function:
Object.filter(foo, function (key, value) {
return Ext.isString(value);
});
Code
Disclaimer: I chose to use Ext JS core for brevity. Did not feel it was necessary to write type checkers for object types as it was not part of the question.
// Helper function
function print(obj) {
document.getElementById('disp').innerHTML += JSON.stringify(obj, undefined, ' ') + '<br />';
console.log(obj);
}
Object.filter = function (obj, ignore, invert) {
let result = {}; // Returns a filtered copy of the original list
if (ignore === undefined) {
return obj;
}
invert = invert || false;
let not = function(condition, yes) { return yes ? !condition : condition; };
let isArray = Ext.isArray(ignore);
for (var key in obj) {
if (obj.hasOwnProperty(key) &&
!(isArray && not(!Ext.Array.contains(ignore, key), invert)) &&
!(!isArray && not(!ignore.call(undefined, key, obj[key]), invert))) {
result[key] = obj[key];
}
}
return result;
};
let foo = {
x: 1,
y: 0,
z: -1,
a: 'Hello',
b: 'World'
};
print(Object.filter(foo, ['z', 'a', 'b'], true));
print(Object.filter(foo, (key, value) => Ext.isString(value)));
#disp {
white-space: pre;
font-family: monospace
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/extjs/4.2.1/builds/ext-core.min.js"></script>
<div id="disp"></div>
My opinionated solution:
function objFilter(obj, filter, nonstrict){
r = {}
if (!filter) return {}
if (typeof filter == 'string') return {[filter]: obj[filter]}
for (p in obj) {
if (typeof filter == 'object' && nonstrict && obj[p] == filter[p]) r[p] = obj[p]
else if (typeof filter == 'object' && !nonstrict && obj[p] === filter[p]) r[p] = obj[p]
else if (typeof filter == 'function'){ if (filter(obj[p],p,obj)) r[p] = obj[p]}
else if (filter.length && filter.includes(p)) r[p] = obj[p]
}
return r
}
Test cases:
obj = {a:1, b:2, c:3}
objFilter(obj, 'a') // returns: {a: 1}
objFilter(obj, ['a','b']) // returns: {a: 1, b: 2}
objFilter(obj, {a:1}) // returns: {a: 1}
objFilter(obj, {'a':'1'}, true) // returns: {a: 1}
objFilter(obj, (v,k,o) => v%2===1) // returns: {a: 1, c: 3}
https://gist.github.com/bernardoadc/872d5a174108823159d845cc5baba337
var foo = {
bar: "Yes",
pipe: "No"
};
const ret = Object.entries(foo).filter(([key, value])=> value === 'Yes');
https://masteringjs.io/tutorials/fundamentals/filter-object
If you have Symbol properties in your object, that should be filtered too, you can not use: Object.keys Object.entries Object.fromEntries, ... because:
Symbol keys are not enumerable !
You could use Reflect.ownKeys and filter keys in reduce
Reflect.ownKeys(o).reduce((a, k) => allow.includes(k) && {...a, [k]: o[k]} || a, {});
(Open DevTools for log output - Symbols are not logged on Stackoverflow UI)
const bKey = Symbol('b_k');
const o = {
a: 1,
[bKey]: 'b',
c: [1, 3],
[Symbol.for('d')]: 'd'
};
const allow = ['a', bKey, Symbol.for('d')];
const z1 = Reflect.ownKeys(o).reduce((a, k) => allow.includes(k) && {...a, [k]: o[k]} || a, {});
console.log(z1); // {a: 1, Symbol(b_k): "b", Symbol(d): "d"}
console.log(bKey in z1) // true
console.log(Symbol.for('d') in z1) // true
This is equal to this
const z2 = Reflect.ownKeys(o).reduce((a, k) => allow.includes(k) && Object.assign(a, {[k]: o[k]}) || a, {});
const z3 = Reflect.ownKeys(o).reduce((a, k) => allow.includes(k) && Object.defineProperty(a, k, {value: o[k]}) || a, {});
console.log(z2); // {a: 1, Symbol(b_k): "b", Symbol(d): "d"}
console.log(z3); // {a: 1, Symbol(b_k): "b", Symbol(d): "d"}
Wrapped in a filter() function, an optional target object could be passed
const filter = (o, allow, t = {}) => Reflect.ownKeys(o).reduce(
(a, k) => allow.includes(k) && {...a, [k]: o[k]} || a,
t
);
console.log(filter(o, allow)); // {a: 1, Symbol(b_k): "b", Symbol(d): "d"}
console.log(filter(o, allow, {e: 'e'})); // {a: 1, e: "e", Symbol(b_k): "b", Symbol(d): "d"}
You could also do something like this where you are filtering on the entries to find the key provided and return the value
let func = function(items){
let val
Object.entries(this.items).map(k => {
if(k[0]===kind){
val = k[1]
}
})
return val
}
If you wish to mutate the same object rather than create a new one.
The following example will delete all 0 or empty values:
const sev = { a: 1, b: 0, c: 3 };
const deleteKeysBy = (obj, predicate) =>
Object.keys(obj)
.forEach( (key) => {
if (predicate(obj[key])) {
delete(obj[key]);
}
});
deleteKeysBy(sev, val => !val);
Like everyone said, do not screw around with prototype. Instead, simply write a function to do so. Here is my version with lodash:
import each from 'lodash/each';
import get from 'lodash/get';
const myFilteredResults = results => {
const filteredResults = [];
each(results, obj => {
// filter by whatever logic you want.
// sample example
const someBoolean = get(obj, 'some_boolean', '');
if (someBoolean) {
filteredResults.push(obj);
}
});
return filteredResults;
};
If you don't need the original object, this is a simple, very boring answer that doesn't waste memory:
const obj = {'a': 'want this', 'b': 'want this too', 'x': 'remove this'}
const keep = new Set(['a', 'b', 'c'])
function filterObject(obj, keep) {
Object.keys(obj).forEach(key => {
if (!keep.has(key)) {
delete obj[key]
}
})
}
If you're only filtering a small number of objects, and your objects don't have many keys, you might not want to bother with constructing a Set, in which case use array.includes instead of set.has.
I just wanted to add the way that I do it because it saves me creating extra functions, I think is cleaner and I didn't see this answer:
let object = {a: 1, b: 2, c: 3};
[object].map(({a,c}) => ({a,c}))[0]; // {a:1, c:2}
The cool thing is that also works on arrays of objects:
let object2 = {a: 4, b: 5, c: 6, d: 7};
[object, object2].map(({a,b,c,d}) => ({a,c})); //[{"a":1,"c":3},{"a":4,"c":6}]
[object, object2].map(({a,d}) => ({a,d})); //[{"a":1,"d":undefined},{"a":4,"d":7}]
In these cases I use the jquery $.map, which can handle objects. As mentioned on other answers, it's not a good practice to change native prototypes (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain#Bad_practice_Extension_of_native_prototypes)
Below is an example of filtering just by checking some property of your object. It returns the own object if your condition is true or returns undefined if not. The undefined property will make that record disappear from your object list;
$.map(yourObject, (el, index)=>{
return el.yourProperty ? el : undefined;
});

Categories