JSONata: Remove first occurrence of item in array

921 Views Asked by At

I want to remove the first occurrence of an element in an array in JSONata.

I tried this:

(
    $largerArray := ["a","b","c","a","b","c"];
    $itemToExclude := "a";
    $expectedArray := ["b","c","a","b","c"];
    
    $doesNotWorkBecauseVariablesAreLocal := function($array, $element){(
        $found := false;
        $filter($array, function($v){(
            $isElem := $v = $element;
            $shouldDrop := $isElem and $not($found);
            $found := $found or $isElem;
            $not($shouldDrop)
        )})
    )};

    $doesNotWorkBecauseVariablesAreLocal($largerArray,"a");

    $reduced := $reduce($largerArray, function($acc,$arrayItem){(
        $array := $lookup($acc, "array");
        $foundAlready := $lookup($acc,"foundAlready");
        $itemToExclude := $lookup($acc,"itemToExclude");
        $shouldExclude := $arrayItem = $itemToExclude and $not($foundAlready);
        $foundAlready := $foundAlready or $shouldExclude;
        $shouldExclude ? {"array":$array, "foundAlready":$foundAlready, "itemToExclude":$itemToExclude} : {"array":$append($array,$arrayItem), "foundAlready":$foundAlready, "itemToExclude":$itemToExclude}
    )}, {"array":[], "foundAlready":false, "itemToExclude":$itemToExclude});

    $reduced.array;
)

doesNotWorkBecauseVariablesAreLocal does not work because the found variable is locally scoped. So I guess the first part of my question is: could this kind of approach be made to work?

The fact that it didn't seem to work made me try the second approach, of carrying state through an object that contains the array, the foundAlready boolean and the itemToExclude

It works, but it isn't elegant.

What is the idiomatic JSONata way to pull this off?

2

There are 2 best solutions below

0
On BEST ANSWER

You can use $reduce (https://docs.jsonata.org/higher-order-functions#reduce) to find the first occurrence of an item. $reduce iterates over an array and passes an accumulator (the first index) as well as the current index and value. We can initialize the accumulator to -1 and if it is still -1 and the value matches the search, we return the index as the new accumulator value.

Once we have the index, we can use $filter (https://docs.jsonata.org/higher-order-functions#filter) to remove that index from the array:

(
    $largerArray := ["a","b","c","a","b","c"];
    $itemToExclude := "b";
    $expectedArray := ["b","c","a","b","c"];
    

    $dropFirst := function($array,$item){(
        $index := $reduce($array, function($accumulator, $value, $index){ $value = $item and $accumulator = -1 ? $index : $accumulator }, -1 );
        $filter($array, function($v, $i){$i != $index ? $v})
    )};

    $dropFirst($largerArray, $itemToExclude)
)
0
On

You're right that there seems to be no way to modify the $found variable that's from the outer-scope. That's a bummer!

You were on the right track with your reduce-based alternative. @aeberhart beat me to it, but I came up with a very similar solution:

(
  $largerArray := ["a","b","c","a","b","c"];

  $findFirstOccurenceIndex := function($array, $itemToFind) {
    $reduce($array, function($acc, $item, $index) {
      (
        $isItemToFind := $item = $itemToFind;
        $acc != null ? $acc : $isItemToFind ? $index : null
      )
      },
      null
    )
  };

  $firstOccurence := $findFirstOccurenceIndex($largerArray, "a");
  
  $filter($largerArray, function($item, $index) { $index != $firstOccurence })
)

If you want to play around with it, here's a link to it in the JSONata Playground.