9

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']
  • How one and two are returned while providing 1,3 in the slice method call? – Mahmood Jun 20 '17 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. – Caspar Harmer Jun 20 '17 at 23:05
  • 1
    @Mahmood the numbers are start and end values for the array – Caspar Harmer Jun 20 '17 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!! – Caspar Harmer Jun 20 '17 at 23:53
12

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)
| improve this answer | |
  • 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 '17 at 23:00
  • I'm getting the same input/output combos as the OP and linked documentation. – Adrian Larson Jun 20 '17 at 23:01
  • 1
    Yours was a little quicker than @sfdcfox 's - both were better than mine. – Caspar Harmer Jun 20 '17 at 23:54
  • The documentation allows for negative indexes, which specify an offset from the end rather than the beginning of the list. – IllusiveBrian Jun 20 '17 at 23:57
  • @AdrianLarson What about slice(data, -3, -1)? – sfdcfox Jun 21 '17 at 0:29
7

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.

| improve this answer | |
  • @AdrianLarson can't decide which version I like best, so I'm going to time it. – Caspar Harmer Jun 20 '17 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 – Caspar Harmer Jun 21 '17 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 '17 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 '18 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 '18 at 17:33
5

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)
| improve this answer | |
  • 1
    That's a great answer - thanks for adding it martin! Very clean and encapusulated. – Caspar Harmer Jun 21 '17 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.