2

I have an object, of which parameters contain and array of object. I receive 1 object id and I need to find its position in that whole mess. With procedural programming I got it working with:

const opportunitiesById =  {
  1: [
    { id: 1, name: 'offer 1' },
    { id: 2, name: 'offer 1' }
  ],
  2: [
    { id: 3, name: 'offer 1' },
    { id: 4, name: 'offer 1' }
  ],
  3: [
    { id: 5, name: 'offer 1' },
    { id: 6, name: 'offer 1' }
  ]
};

const findObjectIdByOfferId = (offerId) => {
  let opportunityId;
  let offerPosition;
  const opportunities = Object.keys(opportunitiesById);

  opportunities.forEach(opportunity => {
    const offers = opportunitiesById[opportunity];

    offers.forEach((offer, index) => {
      if (offer.id === offerId) {
        opportunityId = Number(opportunity);
        offerPosition = index;
      }
    })
  });

return { offerPosition, opportunityId };
}

console.log(findObjectIdByOfferId(6)); // returns { offerPosition: 1, opportunityId: 3 }

However this is not pretty and I want to do that in a functional way. I've looked into Ramda, and I can find an offer when I'm looking into a single array of offers, but I can't find a way to look through the entire object => each array to find the path to my offer.

R.findIndex(R.propEq('id', offerId))(opportunitiesById[1]);

The reason I need to know the path is because I then need tho modify that offer with new data and update it back where it is.

Thanks for any help

Got The Fever Media
  • 750
  • 1
  • 9
  • 27

3 Answers3

3

You could piece it together using lots of little functions but I want to show you how to encode your intentions in a more straightforward way. This program has an added benefit that it will return immediately. Ie, it will not continue to search thru additional key/value pairs after a match is found.

Here's a way you can do it using mutual recursion. First we write findPath -

const identity = x =>
  x

const findPath =
  ( f = identity
  , o = {}
  , path = []
  ) =>
    Object (o) === o
      ? f (o) === true
        ? path
        : findPath1 (f, Object .entries (o), path)
      : undefined

If the input is an object, we pass it to the user's search function f. If the user's search function returns true, a match has been found and we return the path. If there is not match, we search each key/value pair of the object using a helper function. Otherwise, if the input is not an object, there is no match and nothing left to search, so return undefined. We write the helper, findPath1 -

const None =
  Symbol ()

const findPath1 =
  ( f = identity
  , [ [ k, v ] = [ None, None ], ...more ]
  , path = []
  ) =>
    k === None
      ? undefined
      : findPath (f, v, [ ...path, k ])
        || findPath1 (f, more, path)

If the key/value pairs have been exhausted, there is nothing left to search so return undefined. Otherwise we have a key k and a value v; append k to the path and recursively search v for a match. If there is not a match, recursively search the remaining key/values, more, using the same path.

Note the simplicity of each function. There's nothing happening except for the absolute minimum number of steps to assemble a path to the matched object. You can use it like this -

const opportunitiesById = 
  { 1:
      [ { id: 1, name: 'offer 1' }
      , { id: 2, name: 'offer 1' }
      ]
  , 2:
      [ { id: 3, name: 'offer 1' }
      , { id: 4, name: 'offer 1' }
      ]
  , 3:
      [ { id: 5, name: 'offer 1' }
      , { id: 6, name: 'offer 1' }
      ]
  }

findPath (offer => offer.id === 6, opportunitiesById)
// [ '3', '1' ]

The path returned leads us to the object we wanted to find -

opportunitiesById['3']['1']
// { id: 6, name: 'offer 1' }

We can specialize findPath to make an intuitive findByOfferId function -

const findByOfferId = (q = 0, data = {}) =>
  findPath (o => o.id === q, data)

findByOfferId (3, opportunitiesById)
// [ '2', '0' ]

opportunitiesById['2']['0']
// { id: 3, name: 'offer 1' }

Like Array.prototype.find, it returns undefined if a match is never found -

findByOfferId (99, opportunitiesById)
// undefined

Expand the snippet below to verify the results in your own browser -

const identity = x =>
  x

const None =
  Symbol ()

const findPath1 =
  ( f = identity
  , [ [ k, v ] = [ None, None ], ...more ]
  , path = []
  ) =>
    k === None
      ? undefined
      : findPath (f, v, [ ...path, k ])
        || findPath1 (f, more, path)

const findPath =
  ( f = identity
  , o = {}
  , path = []
  ) =>
    Object (o) === o
      ? f (o) === true
        ? path
        : findPath1 (f, Object .entries (o), path)
      : undefined

const findByOfferId = (q = 0, data = {}) =>
  findPath (o => o.id === q, data)

const opportunitiesById = 
  { 1:
      [ { id: 1, name: 'offer 1' }
      , { id: 2, name: 'offer 1' }
      ]
  , 2:
      [ { id: 3, name: 'offer 1' }
      , { id: 4, name: 'offer 1' }
      ]
  , 3:
      [ { id: 5, name: 'offer 1' }
      , { id: 6, name: 'offer 1' }
      ]
  }

console .log (findByOfferId (3, opportunitiesById))
// [ '2', '0' ]

console .log (opportunitiesById['2']['0'])
// { id: 3, name: 'offer 1' }

console .log (findByOfferId (99, opportunitiesById))
// undefined

In this related Q&A, I demonstrate a recursive search function that returns the matched object, rather than the path to the match. There's other useful tidbits to go along with it so I'll recommend you to give it a look.


Scott's answer inspired me to attempt an implementation using generators. We start with findPathGen -

const identity = x =>
  x

const findPathGen = function*
( f = identity
, o = {}
, path = []
)
{ if (Object (o) === o)
    if (f (o) === true)
      yield path
    else
      yield* findPathGen1 (f, Object .entries (o), path)
}

And using mutual recursion like we did last time, we call on helper findPathGen1 -

const findPathGen1 = function*
( f = identity
, entries = []
, path = []
)
{ for (const [ k, v ] of entries)
    yield* findPathGen (f, v, [ ...path, k ])
}

Finally, we can implement findPath and the specialization findByOfferId -

const first = ([ a ] = []) =>
  a

const findPath = (f = identity, o = {}) =>
  first (findPathGen (f, o))

const findByOfferId = (q = 0, data = {}) =>
  findPath (o => o.id === q, data)

It works the same -

findPath (offer => offer.id === 3, opportunitiesById)
// [ '2', '0' ]

findPath (offer => offer.id === 99, opportunitiesById)
// undefined

findByOfferId (3, opportunitiesById)
// [ '2', '0' ]

findByOfferId (99, opportunitiesById)
// undefined

And as a bonus, we can implement findAllPaths easily using Array.from -

const findAllPaths = (f = identity, o = {}) =>
  Array .from (findPathGen (f, o))

findAllPaths (o => o.id === 3 || o.id === 6, opportunitiesById)
// [ [ '2', '0' ], [ '3', '1' ] ]

Verify the results by expanding the snippet below

const identity = x =>
  x

const findPathGen = function*
( f = identity
, o = {}
, path = []
)
{ if (Object (o) === o)
    if (f (o) === true)
      yield path
    else
      yield* findPathGen1 (f, Object .entries (o), path)
}

const findPathGen1 = function*
( f = identity
, entries = []
, path = []
)
{ for (const [ k, v ] of entries)
    yield* findPathGen (f, v, [ ...path, k ])
}

const first = ([ a ] = []) =>
  a

const findPath = (f = identity, o = {}) =>
  first (findPathGen (f, o))


const findByOfferId = (q = 0, data = {}) =>
  findPath (o => o.id === q, data)

const opportunitiesById = 
  { 1:
      [ { id: 1, name: 'offer 1' }
      , { id: 2, name: 'offer 1' }
      ]
  , 2:
      [ { id: 3, name: 'offer 1' }
      , { id: 4, name: 'offer 1' }
      ]
  , 3:
      [ { id: 5, name: 'offer 1' }
      , { id: 6, name: 'offer 1' }
      ]
  }

console .log (findByOfferId (3, opportunitiesById))
// [ '2', '0' ]

console .log (findByOfferId (99, opportunitiesById))
// undefined

// --------------------------------------------------
const findAllPaths = (f = identity, o = {}) =>
  Array .from (findPathGen (f, o))

console .log (findAllPaths (o => o.id === 3 || o.id === 6, opportunitiesById))
// [ [ '2', '0' ], [ '3', '1' ] ]
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Thank you for your answer. It is very enlightening even thou it a few levels above my current understanding of functional programming. – Got The Fever Media May 10 '19 at 07:02
  • 1
    @GotTheFeverMedia if you understand function composition, transducers, and lenses, I think you should have no problem with `findPath` provided here. There's no hidden dependencies and you can work out the evaluation using simple pencil & paper method. If you have any specific questions, please ask :) – Mulan May 10 '19 at 15:52
  • @GotTheFeverMedia, a strong suggestion: if you want to learn functional techniques, study this answer carefully. There is a lot to learn from it. And then go look at some other answers from user633183. At first try to look beyond the unusual code layout to it's meaning. But then, analyze the logic behind that layout too; it's an extremely powerful style. I have learned more from her posts than from anyone else on StackOverflow. – Scott Sauyet May 12 '19 at 21:58
  • Thanks for the kinds words, Scott. @GotTheFeverMedia if there are any areas where additional explanation would help, I'd be happy to provide it. – Mulan May 13 '19 at 05:23
  • 1
    Thanks for your help guys. I am looking into it – Got The Fever Media May 13 '19 at 14:20
2

I would transform your object into pairs.

So for example transforming this:

{ 1: [{id:10}, {id:20}],
  2: [{id:11}, {id:21}] }

into that:

[ [1, [{id:10}, {id:20}]],
  [2, [{id:11}, {id:21}]] ]

Then you can iterate over that array and reduce each array of offers to the index of the offer you're looking for. Say you're looking for offer #21, the above array would become:

[ [1, -1],
  [2,  1] ]

Then you return the first tuple which second element isn't equal to -1:

[2, 1]

Here's how I'd suggest doing this:

const opportunitiesById =  {
  1: [ { id: 10, name: 'offer 1' },
       { id: 20, name: 'offer 2' } ],
  2: [ { id: 11, name: 'offer 3' },
       { id: 21, name: 'offer 4' } ],
  3: [ { id: 12, name: 'offer 5' },
       { id: 22, name: 'offer 6' } ]
};

const findOfferPath = (id, offers) =>
  pipe(
    toPairs,
    transduce(
      compose(
        map(over(lensIndex(1), findIndex(propEq('id', id)))),
        reject(pathEq([1], -1)),
        take(1)),
      concat,
      []))
    (offers);


console.log(findOfferPath(21, opportunitiesById));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {pipe, transduce, compose, map, over, lensIndex, findIndex, propEq, reject, pathEq, take, concat, toPairs} = R;</script>

Then you can take that path to modify your offer as you see fit:

const opportunitiesById =  {
  1: [ { id: 10, name: 'offer 1' },
       { id: 20, name: 'offer 2' } ],
  2: [ { id: 11, name: 'offer 3' },
       { id: 21, name: 'offer 4' } ],
  3: [ { id: 12, name: 'offer 5' },
       { id: 22, name: 'offer 6' } ]
};

const updateOffer = (path, update, offers) =>
  over(lensPath(path), assoc('name', update), offers);

console.log(updateOffer(["2", 1], '', opportunitiesById));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {over, lensPath, assoc} = R;</script>
customcommander
  • 17,580
  • 5
  • 58
  • 84
2

Here's another approach:

We start with this generator function:

function * getPaths(o, p = []) {
  yield p 
  if (Object(o) === o)
    for (let k of Object .keys (o))
      yield * getPaths (o[k], [...p, k])
} 

which can be used to find all the paths in an object:

const obj = {a: {x: 1, y: 3}, b: {c: 2, d: {x: 3}, e: {f: {x: 5, g: {x: 3}}}}}

;[...getPaths(obj)]
//~> [[], ["a"], ["a", "x"], ["a", "y"], ["b"], ["b", "c"], ["b", "d"], 
//    ["b", "d", "x"], ["b", "e"], ["b", "e", "f"], ["b", "e", "f", "x"], 
//    ["b", "e", "f", "g"], ["b", "e", "f", "g", "x"]]

and then, with this little helper function:

const path = (ps, o) => ps.reduce((o, p) => o[p] || {}, o)

we can write

const findPath = (predicate, o) =>
  [...getPaths(o)] .find (p => predicate (path (p, o) ) )

which we can call like

console.log(
  findPath (a => a.x == 3, obj)
) //~> ["b","d"]

We can then use these functions to write a simple version of your function:

const findByOfferId = (id, data) =>
  findPath (o => o.id === id, data)

const opportunitiesById =  {
  1: [ { id: 10, name: 'offer 1' }, { id: 20, name: 'offer 2' } ],
  2: [ { id: 11, name: 'offer 3' }, { id: 21, name: 'offer 4' } ],
  3: [ { id: 12, name: 'offer 5' }, { id: 22, name: 'offer 6' } ]
}

console.log(
  findByOfferId (22, opportunitiesById)
) //~> ["3", "1"]

console.log(
  findByOfferId (42, opportunitiesById)
) //~> undefined

It is trivial to extend this to get all paths for which the value satisfies the predicate, simply replacing find with filter:

const findAllPaths = (predicate, o) =>
  [...getPaths(o)] .filter (p => predicate (path(p, o) ) )

console.log(
  findAllPaths (a => a.x == 3, obj)
) //=> [["b","d"],["b","e","f","g"]]

There is a concern with all this, though. Even though findPath only needs to find the first match, and even though getPaths is a generator and hence lazy, we force the full run of it with [...getPaths(o)]. So it might be worth using this uglier, more imperative version:

const findPath = (predicate, o) => {
  let it = getPaths(o)
  let res = it.next()
  while (!res.done) {
    if (predicate (path (res.value, o) ) )
      return res.value
    res = it.next()
  }
}

This is what it looks like all together:

function * getPaths(o, p = []) {
  yield p 
  if (Object(o) === o)
    for (let k of Object .keys (o))
      yield * getPaths (o[k], [...p, k])
}

const path = (ps, o) => ps.reduce ((o, p) => o[p] || {}, o)

   
// const findPath = (pred, o) =>
//   [...getPaths(o)] .find (p => pred (path (p, o) ) )


const findPath = (predicate, o) => {
  let it = getPaths(o)
  let res = it.next()
  while (!res.done) {
    if (predicate (path (res.value, o) ) )
      return res.value
    res = it.next()
  }
}

const obj = {a: {x: 1, y: 3}, b: {c: 2, d: {x: 3}, e: {f: {x: 5, g: {x: 3}}}}}

console.log(
  findPath (a => a.x == 3, obj)
) //~> ["b","d"]

const findAllPaths = (pred, o) =>
  [...getPaths(o)] .filter (p => pred (path(p, o) ) )

console.log(
  findAllPaths (a => a.x == 3, obj)
) //~> [["b","d"],["b","e","f","g"]]


const findByOfferId = (id, data) =>
  findPath (o => o.id === id, data)

const opportunitiesById =  {
  1: [ { id: 10, name: 'offer 1' }, { id: 20, name: 'offer 2' } ],
  2: [ { id: 11, name: 'offer 3' }, { id: 21, name: 'offer 4' } ],
  3: [ { id: 12, name: 'offer 5' }, { id: 22, name: 'offer 6' } ]
}

console.log(
  findByOfferId (22, opportunitiesById)
) //~> ["3", "1"]

console.log(
  findByOfferId (42, opportunitiesById)
) //~> undefined

Another brief note: the order in which the paths are generated is only one possibility. If you want to change from pre-order to post-order, you can move the yield p line in getPaths from the first line to the last one.


Finally, you asked about doing this with functional techniques, and mentioned Ramda. As the solution from customcommander shows, you can do this with Ramda. And the (excellent as always) answer from user633183 demonstrates, it's possible to do this with mainly functional techniques.

I still find this a somewhat simpler approach. Kudos to customcommander for finding a Ramda version, because Ramda is not particularly well-suited for recursive tasks, but still the obvious approach to something that has to visit the nodes of a recursive structure like a JS object is to use a recursive algorithm. I'm one of the authors of Ramda, and I haven't even tried to understand how that solution works.

Update

user633183 pointed out that this would be simpler, and still lazy:

const findPath = (predicate, o) => {
  for (const p of getPaths(o)) 
    if (predicate (path (p, o)) ) 
      return p
}
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • The process is easy to follow thanks to your great explanation. Inspired by your work, I added a generator implementation to my answer :D – Mulan May 11 '19 at 21:50
  • There is one weakness with `[...getPaths(o)].find` as all paths are eagerly calculated before `find` can start. A solution compatible with the rest of your program might be to make a `find` function that operates on the generator instead of using Array's `find` – Mulan May 11 '19 at 21:53
  • @user633183: There is a version of `getPaths` included that's meant to get around this, but you're right that an adjusted `find` would be a better alternative. – Scott Sauyet May 11 '19 at 22:38
  • Nice updates, Scott. Is there a particular reason you're handling the iterator manually instead of using `for (const p of getPaths(o)) ...`? – Mulan May 12 '19 at 06:00
  • @user633183: No updates since your previous comment, but I will now that you've pointed out that I can use `for-of`. I hadn't tried, but was under the impression that it was also eager. I just tested, and you're right that it would make for a cleaner implementation. – Scott Sauyet May 12 '19 at 16:15
  • 1
    The updated implementation looks super clean. I also learned that destructuring assignment can be used to get values out of a generator lazily so long as you're not using a rest pattern, which causes the iterator to step until its exhausted. I use this technique in `first` in my answer, which gets the first value from an iterable but ignores the rest. This effectively pauses the iterator after the first value comes out. Always a pleasure collaborating with you. Cheers. – Mulan May 12 '19 at 17:26