3

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: {} }
Emman
  • 3,695
  • 2
  • 20
  • 44

6 Answers6

3

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; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
  • 1
    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 https://jsbench.me/3tl333u2ar/3 – r3wt May 12 '22 at 18:01
  • 1
    Nice (although you know I personally prefer terse names!) I wonder, why did you choose `Math.abs(value) === value` over `value >= 0`? – Scott Sauyet May 12 '22 at 18:41
  • 1
    @r3wt ... thanks for pointing it. I changed the code accordingly. The now 3 folded approach (corecursion + additionally provided custom predicate function) is slower by 10% in comparison to your final solution whereas the version before was just 3% behind. – Peter Seliger May 12 '22 at 19:23
  • 2
    @ScottSauyet ... _"I wonder, why did you choose `Math.abs(value) === value` over `value >= 0`"_ ... now I wonder as well. It got fixed with the final version. – Peter Seliger May 12 '22 at 19:25
  • 1
    @PeterSeliger fnonetheless if its within 3%, its safe to say the code is roughly the same performance wise. there is a great deal of variance with these benchmarks run in the browser. it does make me wonder how similar the bytecode output of your solution vs mine would be. then we could gain further insight into the optimization's v8 does as you mentioned – r3wt May 12 '22 at 19:40
2

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; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • 2nd least efficient answer on the page, https://jsbench.me/3tl333u2ar/1 – r3wt May 12 '22 at 14:33
  • @r3wt: You need to compare apples with apples. This solution does not mutate the original object. I think if you wrapped some `clone` function before your own, the numbers would be a lot more similar. – Scott Sauyet May 12 '22 at 14:38
  • @ScottSauyet The OP doesn't reference any need for immutability, yet even with a deep clone, my code is obviously still more efficient. There is no reason to overengineer a solution to this solved problem, thats why i shared my answer. its a solved problem with a simple and efficient algorithm, thanks to the fact that objects are pass by reference value in js. https://jsbench.me/3tl333u2ar/2 – r3wt May 12 '22 at 14:53
  • In my browser (Chrome 100, Windows 10) Nina's version is the fastest in this benchmark. – Scott Sauyet May 12 '22 at 15:01
  • 1
    There are those of us interested in functional programming who find the manipulation of existing data an anathema. So your "solved problem" is more of an anti-pattern to us. – Scott Sauyet May 12 '22 at 15:02
  • @ScottSauyet So far Peter Seliger's answer seems to be the most efficient FP implementation, though it doesn't include the predicate function as op requested. I went ahead and optimized mine further, and included all poster's updated answers. According to chrome my updated version is the fastest. https://jsbench.me/3tl333u2ar/3 Final thought I'll add here, is that this post is related to stock tickers in some way, whether its a trading bot, or a webapp displaying ticker data, the performance could matter here, which is why i felt the need to weigh in. – r3wt May 12 '22 at 17:54
  • @r3wt, btw for a stock ticker, i would skip a key/value pair. this is faster than building a new object or delete some properties. – Nina Scholz May 12 '22 at 18:03
  • @r3wt: yes, if performance is paramount, then I would code in a very different manner. In my webapp/werbservice world, clean code and reusability almost always trump performance. But there are times for either. – Scott Sauyet May 12 '22 at 20:08
  • @ScottSauyet performance, clean code, and reusability aren't mutually exclusive. even scala has mutable datatypes – r3wt May 12 '22 at 21:11
  • 1
    @r3wt: Of course not. But where you came to this focused on performance, I came to it worried about reusable parts and simple code. I could increase performance by using some of the tricks Peter did, and you could clean your code by extracting more reusable parts. It's good to give the OP various options which might meet the underlying needs. – Scott Sauyet May 12 '22 at 21:21
  • 2
    @ScottSauyet i understand your fp principles, but this language has pass by reference semantics and mutable data types, meaning both immutability and pure functions are opt-in behaviors up to the developers of a code base. there are times where immutability is strictly required, as well as having side effect free functions. agreed. but in my opinion, this use case isn't one, because both immutable and mutable cloning may be desirable, and thus cloning needs to be optional, either behind a parameter, or done externally. i really don't understand your claims my code is unclean and not reusable. – r3wt May 12 '22 at 21:55
2

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];
  }
};
r3wt
  • 4,642
  • 2
  • 33
  • 55
2

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.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 1
    Not only is this code not simple, its also about 5x slower than the standard for/loop recursion technique. https://jsbench.me/3tl333u2ar/1 – r3wt May 12 '22 at 14:27
  • @r3wt: I would argue the question of simplicity. But there's a much bigger difference than simplicity or speed. This one does not mutate the original data; the other benchmarked one does. We're not barbarians here, are we? :-) – Scott Sauyet May 12 '22 at 14:31
  • if mutability is a concern, the programmer can clone the object first, which is beyond the scope of the question. As to the simplicity question, this is a solved problem, using a simple for loop and recursion pattern, so what use is there to reinvent the wheel? – r3wt May 12 '22 at 14:44
  • Thanks Scott. Indeed, `filterObj()` is what I was subconsciously after. However, can `filterObj()` be tweaked to accept just a simple predicate (e.g., `const isPositive = x => x > 0`) rather than a function with the entire type-checking thing? – Emman May 12 '22 at 14:57
  • @Emman: This version won't lend itself very well to that, because we're using it on every node. We can write one that works only on leaf nodes. I will try to find time to dig one up later in the day. – Scott Sauyet May 12 '22 at 15:04
  • @Emman: Ok, I went ahead and found it now. There's an update with a version that you might find simpler to configure. – Scott Sauyet May 12 '22 at 15:27
1

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; }
Tanay
  • 871
  • 1
  • 6
  • 12
  • Should probably be `if (v !== null)` or it will remove 0 values (or anything that evaluates falsy) – Dave Meehan May 12 '22 at 12:19
  • Thanks, so far this is the only solution that allows me to *easily* generalize the function to be based on a predicate for the filtering procedure. That is, `const isPositive = (x) => x >= 0` and then, inside `reduce()`: `const v = typeof value === "object" ? removeNegatives(value) : isPositive(value) ? value : null` – Emman May 12 '22 at 12:31
  • `typeof value === "object"` is not a reliable check since it is false positive for the `null` value ... make it ... `value && typeof value === "object"` instead or even `!!value && typeof value === "object"` – Peter Seliger May 12 '22 at 16:46
0

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);
Andy
  • 61,948
  • 13
  • 68
  • 95
  • `obj[prop] < 0` should probably not occur if its already been assessed to be an `object` – Dave Meehan May 12 '22 at 12:21
  • It won't occur. If it's an object call the function again. If it's not check the value of the property, and remove it if's < 0. @DaveMeehan – Andy May 12 '22 at 12:25
  • 1
    The evaluation will occur, I agree that an object won't evaluate less than zero, but its a redundant evaluation. First conditional could be written as `{ remove(obj[prop]); continue }` to avoid the second redundant evaluation. – Dave Meehan May 12 '22 at 12:27
  • Well, would you look at that. It's a good thing I don't write a lot recursive code, @DaveMeehan. Thanks very much for your feedback. I learned something new today thanks to you. – Andy May 12 '22 at 12:54
  • 2
    Yay for collaboration ;-) – Dave Meehan May 12 '22 at 14:13
  • 1
    @DaveMeehan I am currently reading your blog :) – Andy May 12 '22 at 14:14