11

Sometimes I like to use a Queueable to perform work on a list of Ids or sObjects over the heaver Batchable Interface.

However, to do this, we need a very solid implementation of an Array slice method.

Most other programming languages have a native slice method, but Apex does not.

While I have written my own version of slice and it's working fine, I would be interested in hearing about other implementations of slice - the more efficient the better.

My version is in this Utilities class on github here

It's modelled after the Javascript version described on the mozilla site here

I would expect that the usage and output be similar to this:

String[] arr = ['zero', 'one', 'two', 'three'];
String[] sliced = Utilities.slice(arr,1, 3);

System.debug(arr);      // ['zero', 'one', 'two', 'three']
System.debug(sliced);   // ['one', 'two']
4
  • How one and two are returned while providing 1,3 in the slice method call?
    – Mahmood
    Jun 20, 2017 at 22:40
  • Guys thanks for giving this some though - both of these techniques look very good - fox's for it's much safer implementation and adrian's for taking a different approach. I'm having trouble deciding who I should award the accept to. Jun 20, 2017 at 23:05
  • 1
    @Mahmood the numbers are start and end values for the array Jun 20, 2017 at 23:10
  • Ok the results are in! sfdcfox - I added a small change to your array temp[i-startIndex] = ary[i]; adrian - I added bounds checking so that it was compariable with sfdcfox's version (not that this mattered really, the time is all in the loop anyway. Adrian median time was about 15ms and Sfdc median time was 23ms. Thanks again!! Jun 20, 2017 at 23:53

3 Answers 3

17

Here's a pattern that seems relatively efficient and easier to understand.

public static List<Object> slice(List<Object> input, Integer ge, Integer l)
{
    List<Object> output = input.clone();
    for (Integer i = 0; i < ge; i++) output.remove(0);
    Integer elements = l - ge;
    while (output.size() > elements) output.remove(elements);
    return output;
}

If you want to support negative index, just add these lines at the beginning of the method:

if (ge < 0) ge += input.size();
if (l < 0) l += input.size();

And running it through this script yields the proper input/output combos:

List<String> data = new List<String> { 'Banana', 'Orange', 'Lemon', 'Apple', 'Mango' };
List<String> sliced = (List<String>)slice(data, 1, 3);

system.debug(data); // (Banana, Orange, Lemon, Apple, Mango)
system.debug(sliced); // (Orange, Lemon)

List<String> data = new List<String> { 'zero', 'one', 'two', 'three' };
List<String> sliced = (List<String>)slice(data, 1, 3);

system.debug(data); // (zero, one, two, three)
system.debug(sliced); // (one, two)
6
  • This version assumes that ge and l are within legal ranges, and also doesn't act like the JavaScript version it was meant to emulate.
    – sfdcfox
    Jun 20, 2017 at 23:00
  • I'm getting the same input/output combos as the OP and linked documentation.
    – Adrian Larson
    Jun 20, 2017 at 23:01
  • 1
    Yours was a little quicker than @sfdcfox 's - both were better than mine. Jun 20, 2017 at 23:54
  • The documentation allows for negative indexes, which specify an offset from the end rather than the beginning of the list. Jun 20, 2017 at 23:57
  • @AdrianLarson What about slice(data, -3, -1)?
    – sfdcfox
    Jun 21, 2017 at 0:29
11

While it's not a bad start, I can think of a number of problems:

  • No bounds checks are provided
  • Only Id and SObject are supported
  • SObject version returns generic list, does not support upsert
  • Has debug statements
  • Is overly verbose
  • Thrashes the heap more than necessary

Here's a version that closer to what I'd include in a library (which I'm working on), but feel free to study and borrow this code.

// Edit: now behaves better in more cases
public static Object[] slice(Object[] ary, Integer first, Integer last) {
    Object[] res = ary.clone(), temp;
    Integer size = ary.size(),
        startIndex = Math.min(size, Math.max(-1, first<0? size+first: first)),
        endIndex = Math.min(size, Math.max(-1, last<0? size+last: last-1)),
        offset = Math.max(-1, endIndex-startIndex);
    temp = new Object[offset+1];
    for(Integer h = 0, i = startIndex, j = endIndex; i <= j; ) {
        temp[h++] = ary[i++];
    }
    res.clear();
    res.addAll(temp);
    return res;
}

This version supports all generic lists, not just Id or SObject, returns a list of the same type as the original (so supports upsert), and has very similar behavior to JavaScript.

5
  • @AdrianLarson can't decide which version I like best, so I'm going to time it. Jun 20, 2017 at 23:07
  • It'd be nice to see exactly where Adrian is getting his speed gains - all I can currently see is the overall script time. I suppose its the internal efficiency of the built in remove method that beats our array element dereferencing technique Jun 21, 2017 at 1:02
  • @CasparHarmer My version behaves "more" like splice (handles negative values more or less as specified), but it is still glitchy. Adrian's version does a bit more heap allocation but less checking. It's really just a matter of how close you need the behavior to be. Without the negative indexes, I'd expect my version to be at least as fast.
    – sfdcfox
    Jun 21, 2017 at 1:11
  • @sfdcfox I don't think your answer works at all. Try slice(arr, 1, 2) and you'll get out of bounds exception... Also, it seems to start from 1, not 0 (I don't think that's how usual s(p)lice works) :|
    – dzh
    Sep 25, 2018 at 5:00
  • @dzh You're right, it was more back-of-a-napkin implementation. However, since you've made mention, I've (hopefully) addressed all of the issues. It should now be a lot closer to the intended behavior and removes the out-of-bound errors.
    – sfdcfox
    Sep 25, 2018 at 17:33
8

There's already some good answers here, but I figured I'd throw my hat into the ring anyways.

In my approach, I've tried to stay as close to the Javascript versions as possible.

The JS slice is a variable arity function that makes use of up to the first two arguments it is provided, so I provided three methods for my Slice class with 0, 1, and 2 parameters respectively.

Also, JS doesn't have integers, and all numbers are floating point, so I defined my method parameters as Decimals, and rounded with them the CEILING round mode; meaning that -0.5 is taken to be 0.

I wanted to name the parameters begin and end to stay inline with the mdn page, but those are unfortunately reserved (for future use) keywords.

I also went with a more OO approach instead of using static utility methods, but that should be easy enough to change if you want to do it the other way instead.

public class Slice {

    private list<Object> olist;

    public Slice(list<Object> olist){
        this.olist = olist;
    }   

    public list<Object> slice(){
        return olist.clone();
    }   

    public list<Object> slice(Decimal x_begin){
        Integer start = x_begin == NULL ? 0 : (Integer) x_begin.round(System.RoundingMode.CEILING);
        return commonSlice(start,olist.size());
    }   

    public list<Object> slice(Decimal x_begin, Decimal x_end){
        Integer start  = x_begin == NULL ? 0            : (Integer) x_begin.round(System.RoundingMode.CEILING);
        Integer finish = x_end   == NULL ? olist.size() : (Integer) x_end.round(System.RoundingMode.CEILING);
        return commonSlice(start, finish);
    }   

    private list<Object> commonSlice(Integer x_begin, Integer x_end){
        list<Object> ret = new list<Object>();
        x_begin = x_begin < 0 ? olist.size() + x_begin : x_begin ;
        x_end   = x_end   < 0 ? olist.size() + x_end   : x_end   ;   
        Integer maxIndex = olist.size();
        while (x_begin < maxIndex && x_begin < x_end){
            ret.add(olist[x_begin]);
            x_begin++;
        }
        return ret;
    }   


}

Some examples:

list<Integer> ilist = new list<Integer>{1,2,3,4,5};
Slice s = new Slice(ilist);
system.debug(s.slice());       // (1, 2, 3, 4, 5)
system.debug(s.slice(-.5,20)); // (1, 2, 3, 4, 5)
system.debug(s.slice(.5));     // (2, 3, 4, 5)
system.debug(s.slice(1));      // (2, 3, 4, 5)
system.debug(s.slice(2,-2));   // (3)
system.debug(s.slice(2,-3));   // ()
system.debug(s.slice(7,100));  // ()
system.debug(s.slice(NULL,2)); // (1, 2)
1
  • 1
    That's a great answer - thanks for adding it martin! Very clean and encapusulated. Jun 21, 2017 at 6:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.