A combination of Array.prototype.flatMap
, Object.entries
and Object.fromEntries
along with a dose of recursion can make problems like this fairly simple:
const removeNegatives = (obj) => Object (obj) === obj
? Object .fromEntries (Object .entries (obj) .flatMap (
([k, v]) => v < 0 ? [] : [[k, removeNegatives (v)]]
))
: obj
const stockMarkets = {tokyo: {today: {toyota: -1.56, sony: -0.89, nippon: -0.94, mitsubishi: 0.65, }, yearToDate: {toyota: -75.95, softbank: -49.83, canon: 22.9}, }, nyc: {sp500: {ea: 8.5, tesla: -66}, dowJones: {visa: 3.14, chevron: 2.38, intel: -1.18, salesforce: -5.88, }, }, berlin: {foo: 2}, paris: -3}
console .log (removeNegatives (stockMarkets))
.as-console-wrapper {max-height: 100% !important; top: 0}
If our input is not an object, we just return it intact. If it is, we split it into key-value pairs, then for each of those, if the value is a negative number, we skip it; otherwise we recur on that value. Then we stitch these resulting key-value pairs back into an object.
You might want to do a type-check on v
before v < 0
. It's your call.
This is begging for one more level of abstraction, though. I would probably prefer to write it like this:
const filterObj = (pred) => (obj) => Object (obj) === obj
? Object .fromEntries (Object .entries (obj) .flatMap (
([k, v]) => pred (v) ? [[k, filterObj (pred) (v)]] : []
))
: obj
const removeNegatives = filterObj ((v) => typeof v !== 'number' || v > 0)
Update: Simple leaf filtering
The OP asked for an approach that allows for simpler filtering on the leaves. The easiest way I know to do that is to go through an intermediate stage like this:
[
[["tokyo", "today", "toyota"], -1.56],
[["tokyo", "today", "sony"], -0.89],
[["tokyo", "today", "nippon"], -0.94],
[["tokyo", "today", "mitsubishi"], 0.65],
[["tokyo", "yearToDate", "toyota"], -75.95],
[["tokyo", "yearToDate", "softbank"], -49.83],
[["tokyo", "yearToDate", "canon"], 22.9],
[["nyc", "sp500", "ea"], 8.5],
[["nyc", "sp500", "tesla"], -66],
[["nyc", "dowJones", "visa"], 3.14],
[["nyc", "dowJones", "chevron"], 2.38],
[["nyc", "dowJones", "intel"], -1.18],
[["nyc", "dowJones", "salesforce"], -5.88],
[["berlin", "foo"], 2],
[["paris"], -3]
]
then run our simple filter on those entries to get:
[
[["tokyo", "today", "mitsubishi"], 0.65],
[["tokyo", "yearToDate", "canon"], 22.9],
[["nyc", "sp500", "ea"], 8.5],
[["nyc", "dowJones", "visa"], 3.14],
[["nyc", "dowJones", "chevron"], 2.38],
[["berlin", "foo"], 2],
]
and reconstitute that back into an object. I have lying around functions that do that extract and rehydration, so it's really just a matter of tying them together:
// 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), {})
const filterLeaves = (fn) => (obj) =>
hydrate (pathEntries (obj) .filter (([k, v]) => fn (v)))
// main function
const removeNegatives = filterLeaves ((v) => v >= 0)
// sample data
const stockMarkets = {tokyo: {today: {toyota: -1.56, sony: -0.89, nippon: -0.94, mitsubishi: 0.65, }, yearToDate: {toyota: -75.95, softbank: -49.83, canon: 22.9}, }, nyc: {sp500: {ea: 8.5, tesla: -66}, dowJones: {visa: 3.14, chevron: 2.38, intel: -1.18, salesforce: -5.88, }, }, berlin: {foo: 2}, paris: -3}
// demo
console .log (removeNegatives (stockMarkets))
.as-console-wrapper {max-height: 100% !important; top: 0}
You can see details of pathEntries
, setPath
, and hydrate
in various other answers. The important function here is filterLeaves
, which simply takes a predicate for leaf values, runs pathEntries
, filters the result with that predicate and calls hydrate
on the result. This makes our main function the trivial, filterLeaves ((v) => v > 0)
.
But we could do all sorts of things with this breakdown. We could filter based on keys and values. We could filter and map them before hydrating. We could even map to multiple new key-value results. Any of these possibilities are as simple as this filterLeaves
.