Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

$indexOf function for array enhancement request #187

Closed
wnm3 opened this issue Apr 4, 2018 · 5 comments · Fixed by #371
Closed

$indexOf function for array enhancement request #187

wnm3 opened this issue Apr 4, 2018 · 5 comments · Fixed by #371

Comments

@wnm3
Copy link

wnm3 commented Apr 4, 2018

I wanted to leverage the location of elements in an array to do something, but couldn't figure out how to get the index of a particular array element matching search criteria. I would like to see something like:

$indexOf(Account.Order[OrderID="order103"])
or
Account.Order.indexOf(OrderID="order103")
or
Account.Order[OrderID="order103"].index

Above are listed in order of preference but would be happy with any of them.
Also posted here if you'd prefer I just use a function:
https://stackoverflow.com/questions/49655474/how-to-get-index-of-element-in-array-in-jsonata

@wnm3
Copy link
Author

wnm3 commented Apr 4, 2018

These additions may suffice:

    staticFrame.bind('indexOf', defineFunction(functionIndexOf, '<ax:n>'));

    /**
     * IndexOf function
     * @param {Array} arg1 - array to be searched
     * @param {Object} arg2 - object to search for
     */
    function functionIndexOf(arg1, arg2) {
        if(typeof arg1 === 'undefined') {
          return -1;
        }
        if (typeof arg2 === 'undefined') {
           return -1;
        }
        if (!Array.isArray(arg1)) {
           return -1;
        }
        return arg1.indexOf(arg2);
    }

@xogeny
Copy link
Contributor

xogeny commented Apr 4, 2018

There are a couple of problems here. First, $indexOf will find the first match, but it won't necessarily be the same index as you would get if you were iterating over them. But more importantly, using the built-in indexOf means that it will do === comparison. As far as I can tell, nothing in the semantics of JSONata ensure that an implementation won't clone objects for some internal reason (e.g., to ensure immutability when passing objects into foreign functions. Finally, I think this will be an issue if you have multiple matches in your predicate. Consider the following input:

[
  {"firstName": "Bill", "lastName": "Williams"},
  {"firstName": "Bill", "lastName": "Smith"}
]

If my JSONata expression is $[firstName="Bill"], then my result will be:

[
  {
    "firstName": "Bill",
    "lastName": "Williams"
  },
  {
    "firstName": "Bill",
    "lastName": "Smith"
  }
]

But, if you do $indexOf($[OrderID="order103"]) you are going to get -1. That is because the result of $[firstName="Bill"] is an array which won't match any element in your original input.

What I think you want is to record the indices and then do something with them, e.g., this contrived example:

$[$indexOf($, firstName="Bill")].lastName

In this case, the $indexOf function is behaving like your middle example. It goes takes both the array of interest, $, and a predicate expression, firstName="Bill" and returns an array of integers. If such a function existed, then the $[...] would, in fact, extract the elements in $ because the predicate expression is an array of integers (see #154).

But here is the rub. In order to implement $indexOf, you'd need the ability to create macros. This is because we actually want to pass the abstract syntax tree (of the predicate expression) into the function which (as far as I know) isn't currently possible. We need this in order to defer evaluation of the predicate expression.

You are pointing out an interesting quirk in JSONata. Although a predicate expression can be used to select elements from an array, there doesn't seem to be a way to actually evaluate it to get the indices. So my analysis is that you'd either need a way to add user macros or it would have to be implemented as a builtin operator. So, for example, let's say that the predicate expression was preceded by a # (currently unused as a unary operator in JSONata), you could define that to mean "return the indices that this predicate expression matches rather than the values it matches". Then the solution to your problem could be expressed as:

Account.Order#[OrderID="order103"].index

But again, this doesn't exist at the moment. I'm just trying to explain why, as far as I can tell, it cannot achieved by binding a function. It is more integral than that.

But perhaps @andrew-coleman sees a way around this...

@andrew-coleman
Copy link
Member

@wnm3 I think this is related to this enhancement request: #102

@wnm3
Copy link
Author

wnm3 commented Apr 5, 2018

Yes, it is related. Thank you.
In my case, I have a mapping of array entries to another array so finding a value in one and returning the index allows me to directly access the other. In truth, I'm wanting to use the index as the channel selector in Node-RED. In my case, I was considering a call like:
$indexOf(myArray, myArray[key=searchVal]) so I would be able to leverage === for the comparison but would only expect the first match index to be returned as is done with Array.indexOf.

I could use your approach with # if that is the desired direction. In my case, I doubt we could use the macro as we have only a single String holding what we want JSONata to evaluate as a "rule".

I was also thinking something modeled after JavaScript Array.findIndex where we would pass a filter function might work well. The $filter method seems to take an array and a function -- perhaps just a copy of $filter that returns the indices of the cells that match the comparator function?

I hope we can find some way to provide access to indices. Thanks for the detailed feedback.

@xogeny
Copy link
Contributor

xogeny commented Apr 5, 2018

Note, the # thing was just to demonstrate that it could be done using a special operator and syntax, not that it should. That is really @andrew-coleman's call. I was just trying to explain the issues that I saw.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants