Slice List String Etc

From Second Life Wiki
Revision as of 17:02, 24 January 2015 by ObviousAltIsObvious Resident (talk | contribs) (<lsl> tag to <source>)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Slice List String Etc.

The functions here return empty lists and strings when expected, while slicing lists or strings.

Introduction

LSL defines llList2List to slice lists of llGetListLength entries and LSL defines llGetSubString to slice strings of llStringLength chars.

Some people write scripts that expect llList2List to return an empty list more often than it does. In a couple of boundary test cases, llList2List functions exactly as specified, thus astonishingly returns a list of all entries or a list of the first entry, rather than the expected empty list. In exactly the same, way llGetSubString sometimes astonishes people by returning a list of all chars or a list of the first char, in these boundary test cases where some people erroneously expect the empty string.

The example slicing functions here more often just do the right thing, returning empty lists and strings when expected. Page down below the code to see a brief informal proof of this advantage, cribbed here from the design of the Python scripting language, if those detailed mathematics interest you.

Code to slice zero or more entries from a list

// Return the entries between the first index and the lastPlus index.
// cf. http://wiki.secondlife.com/wiki/Slice_List_String_Etc
// cf. http://www.google.com/search?q=site:docs.python.org+slice

list listGetBetween(list entries, integer first, integer lastPlus)
{
    
    // Count negative indices back from beyond, stopping at zero
    
    integer beyond = llGetListLength(entries);
    if (first < 0) { first += beyond; if (first < 0) { first = 0; } }
    if (lastPlus < 0) { lastPlus += beyond; if (lastPlus < 0) { lastPlus = 0; } }
    
    // Slice if indices nonnegative and strictly ordered
    
    if (first < lastPlus) // implies && (1 <= lastPlus)
    {
        return llList2List(entries, first, lastPlus - 1);
    }
    
    // Else return the empty list
    
    return [];
}

// Demo

default
{
    state_entry()
    {
        list entries = ["a", "b", "c", "d", "e", "f", "g", "h"];
        
        llOwnerSay(llList2CSV(listGetBetween(entries, 0, 0))); // no entries
        llOwnerSay(llList2CSV(listGetBetween(entries, 0, llGetListLength(entries)))); // all entries
        
        llOwnerSay(llList2CSV(listGetBetween(entries, 0, 6))); // first six
        llOwnerSay(llList2CSV(listGetBetween(entries, 6, llGetListLength(entries)))); // all but first six
        
        llOwnerSay(llList2CSV(listGetBetween(entries, 0, -4))); // all but last four
        llOwnerSay(llList2CSV(listGetBetween(entries, -4, llGetListLength(entries)))); // last four
        
        llOwnerSay(llList2CSV(listGetBetween(entries, 0, 3))); // entries before the fourth char
        llOwnerSay(llList2CSV(listGetBetween(entries, 3, 4))); // the fourth char
        llOwnerSay(llList2CSV(listGetBetween(entries, 4, llGetListLength(entries)))); // the entries after
        
        llOwnerSay(llList2CSV(llList2List(entries, 0, 0))); // never empty, sometimes astonishingly so
        llOwnerSay(llList2CSV(llList2List(entries, llGetListLength(entries), llGetListLength(entries) - 1)));
        
        llOwnerSay("OK");
    }
}

Code to slice zero or more chars from a string

// Return the chars between the first index and the lastPlus index.
// cf. http://wiki.secondlife.com/wiki/Slice_List_String_Etc
// cf. http://www.google.com/search?q=site:docs.python.org+slice

string stringGetBetween(string chars, integer first, integer lastPlus)
{
    
    // Count negative indices back from beyond, stopping at zero
    
    integer beyond = llStringLength(chars);
    if (first < 0) { first += beyond; if (first < 0) { first = 0; } }
    if (lastPlus < 0) { lastPlus += beyond; if (lastPlus < 0) { lastPlus = 0; } }
    
    // Slice if indices nonnegative and strictly ordered
    
    if (first < lastPlus) // implies && (1 <= lastPlus)
    {
        return llGetSubString(chars, first, lastPlus - 1);
    }
    
    // Else return the empty string
    
    return "";
}

// Demo

default
{
    state_entry()
    {
        string qu = "\"";
        string chars = "abcdefgh";
        
        llOwnerSay(qu + stringGetBetween(chars, 0, 0) + qu); // no chars
        llOwnerSay(qu + stringGetBetween(chars, 0, llStringLength(chars)) + qu); // all chars
        
        llOwnerSay(qu + stringGetBetween(chars, 0, 6) + qu); // first six
        llOwnerSay(qu + stringGetBetween(chars, 6, llStringLength(chars)) + qu); // all but first six
        
        llOwnerSay(qu + stringGetBetween(chars, 0, -4) + qu); // all but last four
        llOwnerSay(qu + stringGetBetween(chars, -4, llStringLength(chars)) + qu); // last four
        
        llOwnerSay(qu + stringGetBetween(chars, 0, 3) + qu); // chars before the fourth char
        llOwnerSay(qu + stringGetBetween(chars, 3, 4) + qu); // the fourth char
        llOwnerSay(qu + stringGetBetween(chars, 4, llStringLength(chars)) + qu); // the chars after

        llOwnerSay(qu + llGetSubString(chars, 0, 0) + qu); // never empty, sometimes astonishingly so
        llOwnerSay(qu + llGetSubString(chars, llStringLength(chars), llStringLength(chars) - 1) + qu);
        
        llOwnerSay("OK");
    }
}

Brief informal proof of the natural usability advantage these slicing functions hold over the LSL slicing functions

Four steps of human logic:

1. Focus on the small space of test cases most commonly found in actual scripts that slice lists or strings:

  • Let First be the index of the first entry or char that you do want, let LastPlus be the index of the first entry or char you don't want.
  • Suppose you politely guarantee ((0 <= First) && (First <= Beyond)).
  • Suppose you politely guarantee ((0 <= LastPlus) && (LastPlus <= Beyond)).
  • Suppose you politely guarantee (First <= LastPlus).

2. See that a slice of length (LastPlus - First) is what you get always from the slicing functions here, as you expect, in those common circumstances.

3. Notice these functions encode slices as (First, LastPlus).

4. Remember LSL encodes slices as (First, LastPlus - 1) only while (First < LastPlus). The LSL slice length is (LastPlus - First) like it should be, while (First < LastPlus). But when (LastPlus - First) goes to zero, the LSL slice length goes astonishingly discontinuous in two dimensions. The LSL slice length jumps out to Beyond when ((First == LastPlus) && (LastPlus < Beyond)) and jumps back to 1 when ((First == LastPlus) && (LastPlus == Beyond)).

Get it?

These functions don't astonish your human intuition with those arbitrary discontinuities. Sure yea, unusually well-disciplined people do actually write LSL scripts to verbosely work around those discontinuities inline at every call of llList2List or llGetSubString. Other people erroneously think they are remembering to work around both those discontinuities at every call, but then actually forget one or more calls, or forget one or the other discontinuity, and thus write bugs.

Making a habit of calling these slice functions in place of the LSL slice functions will give you the expected, continuous, adjacent answer even in the boundary case of the empty slice, and thus often making your whole script work better, albeit measurably slower.

Enjoy!

P.S. Don't forget, these slice functions do the same work as the LSL slice functions, but the second parameter is significantly different: it is the LastPlus index, not the Last index. You can't just substitute these slice functions for the LSL slice functions -- you have to also change the calculation of the second parameter.