Related
I have the following object:
const modules = {
celebrity: {
actor: {
male: 'male',
female: 'female'
},
director: 'director'
},
movie: 'movie',
user: 'user'
};
In result I want an array of string as the following:
[
"celebrity/actor/male",
"celebrity/actor/female",
"celebrity/director",
"movie",
"user"
]
I create the following function:
function getPathsList(node, path = '') {
let pathsList = [];
const childs = Object.entries(node);
for (const child of childs) {
if (typeof child[1] === 'string') {
path += `/${child[1]}`
pathsList.push(path)
} else {
path += `/${child[0]}`
pathsList = [...pathsList, ...getPathsList(child[1], path, pathsList)]
}
}
return pathsList;
}
But I got:
[
"/celebrity/actor/male",
"/celebrity/actor/male/female",
"/celebrity/actor/director",
"/celebrity/movie",
"/celebrity/movie/user"
]
I know that the path variable should be initialized somewhere, but I can't figure it out.
You could use an appraoch which works without a path, but assembles the path by iterating the nested part object.
function getPathsList(node) {
const pathsList = [];
for (const [key, value] of Object.entries(node)) {
if (value && typeof value === 'object') {
pathsList.push(...getPathsList(value).map(p => `${key}/${p}`))
} else {
pathsList.push(key);
}
}
return pathsList;
}
const modules = {
celebrity: {
actor: {
male: 'male',
female: 'female'
},
director: 'director'
},
movie: 'movie',
user: 'user'
};
console.log(getPathsList(modules));
Another way, using reduce:
const modules = {celebrity:{actor:{male:"male",female:"female"},director:"director"},movie:"movie",user:"user"};
function getPathsList(node, path = '') {
return Object.entries(node)
.reduce(
(res, [k, v]) => res.concat(
typeof v === "string" ? `${path}${v}` : getPathsList(v, `${path}${k}/`
)
), []);
}
console.log(getPathsList(modules));
You may consider a "dfs" like algorithm where you explore every path from root to leaf.
You then join your path with '/'.
Subtlety: don't put the leaf itself into the path (e.g: otherwise you would get movie/movie)
Below an example using flatMap
const modules = {"celebrity":{"actor":{"male":"male","female":"female"},"director":"director"},"movie":"movie","user":"user"}
const flatten = x => {
if (typeof(x) === 'string') { return [[]] }
// each node has for children its entries
// each node returns an array of path
return Object.entries(x).flatMap(([k, v]) => {
return flatten(v).map(path => [k , ...path])
})
}
console.log(flatten(modules).map(path => path.join('/')))
Where is the difficulty ?
const
modules =
{ celebrity:
{ actor: { male: 'male', female: 'female' }
, director: 'director'
}
, movie: 'movie'
, user: 'user'
}
, pathsList = []
;
function getPathsList( obj, path='' )
{
for (let key in obj )
{
if (typeof(obj[key]) === 'object') getPathsList( obj[key], path+'/'+key )
else pathsList.push( (path+'/'+key).substring(1) )
}
}
getPathsList( modules )
console.log( pathsList )
A very simple recursive solution using Object .entries returns an array containing only an empty string for non-objects, and otherwise, for every key-value, combines the key with the results of a recursive call to the value. The only slightly tricky part is to not insert the slash (/) before an empty string. It looks like this:
const getPathsList = (obj) => Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, v]) => getPathsList (v) .map (p => p ? k + '/' + p : k)
)
: ['']
const modules = {celebrity: {actor: {male: 'male', female: 'female'}, director: 'director'}, movie: 'movie', user: 'user'}
console .log (getPathsList (modules))
But I would prefer to do this a slightly different way, building our function atop one that gathers the result into arrays of values (e.g. [['celebrity', 'actor', 'male'], ['celebrity', 'actor', 'female'], ... ['user']]), then simply joining those new arrays together with slashes. It's quite similar:
const getPaths = (obj) => Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, v]) => getPaths (v) .map (p => [k, ...p])
)
: [[]]
const getPathsList = (obj) =>
getPaths (obj) .map (xs => xs .join ('/'))
const modules = {celebrity: {actor: {male: 'male', female: 'female'}, director: 'director'}, movie: 'movie', user: 'user'}
console .log (getPathsList (modules))
I find that intermediate array format much more helpful.
This is a slightly less sophisticated version of getPaths than I generally write. Usually I distinguish between numeric array indices and string object keys, but that's not relevant here, since we're folding them back into strings, so this version is simplified.
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.
this is my object:
{
"name":"fff",
"onlineConsultation":false,
"image":"",
"primaryLocation":{
"locationName":"ggg",
"street":"",
},
"billingAndInsurance":[
],
"categories":[
""
],
"concernsTreated":[
""
],
"education":[
{
"nameOfInstitution":"ffff",
"description":"fff",
}
],
"experience":[
{
"from":"",
"current":"",
}
],
}
What is the algorithm to recursively remove all empty objects, and empty arrays from this?
this is my code:
function rm(obj) {
for (let k in obj) {
const s = JSON.stringify(obj[k]);
if (s === '""' || s === "[]" || s === "{}") {
delete obj[k];
}
if (Array.isArray(obj[k])) {
obj[k] = obj[k].filter((x) => {
const s = JSON.stringify(obj[x]);
return s !== '""' && s !== "[]" && s !== "{}";
});
obj[k] = obj[k].map(x=>{
return rm(x)
})
}
}
return obj
}
I'v tried multiple algorithms, but none worked. the one above should work with a little more completeness. But I'v exhausted all my resources to make it work
One nice thing about keeping around helpful functions is that you can often solve for your new requirements pretty simply. Using some library functions I've written over the years, I was able to write this version:
const removeEmpties = (input) =>
pathEntries (input)
.filter (([k, v]) => v !== '')
.reduce ((a, [k, v]) => assocPath (k, v, a), {})
This uses two function I had around, pathEntries and assocPath, and I'll give their implementations below. It returns the following when given the input you supplied:
{
name: "fff",
onlineConsultation: false,
primaryLocation: {
locationName: "ggg"
},
education: [
{
nameOfInstitution: "ffff",
description: "fff"
}
]
}
This removes empty string, arrays with no values (after the empty strings are removed) and objects with no non-empty values.
We begin by calling pathEntries (which I've used in other answers here, including a fairly recent one.) This collects paths to all the leaf nodes in the input object, along with the values at those leaves. The paths are stored as arrays of strings (for objects) or numbers (for arrays.) And they are embedded in an array with the value. So after that step we get something like
[
[["name"], "fff"],
[["onlineConsultation"], false],
[["image"], ""],
[["primaryLocation", "locationName"], "ggg"],
[["primaryLocation", "street"], ""],
[["categories", 0], ""],
[["concernsTreated", 0], ""],
[["education", 0, "nameOfInstitution"], "ffff"],
[["education", 0, "description"],"fff"],
[["experience", 0, "from"], ""],
[["experience", 0, "current"], ""]
]
This should looks something like the result of Object.entries for an object, except that the key is not a property name but an entire path.
Next we filter to remove any with an empty string value, yielding:
[
[["name"], "fff"],
[["onlineConsultation"], false],
[["primaryLocation", "locationName"], "ggg"],
[["education", 0, "nameOfInstitution"], "ffff"],
[["education", 0, "description"],"fff"],
]
Then by reducing calls to assocPath (another function I've used quite a few times, including in a very interesting question) over this list and an empty object, we hydrate a complete object with just these leaf nodes at their correct paths, and we get the answer we're seeking. assocPath is an extension of another function assoc, which immutably associates a property name with a value in an object. While it's not as simple as this, due to handling of arrays as well as objects, you can think of assoc like (name, val, obj) => ({...obj, [name]: val}) assocPath does something similar for object paths instead of property names.
The point is that I wrote only one new function for this, and otherwise used things I had around.
Often I would prefer to write a recursive function for this, and I did so recently for a similar problem. But that wasn't easily extensible to this issue, where, if I understand correctly, we want to exclude an empty string in an array, and then, if that array itself is now empty, to also exclude it. This technique makes that straightforward. In the implementation below we'll see that pathEntries depends upon a recursive function, and assocPath is itself recursive, so I guess there's still recursion going on!
I also should note that assocPath and the path function used in pathEntries are inspired by Ramda (disclaimer: I'm one of it's authors.) I built my first pass at this in the Ramda REPL and only after it was working did I port it to vanilla JS, using the versions of dependencies I've created for those previous questions. So even though there are a number of functions in the snippet below, it was quite quick to write.
const path = (ps = []) => (obj = {}) =>
ps .reduce ((o, p) => (o || {}) [p], obj)
const assoc = (prop, val, obj) =>
Number .isInteger (prop) && Array .isArray (obj)
? [... obj .slice (0, prop), val, ...obj .slice (prop + 1)]
: {...obj, [prop]: val}
const assocPath = ([p = undefined, ...ps], val, obj) =>
p == undefined
? obj
: ps.length == 0
? assoc(p, val, obj)
: assoc(p, assocPath(ps, val, obj[p] || (obj[p] = Number .isInteger (ps[0]) ? [] : {})), obj)
const getPaths = (obj) =>
Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, v]) => getPaths (v) .map (p => [Array.isArray(obj) ? Number(k) : k, ... p])
)
: [[]]
const pathEntries = (obj) =>
getPaths (obj) .map (p => [p, path (p) (obj)])
const removeEmpties = (input) =>
pathEntries (input)
.filter (([k, v]) => v !== '')
.reduce ((a, [k, v]) => assocPath (k, v, a), {})
const input = {name: "fff", onlineConsultation: false, image: "", primaryLocation: {locationName: "ggg", street:""}, billingAndInsurance: [], categories: [""], concernsTreated: [""], education: [{nameOfInstitution: "ffff", description: "fff"}], experience: [{from: "", current:""}]}
console .log(removeEmpties (input))
At some point, I may choose to go a little further. I see a hydrate function looking to be pulled out:
const hydrate = (entries) =>
entries .reduce ((a, [k, v]) => assocPath2(k, v, a), {})
const removeEmpties = (input) =>
hydrate (pathEntries (input) .filter (([k, v]) => v !== ''))
And I can also see this being written more Ramda-style like this:
const hydrate = reduce ((a, [k, v]) => assocPath(k, v, a), {})
const removeEmpties = pipe (pathEntries, filter(valueNotEmpty), hydrate)
with an appropriate version of valuesNotEmpty.
But all that is for another day.
It's an interesting problem. I think it can be solved elegantly if we write a generic map and filter function that works on both Arrays and Objects -
const map = (t, f) =>
isArray(t)
? t.map(f)
: isObject(t)
? Object.fromEntries(Object.entries(t).map(([k, v]) => [k, f(v, k)]))
: t
const filter = (t, f) =>
isArray(t)
? t.filter(f)
: isObject(t)
? Object.fromEntries(Object.entries(t).filter(([k, v]) => f(v, k)))
: t
We can write your removeEmpties program easily now -
if the input, t, is an object, recursively map over it and keep the non-empty values
(inductive) t is not an object. If t is a non-empty value, return t
(inductive) t is not an object and t is an empty value. Return the empty sentinel
const empty =
Symbol()
const removeEmpties = (t = {}) =>
isObject(t)
? filter(map(t, removeEmpties), nonEmpty) // 1
: nonEmpty(t)
? t // 2
: empty // 3
Now we have to define what it means to be nonEmpty -
const nonEmpty = t =>
isArray(t)
? t.length > 0
: isObject(t)
? Object.keys(t).length > 0
: isString(t)
? t.length > 0
: t !== empty // <- all other t are OK, except for sentinel
To this point we have use is* functions to do dynamic type-checking. We will define those now -
const isArray = t => Array.isArray(t)
const isObject = t => Object(t) === t
const isString = t => String(t) === t
const isNumber = t => Number(t) === t
const isMyType = t => // As many types as you want
Finally we can compute the result of your input -
const input =
{name:"fff",zero:0,onlineConsultation:false,image:"",primaryLocation:{locationName:"ggg",street:""},billingAndInsurance:[],categories:[""],concernsTreated:[""],education:[{nameOfInstitution:"ffff",description:"fff"}],experience:[{from:"",current:""}]}
const result =
removeEmpties(input)
console.log(JSON.stringify(result, null, 2))
{
"name": "fff",
"zero": 0,
"onlineConsultation": false,
"primaryLocation": {
"locationName": "ggg"
},
"education": [
{
"nameOfInstitution": "ffff",
"description": "fff"
}
]
}
Expand the program below to verify the result in your browser -
const map = (t, f) =>
isArray(t)
? t.map(f)
: isObject(t)
? Object.fromEntries(Object.entries(t).map(([k, v]) => [k, f(v, k)]))
: t
const filter = (t, f) =>
isArray(t)
? t.filter(f)
: isObject(t)
? Object.fromEntries(Object.entries(t).filter(([k, v]) => f(v, k)))
: t
const empty =
Symbol()
const removeEmpties = (t = {}) =>
isObject(t)
? filter(map(t, removeEmpties), nonEmpty)
: nonEmpty(t)
? t
: empty
const isArray = t => Array.isArray(t)
const isObject = t => Object(t) === t
const isString = t => String(t) === t
const nonEmpty = t =>
isArray(t)
? t.length > 0
: isObject(t)
? Object.keys(t).length > 0
: isString(t)
? t.length > 0
: t !== empty
const input =
{name:"fff",zero:0,onlineConsultation:false,image:"",primaryLocation:{locationName:"ggg",street:""},billingAndInsurance:[],categories:[""],concernsTreated:[""],education:[{nameOfInstitution:"ffff",description:"fff"}],experience:[{from:"",current:""}]}
const result =
removeEmpties(input)
console.log(JSON.stringify(result, null, 2))
function removeEmpty(obj){
if(obj.__proto__.constructor.name==="Array"){
obj = obj.filter(e=>e.length)
return obj.map((ele,i)=>{
if(obj.__proto__.constructor.name==="Object")return removeEmpty(ele) /* calling the same function*/
else return ele
})
}
if(obj.__proto__.constructor.name==="Object")for(let key in obj){
switch(obj[key].__proto__.constructor.name){
case "String":
if(obj[key].length===0)delete obj[key]
break;
case "Array":
obj[key] = removeEmpty(obj[key]) /* calling the same function*/
if(! obj[key].length)delete obj[key]
break;
case "Object":
obj[key] = removeEmpty(obj[key]) /* calling the same function*/
break;
}
}
return obj;
}
const input = {name: "fff", onlineConsultation: false, image: "", primaryLocation: {locationName: "ggg", street:""}, billingAndInsurance: [], categories: [""], concernsTreated: [""], education: [{nameOfInstitution: "ffff", description: "fff"}], experience: [{from: "", current:""}]}
console .log(removeEmpty(input))
function dropEmptyElements(object) {
switch (typeof object) {
case "object":
const keys = Object.keys(object || {}).filter(key => {
const value = dropEmptyElements(object[key]);
if(value === undefined) {
delete object[key];
}
return value !== undefined;
});
return keys.length === 0 ? undefined : object;
case "string":
return object.length > 0 ? object : undefined;
default:
return object;
}
}
This should do the trick for you ;)
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);
I have a Javascript array that I would like to split into two based on whether a function called on each element returns true or false. Essentially, this is an array.filter, but I'd like to also have on hand the elements that were filtered out.
Currently, my plan is to use array.forEach and call the predicate function on each element. Depending on whether this is true or false, I will push the current element onto one of the two new arrays. Is there a more elegant or otherwise better way to do this? An array.filter where the will push the element onto another array before it returns false, for instance?
With ES6 you can make use of the spread syntax with reduce:
function partition(array, isValid) {
return array.reduce(([pass, fail], elem) => {
return isValid(elem) ? [[...pass, elem], fail] : [pass, [...fail, elem]];
}, [[], []]);
}
const [pass, fail] = partition(myArray, (e) => e > 5);
Or on a single line:
const [pass, fail] = a.reduce(([p, f], e) => (e > 5 ? [[...p, e], f] : [p, [...f, e]]), [[], []]);
You can use lodash.partition
var users = [
{ 'user': 'barney', 'age': 36, 'active': false },
{ 'user': 'fred', 'age': 40, 'active': true },
{ 'user': 'pebbles', 'age': 1, 'active': false }
];
_.partition(users, function(o) { return o.active; });
// → objects for [['fred'], ['barney', 'pebbles']]
// The `_.matches` iteratee shorthand.
_.partition(users, { 'age': 1, 'active': false });
// → objects for [['pebbles'], ['barney', 'fred']]
// The `_.matchesProperty` iteratee shorthand.
_.partition(users, ['active', false]);
// → objects for [['barney', 'pebbles'], ['fred']]
// The `_.property` iteratee shorthand.
_.partition(users, 'active');
// → objects for [['fred'], ['barney', 'pebbles']]
or ramda.partition
R.partition(R.contains('s'), ['sss', 'ttt', 'foo', 'bars']);
// => [ [ 'sss', 'bars' ], [ 'ttt', 'foo' ] ]
R.partition(R.contains('s'), { a: 'sss', b: 'ttt', foo: 'bars' });
// => [ { a: 'sss', foo: 'bars' }, { b: 'ttt' } ]
I came up with this little guy. It uses for each and all that like you described, but it looks clean and succinct in my opinion.
//Partition function
function partition(array, filter) {
let pass = [], fail = [];
array.forEach((e, idx, arr) => (filter(e, idx, arr) ? pass : fail).push(e));
return [pass, fail];
}
//Run it with some dummy data and filter
const [lessThan5, greaterThanEqual5] = partition([0,1,4,3,5,7,9,2,4,6,8,9,0,1,2,4,6], e => e < 5);
//Output
console.log(lessThan5);
console.log(greaterThanEqual5);
You can use reduce for it:
function partition(array, callback){
return array.reduce(function(result, element, i) {
callback(element, i, array)
? result[0].push(element)
: result[1].push(element);
return result;
}, [[],[]]
);
};
Or if using Typescript:
const partition = <T,>(
array: T[],
callback: (element: T, index: number, array: T[]) => boolean
) => {
return array.reduce(function(result, element, i) {
callback(element, i, array)
? result[0].push(element)
: result[1].push(element);
return result;
}, [[],[]]);
};
Example:
const groceries = [
{ type: "apple" },
{ type: "pear" },
{ type: "banana" }
]
const [apples, others] = partition(
groceries,
(item) => item.type === "apple",
);
// => apples: [{ type: "apple" }]
// => others: [{ type: "pear" }, { type: "banana" }]
Using ES6 syntax you also can do that using recursion (updated to avoid creating new arrays on every iteration):
function partition([current, ...tail], f, left = [], right = []) {
if(current === undefined) {
return [left, right];
}
if(f(current)) {
left.push(current);
return partition(tail, f, left, right);
}
right.push(current);
return partition(tail, f, left, right);
}
This sounds very similar to Ruby's Enumerable#partition method.
If the function can't have side-effects (i.e., it can't alter the original array), then there's no more efficient way to partition the array than iterating over each element and pushing the element to one of your two arrays.
That being said, it's arguably more "elegant" to create a method on Array to perform this function. In this example, the filter function is executed in the context of the original array (i.e., this will be the original array), and it receives the element and the index of the element as arguments (similar to jQuery's each method):
Array.prototype.partition = function (f){
var matched = [],
unmatched = [],
i = 0,
j = this.length;
for (; i < j; i++){
(f.call(this, this[i], i) ? matched : unmatched).push(this[i]);
}
return [matched, unmatched];
};
console.log([1, 2, 3, 4, 5].partition(function (n, i){
return n % 2 == 0;
}));
//=> [ [ 2, 4 ], [ 1, 3, 5 ] ]
In filter function you can push your false items into another variable outside function:
var bad = [], good = [1,2,3,4,5];
good = good.filter(function (value) { if (value === false) { bad.push(value) } else { return true});
Of course value === false need to be real comparasion ;)
But it do almost that same operation like forEach. I think you should use forEach for better code readability.
A lot of answers here use Array.prototype.reduce to build a mutable accumulator, and rightfully point out that for large arrays, this is more efficient than, say, using a spread operator to copy a new array each iteration. The downside is that it's not as pretty as a "pure" expression using the short lambda syntax.
But a way around that is to use the comma operator. In C-like languages, comma is an operator that always returns the right hand operand. You can use this to create an expression that calls a void function and returns a value.
function partition(array, predicate) {
return array.reduce((acc, item) => predicate(item)
? (acc[0].push(item), acc)
: (acc[1].push(item), acc), [[], []]);
}
If you take advantage of the fact that a boolean expression implicitly casts to a number as 0 and 1, and you can make it even more concise, although I don't think it's as readable:
function partition(array, predicate) {
return array.reduce((acc, item) => (acc[+!predicate(item)].push(item), acc), [[], []]);
}
Usage:
const [trues, falses] = partition(['aardvark', 'cat', 'apple'], i => i.startsWith('a'));
console.log(trues); // ['aardvark', 'apple']
console.log(falses); // ['cat']
What about this?
[1,4,3,5,3,2].reduce( (s, x) => { s[ x > 3 ].push(x); return s;} , {true: [], false:[]} )
Probably this is more efficient than the spread operator
Or a bit shorter, but uglier
[1,4,3,5,3,2].reduce( (s, x) => s[ x > 3 ].push(x)?s:s , {true: [], false:[]} )
Try this:
function filter(a, fun) {
var ret = { good: [], bad: [] };
for (var i = 0; i < a.length; i++)
if (fun(a[i])
ret.good.push(a[i]);
else
ret.bad.push(a[i]);
return ret;
}
DEMO
Easy to read one.
const partition = (arr, condition) => {
const trues = arr.filter(el => condition(el));
const falses = arr.filter(el => !condition(el));
return [trues, falses];
};
// sample usage
const nums = [1,2,3,4,5,6,7]
const [evens, odds] = partition(nums, (el) => el%2 == 0)
I ended up doing this because it's easy to understand:
const partition = (array, isValid) => {
const pass = []
const fail = []
array.forEach(element => {
if (isValid(element)) {
pass.push(element)
} else {
fail.push(element)
}
})
return [pass, fail]
}
// usage
const [pass, fail] = partition([1, 2, 3, 4, 5], (element) => element > 3)
And the same method including types for typescript:
const partition = <T>(array: T[], isValid: (element: T) => boolean): [T[], T[]] => {
const pass: T[] = []
const fail: T[] = []
array.forEach(element => {
if (isValid(element)) {
pass.push(element)
} else {
fail.push(element)
}
})
return [pass, fail]
}
// usage
const [pass, fail] = partition([1, 2, 3, 4, 5], (element: number) => element > 3)
ONE-LINER Partition
const partitionBy = (arr, predicate) =>
arr.reduce((acc, item) => (acc[+!predicate(item)].push(item), acc), [[], []]);
DEMO
// to make it consistent to filter pass index and array as arguments
const partitionBy = (arr, predicate) =>
arr.reduce(
(acc, item, index, array) => (
acc[+!predicate(item, index, array)].push(item), acc
),
[[], []]
);
console.log(partitionBy([1, 2, 3, 4, 5], x => x % 2 === 0));
console.log(partitionBy([..."ABCD"], (x, i) => i % 2 === 0));
For Typescript (v4.5)
const partitionBy = <T>(
arr: T[],
predicate: (v: T, i: number, ar: T[]) => boolean
) =>
arr.reduce(
(acc, item, index, array) => {
acc[+!predicate(item, index, array)].push(item);
return acc;
},
[[], []] as [T[], T[]]
);
Lodash partition alternative, same as the first solution of #Yaremenko Andrii but shorter syntax
function partition(arr, callback) {
return arr.reduce(
(acc, val, i, arr) => {
acc[callback(val, i, arr) ? 0 : 1].push(val)
return acc
},
[[], []]
)
}
I know there are multiple solutions already but I took the liberty of putting together the best bits of the answers above and used extension methods on Typescript. Copy and paste and it just works:
declare global {
interface Array<T> {
partition(this: T[], predicate: (e: T) => boolean): T[][];
}
}
if(!Array.prototype.partition){
Array.prototype.partition = function<T>(this: T[], predicate: (e: T) => boolean): T[][] {
return this.reduce<T[][]>(([pass, fail], elem) => {
(predicate(elem) ? pass : fail).push(elem);
return [pass, fail];
}, [[], []]);
}
}
Usage:
const numbers = [1, 2, 3, 4, 5, 6];
const [even, odd] = numbers.partition(n => n % 2 === 0);