I'm trying to sort an array of nested objects. It's working with a static chosen key but I can't figure out how to get it dynamically.
So far I've got this code
sortBy = (isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = (((a || {})['general'] || {})['fileID']) || '';
const valueB = (((b || {})['general'] || {})['fileID']) || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
At this point the keys are hardcoded ['general']['orderID'] but I want this part to be dynamic by adding a keys param to the sortBy function:
sortBy = (keys, isReverse=false) => { ...
keys is an array with the nested keys. For the above example, it will be ['general', 'fileID'].
What are the steps that need to be taken to make this dynamic?
Note: child objects can be undefined therefore I'm using a || {}
Note 2: I'm using es6. No external packages.
The currently accepted answer apart from putting bugs in your code is not doing much to help you. Use of a simple function deepProp would mitigate the painful repetition -
const deepProp = (o = {}, props = []) =>
props.reduce((acc = {}, p) => acc[p], o)
Now without so much noise -
sortBy = (keys, isReverse = false) =>
this.setState ({
files: // without mutating the previous state!
[...this.state.files].sort((a,b) => {
const valueA = deepProp(a, keys) || ''
const valueB = deepProp(b, keys) || ''
return isReverse
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA)
})
})
Still, this does little in terms of actually improving your program. It's riddled with complexity, and worse, this complexity will be duplicated in any component that requires similar functionality. React embraces functional style so this answer approaches the problem from a functional standpoint. In this post, we'll write sortBy as -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
Your question poses us to learn two powerful functional concepts; we'll use these to answer the question -
Monads
Contravariant Functors
Let's not get overwhelmed by terms though and instead focus on gaining an intuition for how things work. At first, it looks like we have a problem checking for nulls. Having to deal with the possibility that some of our inputs may not have the nested properties makes our function messy. If we can generalize this concept of a possible value, we can clean things up a bit.
Your question specifically says you are not using an external package right now, but this is a good time to reach for one. Let's take a brief look at the data.maybe package -
A structure for values that may not be present, or computations that may fail. Maybe(a) explicitly models the effects that implicit in Nullable types, thus has none of the problems associated with using null or undefined — like NullPointerException or TypeError.
Sounds like a good fit. We'll start by writing a function safeProp that accepts an object and a property string as input. Intuitively, safeProp safely returns the property p of object o -
const { Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
// if o is an object
Object (o) === o
// access property p on object o, wrapping the result in a Maybe
? fromNullable (o[p])
// otherwise o is not an object, return Nothing
: Nothing ()
Instead of simply returning o[p] which could be a null or undefined value, we'll get back a Maybe that guides us in handling the result -
const generalFileId = (o = {}) =>
// access the general property
safeProp (o, 'general')
// if it exists, access the fileId property on the child
.chain (child => safeProp (child, 'fileId'))
// get the result if valid, otherwise return empty string
.getOrElse ('')
Now we have a function which can take objects of varying complexity and guarantees the result we're interested in -
console .log
( generalFileId ({ general: { fileId: 'a' } }) // 'a'
, generalFileId ({ general: { fileId: 'b' } }) // 'b'
, generalFileId ({ general: 'x' }) // ''
, generalFileId ({ a: 'x '}) // ''
, generalFileId ({ general: { err: 'x' } }) // ''
, generalFileId ({}) // ''
)
That's half the battle right there. We can now go from our complex object to the precise string value we want to use for comparison purposes.
I'm intentionally avoiding showing you an implementation of Maybe here because this in itself is a valuable lesson. When a module promises capability X, we assume we have capability X, and ignore what happens in the black box of the module. The very point of data abstraction is to hide concerns away so the programmer can think about things at a higher level.
It might help to ask how does Array work? How does it calculate or adjust the length property when an element is added or removed from the array? How does the map or filter function produce a new array? If you never wondered these things before, that's okay! Array is a convenient module because it removes these concerns from the programmer's mind; it just works as advertised.
This applies regardless of whether the module is provided by JavaScript, by a third party such as from npm, or if you wrote the module yourself. If Array didn't exist, we could implement it as our own data structure with equivalent conveniences. Users of our module gain useful functionalities without introducing additional complexity. The a-ha moment comes when you realize that the programmer is his/her own user: when you run into a tough problem, write a module to free yourself from the shackles of complexity. Invent your own convenience!
We'll show a basic implementation of Maybe later in the answer, but for now, we just have to finish the sort ...
We start with two basic comparators, asc for ascending sort, and desc for descending sort -
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
In React, we cannot mutate the previous state, instead, we must create new state. So to sort immutably, we must implement isort which will not mutate the input object -
const isort = (compare = asc, xs = []) =>
xs
.slice (0) // clone
.sort (compare) // then sort
And of course a and b are sometimes complex objects, so case we can't directly call asc or desc. Below, contramap will transform our data using one function g, before passing the data to the other function, f -
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId) // ascending comparator
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
Using the other comparator desc, we can see sorting work in the other direction -
isort
( contramap (desc, generalFileId) // descending comparator
, files
)
// [ { general: { fileId: 'e' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'a' } }
// ]
Now to write the method for your React component, sortBy. The method is essentially reduced to this.setState({ files: t (this.state.files) }) where t is an immutable transformation of your program's state. This is good because complexity is kept out of your components where testing is difficult, and instead it resides in generic modules, which are easy to test -
sortBy = (reverse = true) =>
this.setState
( { files:
isort
( contramap
( reverse ? desc : asc
, generalFileId
)
, this.state.files
)
}
)
This uses the boolean switch like in your original question, but since React embraces functional pattern, I think it would be even better as a higher-order function -
sortBy = (comparator = asc) =>
this.setState
( { files:
isort
( contramap
( comparator
, generalFileId
)
, this.state.files
)
}
)
If the nested property you need to access is not guaranteed to be general and fileId, we can make a generic function which accepts a list of properties and can lookup a nested property of any depth -
const deepProp = (o = {}, props = []) =>
props .reduce
( (acc, p) => // for each p, safely lookup p on child
acc .chain (child => safeProp (child, p))
, fromNullable (o) // init with Maybe o
)
const generalFileId = (o = {}) =>
deepProp (o, [ 'general', 'fileId' ]) // using deepProp
.getOrElse ('')
const fooBarQux = (o = {}) =>
deepProp (o, [ 'foo', 'bar', 'qux' ]) // any number of nested props
.getOrElse (0) // customizable default
console.log
( generalFileId ({ general: { fileId: 'a' } } ) // 'a'
, generalFileId ({}) // ''
, fooBarQux ({ foo: { bar: { qux: 1 } } } ) // 1
, fooBarQux ({ foo: { bar: 2 } }) // 0
, fooBarQux ({}) // 0
)
Above, we use the data.maybe package which provides us with the capability to work with potential values. The module exports functions to convert ordinary values to a Maybe, and vice versa, as well as many useful operations that are applicable to potential values. There's nothing forcing you to use this particular implementation, however. The concept is simple enough that you could implement fromNullable, Just and Nothing in a couple dozen lines, which we'll see later in this answer -
Run the complete demo below on repl.it
const { Just, Nothing, fromNullable } =
require ('data.maybe')
const safeProp = (o = {}, p = '') =>
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const generalFileId = (o = {}) =>
safeProp (o, 'general')
.chain (child => safeProp (child, 'fileId'))
.getOrElse ('')
// ----------------------------------------------
const asc = (a, b) =>
a .localeCompare (b)
const desc = (a, b) =>
asc (a, b) * -1
const contramap = (f, g) =>
(a, b) => f (g (a), g (b))
const isort = (compare = asc, xs = []) =>
xs
.slice (0)
.sort (compare)
// ----------------------------------------------
const files =
[ { general: { fileId: 'e' } }
, { general: { fileId: 'b' } }
, { general: { fileId: 'd' } }
, { general: { fileId: 'c' } }
, { general: { fileId: 'a' } }
]
isort
( contramap (asc, generalFileId)
, files
)
// [ { general: { fileId: 'a' } }
// , { general: { fileId: 'b' } }
// , { general: { fileId: 'c' } }
// , { general: { fileId: 'd' } }
// , { general: { fileId: 'e' } }
// ]
The advantages of this approach should be evident. Instead of one big complex function that is difficult to write, read, and test, we've combined several smaller functions that are easier to write, read, and test. The smaller functions have the added advantage of being used in other parts of your program, whereas the big complex function is likely to only be usable in one part.
Lastly, sortBy is implemented as a higher-order function which means we're not limited to only ascending and descending sorts toggled by the reverse boolean; any valid comparator can be used. This means we could even write a specialized comparator that handles tie breaks using custom logic or compares year first, then month, then day, etc; higher-order functions expand your possibilities tremendously.
I don't like making empty promises so I want to show you that it's not difficult to devise your own mechanisms like Maybe. This is also a nice lesson in data abstraction because it shows us how a module has its own set of concerns. The module's exported values are the only way to access the module's functionalities; all other components of the module are private and are free to change or refactor as other requirements dictate -
// Maybe.js
const None =
Symbol ()
class Maybe
{ constructor (v)
{ this.value = v }
chain (f)
{ return this.value == None ? this : f (this.value) }
getOrElse (v)
{ return this.value === None ? v : this.value }
}
const Nothing = () =>
new Maybe (None)
const Just = v =>
new Maybe (v)
const fromNullable = v =>
v == null
? Nothing ()
: Just (v)
module.exports =
{ Just, Nothing, fromNullable } // note the class is hidden from the user
Then we would use it in our module. We only have to change the import (require) but everything else just works as-is because of the public API of our module matches -
const { Just, Nothing, fromNullable } =
require ('./Maybe') // this time, use our own Maybe
const safeProp = (o = {}, p = '') => // nothing changes here
Object (o) === o
? fromNullable (o[p])
: Nothing ()
const deepProp = (o, props) => // nothing changes here
props .reduce
( (acc, p) =>
acc .chain (child => safeProp (child, p))
, fromNullable (o)
)
// ...
For more intuition on how to use contramap, and perhaps some unexpected surprises, please explore the following related answers -
multi-sort using contramap
recursive search using contramap
You can use a loop to extract a nested property path from an object:
const obj = {
a: {
b: {
c: 3
}
}
}
const keys = ['a', 'b', 'c']
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
console.log(`c=${value}`);
Then you can wrap the function above into a helper:
function getPath(obj, keys) {
let value = obj;
for (const key of keys) {
if (!value) break; // stop once we reach a falsy value. Optionally you can make this a tighter check accounting for objects only
value = value[key];
}
return value;
}
And use it when obtaining your values:
sortBy = (isReverse = false, keys = []) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getPath(a, keys) || '';
const valueB = getPath(b, keys) || '';
// ...
})
}));
}
You can loop ovver the keys to get the values and then compare them like
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const clonedKey = [...keys];
let valueA = a;
let valueB = b
while(clonedKey.length > 0) {
const key = clonedKey.shift();
valueA = (valueA || {})[key];
valueB = (valueB || {})[key];
}
valueA = valueA || '';
valueB = valueB || '';
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
One way could be using reduce() over the new keys argument, something like this:
sortBy = (keys, isReverse=false) =>
{
this.setState(prevState =>
({
files: prevState.files.slice().sort((a, b) =>
{
const valueA = (keys.reduce((acc, key) => (acc || {})[key], a) || '').toString();
const valueB = (keys.reduce((acc, key) => (acc || {})[key], b) || '').toString();
return (isReverse ? valueB.localeCompare(valueA) : valueA.localeCompare(valueB));
})
}));
}
To work with an arbitrary number of keys, you could create a function that could be reused with .reduce() to traverse deeply into nested objects. I'd also put the keys as the last parameter, so that you can use "rest" and "spread" syntax.
const getKey = (o, k) => (o || {})[k];
const sorter = (isReverse, ...keys) => (a, b) => {
const valueA = keys.reduce(getKey, a) || '';
const valueB = keys.reduce(getKey, b) || '';
if (isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
};
const sortBy = (isReverse = false, ...keys) => {
this.setState(prevState => ({
files: prevState.files.sort(sorter(isReverse, ...keys))
}));
}
I also moved the sort function out to its own const variable, and made it return a new function that uses the isReverse value.
This also handles the case when the path resolves to a non-string value by converting it to string. Otherwise .localeCompare might fail.
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
const valueA = getValueAtPath(a, keys);
const valueB = getValueAtPath(b, keys);
if(isReverse) return valueB.localeCompare(valueA);
return valueA.localeCompare(valueB);
})
}));
}
function getValueAtPath(file, path) {
let value = file;
let keys = [...path]; // preserve the original path array
while(value && keys.length) {
let key = keys.shift();
value = value[key];
}
return (value || '').toString();
}
Compare elements in sort function in following way:
let v= c => keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1) * v(a).localeCompare(v(b));
likte this:
sortBy = (keys, isReverse=false) => {
this.setState(prevState => ({
files: prevState.files.sort((a, b) => {
let v=c=>keys.reduce((o,k) => o[k]||'',c)
return (isReverse ? -1 : 1)*v(a).localeCompare(v(b));
})
}));
}
Here is example how this idea works:
let files = [
{ general: { fileID: "3"}},
{ general: { fileID: "1"}},
{ general: { fileID: "2"}},
{ general: { }}
];
function sortBy(keys, arr, isReverse=false) {
arr.sort((a,b,v=c=>keys.reduce((o,k) => o[k]||'',c)) =>
(isReverse ? -1 : 1)*v(a).localeCompare(v(b)) )
}
sortBy(['general', 'fileID'],files,true);
console.log(files);
Related
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 ;)
Consider that we have an 'api_fetchData' which fetch data from server, and based on route, it could be table or a tree, also we have two state that we should update them based on the received data. and i should note that if the data was a table we should sort it's records by its priority. i wonder how should i do that in functionl programming way(like use ramda or either ...)
const tree=null;
and
const table = null:
//table
{
type: "table",
records: [
{
id: 1,
priority: 15
},
{
id: 2,
priority: 3
}
]
}
//tree
{
type: "tree",
children: [
{
type: "table",
records: [
{
id: 1,
priority: 15
},
{
id: 2,
priority: 3
}
]
}
]
}
This is what i did:
//define utils
const Right = x => ({
map: f => Right(f(x)),
fold: (f, g) => g(x),
inspect: x => `Right(${x})`
});
const Left = x => ({
map: f => Left(x),
fold: (f, g) => f(x),
inspect: x => `Left(${x})`
});
const fromNullable = x => (x != null ? Right(x) : Left(null));
const state = reactive({
path: context.root.$route.path,
tree: null,
table: null
});
// final code:
const fetchData = () => {
return fromNullable(mocked_firewall[state.path])
.map(data =>
data.type === "tree"
? (state.tree = data)
: (state.table = R.mergeRight(data, {
records: prioritySort(data.records)
}))
)
.fold(
() => console.log("there is no data based on selected route"),
x => console.log(x)
);
};
You have several problems here -
safely accessing deeply nested data on an uncertain object
sorting an array of complex data using functional techniques
safely and immutably updating deeply nested data on an uncertain object
1. Using .map and .chain is low-level and additional abstraction should be added when syntax becomes painful –
const state =
{ tree:
{ type: "sequoia"
, children: [ "a", "b", "c" ]
}
}
recSafeProp(state, [ "tree", "type" ])
.getOrElse("?") // "sequoia"
recSafeProp(state, [ "tree", "foobar" ])
.getOrElse("?") // "?"
recSafeProp(state, [ "tree", "children", 0 ])
.getOrElse("?") // "a"
recSafeProp(state, [ "tree", "children", 1 ])
.getOrElse("?") // "b"
recSafeProp(state, [ "tree", "children", 99 ])
.getOrElse("?") // "?"
We can implement recSafeProp easily, inventing our own convenience function, safeProp, along the way –
const { Nothing, fromNullable } =
require("data.maybe")
const recSafeProp = (o = {}, props = []) =>
props.reduce // for each props as p
( (r, p) => // safely lookup p on child
r.chain(child => safeProp(child, p))
, fromNullable(o) // init with Maybe o
)
const safeProp = (o = {}, p = "") =>
Object(o) === o // if o is an object
? fromNullable(o[p]) // wrap o[p] Maybe
: Nothing() // o is not an object, return Nothing
2. The Comparison module. You seem to have familiarity with functional modules so let's dive right in –
const { empty, map } =
Comparison
const prioritySort =
map(empty, record => record.priority || 0)
// or...
const prioritySort =
map(empty, ({ priority = 0}) => priority)
myarr.sort(prioritySort)
If you want immutable sort –
const isort = ([ ...copy ], compare = empty) =>
copy.sort(compare)
const newArr =
isort(origArr, proritySort)
Here's the Comparison module. It is introduced in this Q&A. Check it out there if you're interested to see how to make complex sorters in a functional way –
const Comparison =
{ empty: (a, b) =>
a < b ? -1
: a > b ? 1
: 0
, map: (m, f) =>
(a, b) => m(f(a), f(b))
, concat: (m, n) =>
(a, b) => Ordered.concat(m(a, b), n(a, b))
, reverse: (m) =>
(a, b) => m(b, a)
, nsort: (...m) =>
m.reduce(Comparison.concat, Comparison.empty)
}
const Ordered =
{ empty: 0
, concat: (a, b) =>
a === 0 ? b : a
}
Combining sorters –
const { empty, map, concat } =
Comparison
const sortByProp = (prop = "") =>
map(empty, (o = {}) => o[prop])
const sortByFullName =
concat
( sortByProp("lastName") // primary: sort by obj.lastName
, sortByProp("firstName") // secondary: sort by obj.firstName
)
data.sort(sortByFullName) // ...
Composable sorters –
const { ..., reverse } =
Comparison
// sort by `name` then reverse sort by `age` –
data.sort(concat(sortByName, reverse(sortByAge)))
Functional prinicples –
// this...
concat(reverse(sortByName), reverse(sortByAge))
// is the same as...
reverse(concat(sortByName, sortByAge))
As well as –
const { ..., nsort } =
Comparison
// this...
concat(sortByYear, concat(sortByMonth, sortByDay))
// is the same as...
concat(concat(sortByYear, sortByMonth), sortByDay)
// is the same as...
nsort(sortByYear, sortByMonth, sortByDay)
3. The lowest hanging fruit to reach for here is Immutable.js. If I find more time later, I'll show a lo-fi approach to the specific problem.
Your solution seems overkill to me.
Functional programming is about many things, and there is no concise definition, but if your main unit of work is the pure function and if you don't mutate data, you're well on the way to being a functional programmer. Using an Either monad has some powerful benefits at times. But this code looks more like an attempt to fit Either into a place where it makes little sense.
Below is one suggested way to write this code. But I've had to make a few assumptions along the way. First, as you discuss fetching data from a server, I'm assuming that this has to run in an asynchronous mode, using Promises, async-await or a nicer alternative like Tasks or Futures. I further assumed that where you mention mocked_firewall is where you are doing the actual asynchronous call. (Your sample code treated it as an object where you could look up results from a path; but I can't really make sense of that for mimicking a real service.) And one more assumption: the fold(() => log(...), x => log(x)) bit was nothing essential, only a demonstration that your code did what it was supposed to do.
With all that in mind, I wrote a version with an object mapping type to functions, each one taking data and state and returning a new state, and with the central function fetchData that takes something like your mocked_firewall (or really like my alteration of that to return a Promise) and returns a function that accepts a state and returns a new state.
It looks like this:
// State-handling functions
const handlers = {
tree: (data, state) => ({... state, tree: data}),
table: (data, state) => ({
... state,
table: {... data, records: prioritySort (data .records)}
})
}
// Main function
const fetchData = (firewall) => (state) =>
firewall (state)
.then ((data) => (handlers [data .type] || ((data, state) => state)) (data, state))
// Demonstrations
fetchData (mocked_firewall) ({path: 'foo', table: null, tree: null})
.then (console .log) // updates the tree
fetchData (mocked_firewall) ({path: 'bar', table: null, tree: null})
.then (console .log) // updates the table
fetchData (mocked_firewall) ({path: 'baz', table: null, tree: null})
.then (console .log) // server returns type we can't handle; no update
fetchData (mocked_firewall) ({path: 'qux', table: null, tree: null})
.then (console .log) // server returns no `type`; no update
.as-console-wrapper {min-height: 100% !important; top: 0}
<script>
// Dummy functions -- only for demonstration purposes
const prioritySort = (records) =>
records .slice (0) .sort (({priority: p1}, {priority: p2}) => p1 - p2)
const mocked_firewall = ({path}) =>
Promise .resolve ({
foo: {
type: "tree",
children: [{
type: "table",
records: [{id: 1, priority: 15}, {id: 2, priority: 3}]
}]
},
bar: {
type: 'table',
records: [{id: 1, priority: 7}, {id: 2, priority: 1}, {id: 3, priority: 4}]
},
baz: {type: 'unknown', z: 45},
} [path] || {})
</script>
You will notice that this does not alter the state; instead it returns a new object for the state. I see that this is tagged vue, and as I understand it, that is not how Vue works. (This is one of the reasons I haven't really used Vue, in fact.) You could easily change the handlers to update the state in place, with something like tree: (data, state) => {state.tree = data; return state}, or even skipping the return. But don't let any FP zealots catch you doing this; remember that the functional programmers are well versed in "key algorithmic techniques such as recursion and condescension." 1.
You also tagged this ramda.js. I'm one of the founders of Ramda, and a big fan, but I see Ramda helping here only around the edges. I included for instance, a naive version of the prioritySort that you mentioned but didn't supply. A Ramda version would probably be nicer, something like
const prioritySort = sortBy (prop ('priority'))
Similarly, if you don't want to mutate the state, we could probably rewrite the handler functions with Ramda versions, possibly simplifying a bit. But that would be minor. For the main function, I don't see anything that would be improved by using Ramda functions.
There is a good testability argument to be made that we should pass not just the firewall to the main function but also the handlers object. That's entirely up to you, but it does make it easier to mock and test the parts independently. If you don't want to do that, it is entirely possible to inline them in the main function like this:
const fetchData = (firewall) => (state) =>
firewall (state)
.then ((data) => (({
tree: (data, state) => ({... state, tree: data}),
table: (data, state) => ({
... state,
table: {...data, records: prioritySort(data .records)}
})
}) [data .type] || ((data, state) => state)) (data, state))
But in the end, I find the original easier to read, either as is, or with the handlers supplied as another parameter of the main function.
1 The original quote is from Verity Stob, but I know it best from James Iry's wonderful A Brief, Incomplete, and Mostly Wrong History of Programming Languages.
🎈 Short and simple way
In my example I use ramda, you need to compose some functions and voila ✨:
const prioritySort = R.compose(
R.when(
R.propEq('type', 'tree'),
R.over(
R.lensProp('children'),
R.map(child => prioritySort(child))
)
),
R.when(
R.propEq('type', 'table'),
R.over(
R.lensProp('records'),
R.sortBy(R.prop('priority')),
)
),
)
const fetchData = R.pipe(
endpoint => fetch(`https://your-api.com/${endpoint}`, opts).then(res => res.json()),
R.andThen(prioritySort),
)
fetchData(`tree`).then(console.log)
fetchData(`table`).then(console.log)
Check the demo
For inspection you can simple use function const log = value => console.log(value) || value
R.pipe(
// some code
log,
// some code
)
it will log the piping value.
I'm new to programming, and I was working a question which is bother me for a while.
I want to recursively create a nested object from another nested object in javascript,
below is the sample data for input, but in real situation, I don't how deep will this object to be.
nums = {
Obj:{
x1:{
x11:43,
x12:4,
x13:612
},
x2:{
x21:4,
x22:7,
},
x3:2,
}}
this is the result I want (see number is even or odd, even=true, odd=false)
res = {
Obj:{
x1:{
x11:false,
x12:true,
x13:true
},
x2:{
x21:true,
x22:false,
},
x3:true,
}}
and this is my code
const nums = {
Obj:{
x1:{
x11:43,
x12:4,
x13:612
},
x2:{
x21:4,
x22:7,
},
x3:2,
}
}
const res ={};
getResult(nums);
console.log(res);
function getResult(x){
Object.keys(x).forEach(element => {
if(isNaN(x[element])){
res[element]=getResult(x[element]);
} else {
let result = (x[element] % 2 < 1)? true:false;
return {[element]: result}; // this is where I don't know what to, I try to return a object,
// but it gives{x1: undefined, x2: undefined, Obj: undefined}
//
// if I change to "return res[element]=result"
// every sub-Object will add under the same level
}
});
}
I will really appreciate if someone can help me on this.
Instead of return {[element]: result}; , overwrite the value, and return the mutated object from the function after the loop :
note that this will mutate the original object, if you want to keep it, make a copy :
const copy = JSON.parse(JSON.stringify(nums));
const nums = {
Obj: {
x1: {
x11: 43,
x12: 4,
x13: 612
},
x2: {
x21: 4,
x22: 7,
},
x3: 2,
}
}
const res = {};
const copy = JSON.parse(JSON.stringify(nums));
getResult(copy);
console.log(res);
function getResult(x) {
Object.keys(x).forEach(element => {
if (isNaN(x[element])) {
res[element] = getResult(x[element]);
} else {
let result = (x[element] % 2 < 1) ? true : false;
x[element] = result; // overwrite the number with true or flse
}
});
return x; // return the mutated object
}
Instead of mutating something make something more functional and return a new object:
const getResults = o => typeof o === "object"
? Object.keys(o).reduce((a, k) => ({ ...a, [k]: getResults(o[k]) }), {})
: o % 2 === 1;
Basically we check if object is an object (using typeof) and go deeper if so. Otherwise we check if it is odd or even.
You can also think of this more generically, writing a function that will apply your transformation to all the leaf nodes of your object, then calling it with an isEven function. Here's one technique:
const mapLeaves = (fn) => (tree) =>
typeof tree == "object"
? Object .fromEntries (Object .entries (tree) .map (
([k, v]) => [k, mapLeaves (fn) (v)]
))
: fn (tree)
const isEven = (n) => n % 2 == 0
const nums = {Obj: {x1: {x11: 43, x12: 4, x13: 612}, x2: {x21: 4, x22: 7}, x3: 2}}
console .log (
mapLeaves (isEven) (nums)
)
And of course mapLeaves (isEven) is a reusable function that you could apply to multiple objects.
This does not handle arrays. It would only be slightly more complex to create a version of mapLeaves that also applied this to entries of an array:
const mapLeaves = (fn) => (tree) =>
Array .isArray (tree)
? tree .map (x => mapLeaves (fn) (x))
: typeof tree == "object"
? Object .fromEntries (Object .entries (tree) .map (
([k, v]) => [k, mapLeaves (fn) (v)]
))
: fn (tree)
I have a JavaScript object with some nested properties that I want to update based on some conditions. The starting object could be something like:
const options = {
formatOption: {
label: 'Model Format',
selections: {
name: 'Specific Format',
value: '12x28',
}
},
heightOption: {
label: 'Model Height',
selections: {
name: 'Specific Height',
value: '15',
}
}
};
I have come up with a solution using Object.keys, reduce and the spread operator, but I would like to know if this is the best / more concise way as of today or if there is a better way. I'm not looking for the most performing option, but for a "best practice" (if there is one) or a more elegant way.
EDIT 30/01/20
As pointed out in the comments by #CertainPerformance my code was mutating the original options variable, so I am changing the line const option = options[key]; to const option = { ...options[key] };. I hope this is correct and that the function is not mutating the original data.
const newObject = Object.keys(options).reduce((obj, key) => {
const option = { ...options[key] };
const newVal = getNewValue(option.label); // example function to get new values
// update based on existence of new value and key
if (option.selections && option.selections.value && newVal) {
option.selections.value = newVal;
}
return {
...obj,
[key]: option,
};
}, {});
getNewValue is an invented name for a function that I am calling in order to get an 'updated' version of the value I am looking at. In order to reproduce my situation you could just replace
the line const newVal = getNewValue(option.label); with const newVal = "bla bla";
Since you tagged this q with functional-programming here is a functional approach. Functional Lenses are an advanced FP tool and hence hard to grasp for newbies. This is just an illustration to give you an idea of how you can solve almost all tasks and issues related to getters/setters with a single approach:
// functional primitives
const _const = x => y => x;
// Identity type
const Id = x => ({tag: "Id", runId: x});
const idMap = f => tx =>
Id(f(tx.runId));
function* objKeys(o) {
for (let prop in o) {
yield prop;
}
}
// Object auxiliary functions
const objSet = (k, v) => o =>
objSetx(k, v) (objClone(o));
const objSetx = (k, v) => o =>
(o[k] = v, o);
const objDel = k => o =>
objDelx(k) (objClone(o));
const objDelx = k => o =>
(delete o[k], o);
const objClone = o => {
const p = {};
for (k of objKeys(o))
Object.defineProperty(
p, k, Object.getOwnPropertyDescriptor(o, k));
return p;
};
// Lens type
const Lens = x => ({tag: "Lens", runLens: x});
const objLens_ = ({set, del}) => k => // Object lens
Lens(map => ft => o =>
map(v => {
if (v === null)
return del(k) (o);
else
return set(k, v) (o)
}) (ft(o[k])));
const objLens = objLens_({set: objSet, del: objDel});
const lensComp3 = tx => ty => tz => // lens composition
Lens(map => ft =>
tx.runLens(map) (ty.runLens(map) (tz.runLens(map) (ft))));
const lensSet = tx => v => o => // set operation for lenses
tx.runLens(idMap) (_const(Id(v))) (o);
// MAIN
const options = {
formatOption: {
label: 'Model Format',
selections: {
name: 'Specific Format',
value: '12x28',
}
},
heightOption: {
label: 'Model Height',
selections: {
name: 'Specific Height',
value: '15',
}
}
};
const nameLens = lensComp3(
objLens("formatOption"))
(objLens("selections"))
(objLens("name"));
const options_ = lensSet(nameLens) ("foo") (options).runId;
// deep update
console.log(options_);
// reuse of unaffected parts of the Object tree (structural sharing)
console.log(
options.heightOptions === options_.heightOptions); // true
This is only a teeny-tiny part of the Lens machinery. Functional lenses have the nice property to be composable and to utilize structural sharing for some cases.
If you want to set a value for a nested property in a immutable fashion,
then you should consider adopting a library rather than doing it manually.
In FP there is the concept of lenses
Ramda provides a nice implementation: https://ramdajs.com/docs/
const selectionsNameLens = R.lensPath(
['formatOption', 'selections', 'name'],
);
const setter = R.set(selectionsNameLens);
// ---
const data = {
formatOption: {
label: 'Model Format',
selections: {
name: 'Specific Format',
value: '12x28',
},
},
heightOption: {
label: 'Model Height',
selections: {
name: 'Specific Height',
value: '15',
},
},
};
console.log(
setter('Another Specific Format', data),
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js" integrity="sha256-xB25ljGZ7K2VXnq087unEnoVhvTosWWtqXB4tAtZmHU=" crossorigin="anonymous"></script>
The first comment from CertainPerformance made me realize that I was mutating the original options variable. My first idea was to make a copy with the spread operator, but the spread operator only makes a shallow copy, so even in my edit I was still mutating the original object.
What I think is a solution is to create a new object with only the updated property, and to merge the two objects at the end of the reducer.
EDIT
The new object also needs to be merged with the original option.selections, otherwise I would still overwrite existing keys at that level (ie I would overwrite option.selections.name).
Here is the final code:
const newObject = Object.keys(options).reduce((obj, key) => {
const option = options[key];
const newVal = getNewValue(option.label); // example function to get new values
const newOption = {}; // create a new empty object
// update based on existence of new value and key
if (option.selections && option.selections.value && newVal) {
// fill the empty object with the updated value,
// merged with a copy of the original option.selections
newOption.selections = {
...option.selections,
value: newVal
};
}
return {
...obj, // accumulator
[key]: {
...option, // merge the old option
...newOption, // with the new one
},
};
}, {});
A more concise version that has been suggested to me would be to use forEach() instead of reduce(). In this case the only difficult part would be to clone the original object. One way would be to use lodash's _.cloneDeep(), but there are plenty of options (see here).
Here is the code:
const newObject = _.cloneDeep(options);
Object.keys(newObject).forEach(key => {
const newVal = getNewValue(newObject[key].label); // example function to get new values
// update based on existence of new value and key
if (newObject[key].selections && newObject[key].selections.value && newVal) {
newObject[key].selections.value = newVal;
}
});
The only problem is that forEach() changes values that are declared outside of the function, but reduce() can mutate its parameter (as it happened in my original solution), so the problem is not solved by using reduce() alone.
I'm not sure that this is the best solution, but it surely is much more readable for the average developer than my first try or the other solutions.