0

I have JSON something like:

[
  {
    "property": "Foo",
    "address": "Foo Address",
    "debitNote": [
      {
        "title": "Debit A Foo",
        "desc": "Desc Debit Foo",
        "listDebit": [
          {
            "id": "IP-1A1",
            "amount": "273000"
          },
          {
            "id": "IP-1A2",
            "amount": "389000"
          },
          {
            "id": "IP-1A3",
            "amount": "382000"
          },
          {
            "id": "IP-1A4",
            "amount": "893000"
          },
          {
            "id": "IP-1A5",
            "amount": "1923000"
          }
        ]
      },
      {
        "title": "Debit B Foo",
        "desc": "Desc Debit B Foo",
        "listDebit": [
          {
            "id": "IP-1B1",
            "amount": "120000"
          },
          {
            "id": "IP-1B2",
            "amount": "192000"
          }
        ]
      }
    ]
  },
  {
    "property": "Bar",
    "address": "Address Bar",
    "debitNote": [
      {
        "title": "Debit A Bar",
        "desc": "Desc Bar",
        "listDebit": [
          {
            "id": "IP-1C1",
            "amount": "893000"
          },
          {
            "id": "IP-1C2",
            "amount": "1923000"
          }
        ]
      },
      {
        "title": "Debit B Bar",
        "desc": "Desc Debit B Bar",
        "listDebit": [
          {
            "id": "IP-1D1",
            "amount": "192000"
          }
        ]
      }
    ]
  }
]

I need to fix 2 problem from those json:

  1. check if value is match with id inside JSON
  2. remove element if id of JSON is found by value and the id index is the latest index

I can handle the 1st task with current code:

let json = [{"property":"Foo","address":"Foo Address","debitNote":[{"title":"Debit A Foo","desc":"Desc Debit Foo","listDebit":[{"id":"IP-1A1","amount":"273000"},{"id":"IP-1A2","amount":"389000"},{"id":"IP-1A3","amount":"382000"},{"id":"IP-1A4","amount":"893000"},{"id":"IP-1A5","amount":"1923000"}]},{"title":"Debit B Foo","desc":"Desc Debit B Foo","listDebit":[{"id":"IP-1B1","amount":"120000"},{"id":"IP-1B2","amount":"192000"}]}]},{"property":"Bar","address":"Address Bar","debitNote":[{"title":"Debit A Bar","desc":"Desc Bar","listDebit":[{"id":"IP-1C1","amount":"893000"},{"id":"IP-1C2","amount":"1923000"}]},{"title":"Debit B Bar","desc":"Desc Debit B Bar","listDebit":[{"id":"IP-1D1","amount":"192000"}]}]}]

function findID(array, value){
  return array.some((item)=>{
    if(item.id == value) {
      return true
    }
    return Object.keys(item).some(x=>{
      if(Array.isArray(item[x])) return findID(item[x], value)
    })
  })
}
  
console.log(findID(json, 'IP-1D1'))
console.log(findID(json, 'IP-1A1'))
console.log(findID(json, 'IP-1B1'))
console.log(findID(json, 'IP-1Z1'))

But I'm stuck with the 2nd task with the expected result as follow:

console.log(removeID(json, 'IP-1A5')) // {"id": "IP-1A5", "amount": "1923000"} removed
console.log(removeID(json, 'IP-1A3')) // couldn't remove, because there's {"id": "IP-1A4", "amount": "893000"} after id 'IP-1A3'

anyone suggestion to fix my 2nd problem?

6
  • You need to be more specific about what constitutes "latest". Is it just whatever number starts at index 5, or something more complex? Commented Jun 3, 2021 at 2:36
  • How much deep the nesting level will be? Is it that shown in the question or it can go deeper? Commented Jun 3, 2021 at 2:37
  • @Tibrogargan, since the id is not guaranteed incremental, i can say it just whatever number of id Commented Jun 3, 2021 at 3:26
  • @decpk currently the array nesting deep is like what JSON created Commented Jun 3, 2021 at 3:28
  • That doesn't help. You need to supply some well defined mechanism to determine what is latest? Obviously the "id" is actually two (or more) pieces of information compounded together to produce an identifier, but what's the formula? Is it always a 5 letter prefix followed by a number, or could it be "anything followed by - followed by some numbers followed by one or more letters followed by a number". i.e ABCDEFG13854761324-7864784JHKFLKHFJG0 could be legitimate Commented Jun 3, 2021 at 3:58

3 Answers 3

2

It's still not clear what you mean by the latest id. But we might be able to break the problem down so that decision is isolated from the rest of the logic.

We can start to write a generic traversal of a Javascript structure with arbitrary nesting of arrays and objects, one that builds a new object according to whether a given node matches a test we supply, and then build on that one which removes elements matching a given id. It might look something like this:

const deepFilter = (pred) => (o) =>
  Array .isArray (o)
    ? o .filter (x => pred (x)) .map (deepFilter (pred))
  : Object (o) === o
    ? Object .fromEntries (
        Object .entries (o) .flatMap (([k , v]) => pred (v) ? [[k, deepFilter (pred) (v)]] : [])
      ) 
  : o

const deepReject = (pred) =>
  deepFilter ((x) => ! pred (x))

const removeById = (target) => deepReject (({id}) => target == id)

deepFilter accepts a predicate function and returns a function which accepts such a nested object/array, and returns a copy of it including the nodes which match that predicate.

deepReject is built atop deepFilter, simply reversing the sense of the predicate.

And removeById is built atop deepReject, accepting a target id and returning a function that takes an object, returning a copy of it without those nodes whose ids match that target.

Now, if we had a function (lastMatch) to tell us whether an id was the last matching one in our input, we could write our main function like this:

const removeIdIfLast = (id) => (input) => 
  lastMatch (id) (input)
    ? removeById (id) (input)
    : input

I'm going to take a guess at what "the latest index" might mean. But be warned, the rest of this answer involves a guess that might well be wrong.

I will assume that for an id such as 'IP-1A4', the sense of "last" has to do with that final 4, that, say, 'IP-1A1' and 'IP-1A3' are before it, and 'IP-1A5' and 'IP-1A101' are after it., but that we only compare to ids with prefix IP-1A.

So, again wishing we had another function at our disposal (matchingIds), we can write a lastMatch function that finds the maximum id by sorting the ids from the document that match our target, then sorting those descending by extracting the numeric parts and subtracting, then taking the first element of the sorted list. (Sorting is not the most efficient way to find a maximum. If those lists are likely to be long, you might want to write a more efficient version of this function; it's not at all hard, but it would take us afield.)

const lastMatch = (id) => (input) =>
  id == matchingIds (id) (input) .sort (
    (a, b, x = a .match (/.+(\d+)$/) [1], y = b .match (/.+(\d+)$/) [1]) => y - x
  ) [0]

We could write matchingIds based on this idea, if we only had a function to gather all the ids in our data. It might look like this:

const matchingIds = (id, prefix = id .match (/(.+)\d+$/) [1]) => (input) =>
  findIds (input) .filter (id => id .match (/(.+)\d+$/) [1] == prefix)

This is making the assumption (quite possibly unwarranted) that all ids in the input have this something-ending-in-a-string-of-digits structure. There might well need to be a more sophisticated version of this if that assumption is not correct.

So how would we write findIds? Well, a straightforward approach would mimic the design of deepFilter to look like this:

const findIds = (o) => 
  Array.isArray (o)
    ? o .flatMap (findIds)
  : Object (o) === o
    ? Object .entries (o) .flatMap (([k, v]) => k == 'id' ? [v, ...findIds (v)] : findIds (v))
  : []

But I see a useful general-purpose abstraction lurking in that function, so I might build it on that, in this manner:

const fetchAll = (prop) => (o) => 
  Array.isArray (o)
    ? o .flatMap (findIds)
  : Object (o) === o
    ? Object .entries (o) .flatMap (
        ([k, v]) => k == prop ? [v, ... fetchAll (prop) (v)] : fetchAll (prop) (v)
      )
  : []

const findIds = fetchAll ('id')

Putting all this together, here is a solution, based on the assumptions above:

const deepFilter = (pred) => (o) =>
  Array .isArray (o)
    ? o .filter (x => pred (x)) .map (deepFilter (pred))
  : Object (o) === o
    ? Object .fromEntries (
        Object .entries (o) .flatMap (([k , v]) => pred (v) ? [[k, deepFilter (pred) (v)]] : [])
      ) 
  : o

const deepReject = (pred) =>
  deepFilter ((...args) => ! pred (...args))

const removeById = (target) => deepReject (({id}) => target == id)

const fetchAll = (prop) => (o) => 
  Array.isArray (o)
    ? o .flatMap (findIds)
  : Object (o) === o
    ? Object .entries (o) .flatMap (([k, v]) => k == prop ? [v, ... fetchAll (prop) (v)] : fetchAll (prop) (v))
  : []

const findIds = fetchAll ('id')

const matchingIds = (id, prefix = id .match (/(.+)\d+$/) [1]) => (input) =>
  findIds (input) .filter (id => id .match (/(.+)\d+$/) [1] == prefix)
  
const lastMatch = (id) => (input) =>
  id == matchingIds (id) (input) .sort (
    (a, b, x = a .match (/.+(\d+)$/) [1], y = b .match (/.+(\d+)$/) [1]) => y - x
  ) [0]

const removeIdIfLast = (id) => (input) => 
  lastMatch (id) (input)
    ? removeById (id) (input)
    : input

const input = [{property: "Foo", address: "Foo Address", debitNote: [{title: "Debit A Foo", desc: "Desc Debit Foo", listDebit: [{id: "IP-1A1", amount: "273000"}, {id: "IP-1A2", amount: "389000"}, {id: "IP-1A3", amount: "382000"}, {id: "IP-1A4", amount: "893000"}, {id: "IP-1A5", amount: "1923000"}]}, {title: "Debit B Foo", desc: "Desc Debit B Foo", listDebit: [{id: "IP-1B1", amount: "120000"}, {id: "IP-1B2", amount: "192000"}]}]}, {property: "Bar", address: "Address Bar", debitNote: [{title: "Debit A Bar", desc: "Desc Bar", listDebit: [{id: "IP-1C1", amount: "893000"}, {id: "IP-1C2", amount: "1923000"}]}, {title: "Debit B Bar", desc: "Desc Debit B Bar", listDebit: [{id: "IP-1D1", amount: "192000"}]}]}]


console .log (
  'Removing IP-1A4 does nothing:',
  removeIdIfLast ('IP-1A4') (input)
)
console .log (
  'Removing IP-1A5 succeeds:',
  removeIdIfLast ('IP-1A5') (input)
)
.as-console-wrapper {max-height: 100% !important; top: 0}

Sign up to request clarification or add additional context in comments.

Comments

1
// Get the id's path
function findPath(array, val) {
  for (let i = 0; i < array.length; i++) {
    const item = array[i];
    if (item.id !== val) {
      const x = Object.keys(item).find(x => Array.isArray(item[x]))
      if (x) {
        const path = findPath(item[x], val);
        if (path) {
          path.unshift(x);
          path.unshift(i);
          return path
        }
      }
    } else {
      return [i];
    }
  }
}
// Use the path to remove the item from json
function removeLatestIndexId(path, json) {
  const result = path.slice(0, -1).reduce((pre, next) => {
    return pre[next]
  }, json);
  const idIndex = path[path.length - 1]
  if (idIndex === result.length - 1) {
    result.splice(idIndex, 1)
  }
}

https://codepen.io/shuizy/pen/bGqLXoQ

You can use findPath to get the id's path inside json, and use that path to help you solve problem #2. (actually if you have a path you can also solve problem #1)

Comments

-1
function removeID(array, value) {
  for (let dn of json.map((node) => node.debitNote)) {
    for (let lb of dn.map((n) => n.listDebit)) {
      for (let i = 0; i < lb.length; i++) {
        if (lb[i].id === value) {
          return lb.splice(i, 1);
        }
      }
    }
  }

  return {};
}

https://codepen.io/chriswong979/pen/bGqLZOL?editors=1111

1 Comment

Explaining what your code does would be nice, it looks cryptic at first glance, using both for loops and functional constructs like map.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.