'How to remove object properties with negative values, regardless of depth?

I'm looking for a way to remove object properties with negative values. Although an existing solution is provided here, it works only with no-depth objects. I'm looking for a solution to remove negative object properties of any depth.

This calls for a recursive solution, and I made an attempt that advanced me, but still not quite it.

Consider the following stocksMarkets data. The structure is purposely messy to demonstrate my desire to remove negative properties regardless of depth.

const stocksMarkets = {
  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,
};

I want a function, let's call it removeNegatives() that would return the following output:

// pseudo-code
removeNegatives(stocksMarkets)

// {
//   tokyo: {
//     today: {
//       mitsubishi: 0.65,
//     },
//     yearToDate: {
//       canon: 22.9,
//     },
//   },
//   nyc: {
//     sp500: {
//       ea: 8.5,
//     },
//     dowJones: {
//       visa: 3.14,
//       chevron: 2.38,
//     },
//   },
//   berlin: {
//     foo: 2,
//   },
// };

Here's my attempt:

const removeNegatives = (obj) => {
  return Object.entries(obj).reduce((t, [key, value]) => {
    return {
      ...t,
      [key]:
        typeof value !== 'object'
          ? removeNegatives(value)
          : Object.values(value).filter((v) => v >= 0),
    };
  }, {});
};

But it doesn't really gets me what I want :-/ It works on 2nd depth level only (see berlin), and even then, it returns an array with just the value rather than the full property (i.e., including the key).

// { tokyo: [], nyc: [], berlin: [ 2 ], paris: {} }


Solution 1:[1]

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.

Solution 2:[2]

A lean non mutating recursive (tree walking) implementation based on Object.entries, Array.prototype.reduce and some basic type checking ...

function cloneStructureButKeepNumberTypeEntriesOfJustPositiveValues(root) {
  return Object
    .entries(root)
    .reduce((node, [key, value]) => {

      // simple but reliable object type test.
      if (value && ('object' === typeof value)) {

        node[key] =
          cloneStructureButKeepNumberTypeEntriesOfJustPositiveValues(value);

      } else  if (
        // either not a number type
        ('number' !== typeof value) ||

        // OR (if number type then)
        // a positive number value.
        (Math.abs(value) === value)
      ) {
        node[key] = value;
      }
      return node;

    }, {});
}

const stocksMarkets = {
  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,
};
const rosyOutlooks =
  cloneStructureButKeepNumberTypeEntriesOfJustPositiveValues(stocksMarkets);

console.log({ rosyOutlooks, stocksMarkets });
.as-console-wrapper { min-height: 100%!important; top: 0; }

Since there was discussion going on about performance ... A lot of developers still underestimate the performance boost the JIT compiler gives to function statements/declarations.

I felt free tying r3wt's performance test reference, and what I can say is that two function statements with corecursion perform best in a chrome/mac environment ...

function corecursivelyAggregateEntryByTypeAndValue(node, [key, value]) {
  // simple but reliable object type test.
  if (value && ('object' === typeof value)) {

    node[key] =
      cloneStructureButKeepNumberTypeEntriesOfJustPositiveValues(value);

  } else  if (
    // either not a number type
    ('number' !== typeof value) ||

    // OR (if number type then)
    // a positive number value.
    (Math.abs(value) === value)
  ) {
    node[key] = value;
  }
  return node;
}
function cloneStructureButKeepNumberTypeEntriesOfJustPositiveValues(root) {
  return Object
    .entries(root)
    .reduce(corecursivelyAggregateEntryByTypeAndValue, {});
}

const stocksMarkets = {
  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,
};
const rosyOutlooks =
  cloneStructureButKeepNumberTypeEntriesOfJustPositiveValues(stocksMarkets);

console.log({ rosyOutlooks, stocksMarkets });
.as-console-wrapper { min-height: 100%!important; top: 0; }

Edit ... refactoring of the above corecursion based implementation in order to cover the 2 open points mentioned by the latest 2 comments ...

"OP requested in the comments the ability to filter by a predicate function, which your answer doesn't do, but nonetheless i added it to the bench [...]" – r3wt

"Nice (although you know I personally prefer terse names!) I wonder, why did you choose Math.abs(value) === value over value >= 0?" – Scott Sauyet

// The implementation of a generic approach gets covered
// by two corecursively working function statements.
function corecursivelyAggregateEntryByCustomCondition(
  { condition, node }, [key, value],
) {
  if (value && ('object' === typeof value)) {

    node[key] =
      copyStructureWithConditionFulfillingEntriesOnly(value, condition);

  } else if (condition(value)) {

    node[key] = value;
  }
  return { condition, node };
}
function copyStructureWithConditionFulfillingEntriesOnly(
  root, condition,
) {
  return Object
    .entries(root)
    .reduce(
      corecursivelyAggregateEntryByCustomCondition,
      { condition, node: {} },
    )
    .node;
}

// the condition ... a custom predicate function.
function isNeitherNumberTypeNorNegativeValue(value) {
  return (
    'number' !== typeof value ||
    0 <= value
  );
}

// the data to work upon.
const stocksMarkets = {
  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,
};

// object creation.
const rosyOutlooks =
  copyStructureWithConditionFulfillingEntriesOnly(
    stocksMarkets,
    isNeitherNumberTypeNorNegativeValue,
  );

console.log({ rosyOutlooks, stocksMarkets });
.as-console-wrapper { min-height: 100%!important; top: 0; }

Solution 3:[3]

You could get entries and collect them with positive values or nested objects.

const
    filterBy = fn => {
         const f = object => Object.fromEntries(Object
            .entries(object)
            .reduce((r, [k, v]) => {
                if (v && typeof v === 'object') r.push([k, f(v)]);
                else if (fn(v)) r.push([k, v]);
                return r;
            }, [])
        );
        return f;
    },
    fn = v => v >= 0,
    filter = filterBy(fn),
    stocksMarkets = { 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 },
    result = filter(stocksMarkets);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Solution 4:[4]

Note: This mutates the object. if immutability is a concern for your use case, you should clone the object first.

For recursive object node manipulation, it is better to create a helper function which allows recursive "visiting" of object nodes, using a supplied visitor function which is free to manipulate the object node according to business logic. A basic implementation looks like so(shown in typescript so that it is clear what is happening):

function visit_object(obj: any, visitor: (o: any, k: string )=>any|void ) {
  for (let key in obj) {
    if (typeof obj[key] === 'object') {
      visit_object(obj[key],visitor);
    } else {
      visitor(obj,key);//visit the node object, allowing manipulation.
    }
  }
  // not necessary to return obj; js objects are pass by reference value. 
}

As for your specific use case, the following demonstrates how to remove a node whose value is negative.

visit_object( yourObject, (o,k)=>{
  if(o[k]<0){
    delete o[k];
  }
});

Edit: curried version for performance, including optional deepClone

const deepClone = (inObject) => {
  let outObject, value, key;

  if (typeof inObject !== "object" || inObject === null) {
    return inObject; // Return the value if inObject is not an object
  }

  // Create an array or object to hold the values
  outObject = Array.isArray(inObject) ? [] : {};

  for (key in inObject) {
    value = inObject[key];

    // Recursively (deep) copy for nested objects, including arrays
    outObject[key] = deepClone(value);
  }

  return outObject;
},
visit_object = visitor => ( obj ) => {
  for (let key in obj) {
    if (typeof obj[key] === 'object') {
      visit_object( obj[key], visitor);
    } else {
      visitor(obj,key);//visit the node object, allowing manipulation.
    }
  }
  // not necessary to return obj; js objects are pass by reference value. 
},
filter=(o,k)=>{
  if(o[k]<0){
    delete o[k];
  }
};

Solution 5:[5]

Use this:

const removeNegatives = (obj) => {
  return Object.entries(obj).reduce((t, [key, value]) => {
    const v =
      value && typeof value === "object"
        ? removeNegatives(value)
        : value >= 0
        ? value
        : null;
    if (v!==null) {
      t[key] = v;
    }

    return t;
  }, {});
};

const stocksMarkets = {
  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,
};
const positives = removeNegatives(stocksMarkets);

console.log({ positives, stocksMarkets });
.as-console-wrapper { min-height: 100%!important; top: 0; }

Solution 6:[6]

It doesn't have to be that complicated. Just iterate over the object. If the current property is an object call the function again with that object, otherwise check to see if the property value is less than 0 and, if it is, delete it. Finally return the updated object.

const stocksMarkets={tokyo:{today:{toyota:-1.56,sony:-.89,nippon:-.94,mitsubishi:.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};

function remove(obj) {
  for (const prop in obj) {
    if (typeof obj[prop] === 'object') {
      remove(obj[prop]);
      continue;
    }
    if (obj[prop] < 0) delete obj[prop];
  }
  return obj;
}

console.log(remove(stocksMarkets));

Note: this mutates the original object. If you want to make a deep copy of that object and use that instead the quickest way maybe to stringify, and then parse it.

const copy = JSON.parse(JSON.stringify(stocksMarkets));

Or: there is a new brand new feature in some browsers called structuredClone (check the compatibility table).

const clone = structureClone(stocksMarkets);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3
Solution 4
Solution 5
Solution 6