    Extending the split() macro built-in

    This patch extends split()'s functionality. It allows limited splitting,
    where only a certain number of elements should be retrieved, and also
    allows the dropping of the last element found if it is empty.

    The limited count avoids the perhaps unnecessary overhead of generating a
    large array if only the first few elements are to be used. The dropping of
    the empty last element allows simple reconstruction after simple splits,
    very useful for lines. For example, given:

	    a = get_range(0, $text_length)
	    lines = split(a, "\n", "lastnotnull")
	    b = ""
	    for (i = 0; i < lines[]; i++)
	        b = b lines[i] "\n"

    assuming that all lines are '\n' terminated, b == a at the end. Otherwise
    we have to resort to something like:

	    a = get_range(0, $text_length)
	    lines = split(a, "\n")
	    b = ""
	    sep = ""
	    for (i = 0; i < lines[]; i++) {
	        b = b sep lines[i]
	        sep = "\n"
	    }

    to make a == b at the end. which is trickier (albeit more general).

    2008-02-19
    
    Added "nonull" alternative to "lastnotnull": if used, no empty (zero-length)
    pieces will be stored in the array; the count limits the number of pieces
    stored in this case, and does not account for the empty pieces skipped.

diff -ur nedit_official nedit_mod
diff -ur nedit_official/doc/help.etx nedit_mod/doc/help.etx
--- nedit_official/doc/help.etx	2008-01-13 03:48:02.000000000 +0100
+++ nedit_mod/doc/help.etx	2008-02-19 23:01:53.000000000 +0100
@@ -2698,10 +2698,19 @@
   output from the command is returned as the function value, and the command's
   exit status is returned in the global variable $shell_cmd_status.
 
-**split(string, separation_string [, search_type])**
+**split(string, separation_string [, search_type][, maxpieces][, option])**
   Splits a string using the separator specified. Optionally the search_type
   argument can specify how the separation_string is interpreted. The default
-  is "literal". The returned value is an array with keys beginning at 0.
+  is "literal". The returned value is an array with keys beginning at 0. If
+  a numeric value maxpieces is provided, it limits the number of pieces in the
+  resulting array: the last piece, if it has index maxpieces - 1, will contain the
+  end of the original string, complete with separators, that has not been
+  split. If present, option should have value "lastnotnull" or "nonull".
+  "lastnotnull" avoids the creation of an empty element if the original string
+  ends with a separator; if the string is empty, this causes the returned array
+  to contain no entries. "nonull" stops any empty string entries from being
+  added to the array; these skipped entries are not counted if maxpieces was
+  provided.
 
 **string_dialog( message, btn_1_label, btn_2_label, ... )**
   Pops up a dialog prompting the user to enter information. The first argument
diff -ur nedit_official/source/macro.c nedit_mod/source/macro.c
--- nedit_official/source/macro.c	2007-10-04 18:04:25.000000000 +0200
+++ nedit_mod/source/macro.c	2008-02-19 23:52:26.000000000 +0100
@@ -3868,14 +3868,44 @@
 }
 
 /*
-** This function is intended to split strings into an array of substrings
-** Importatnt note: It should always return at least one entry with key 0
-** split("", ",") result[0] = ""
-** split("1,2", ",") result[0] = "1" result[1] = "2"
-** split("1,2,", ",") result[0] = "1" result[1] = "2" result[2] = ""
-** 
-** This behavior is specifically important when used to break up
-** array sub-scripts
+** This function is intended to split strings into an array of substrings.
+**
+**  array = split(string, separator[, searchType][, count]
+**                [, ("lastnotnull"|"nonull")])
+**
+** Mandatory arguments:
+**      string: string to split,
+**      string: separator string or pattern marking where to split
+** Optional arguments:
+**      searchType: separator search type (default is "literal") to use to find
+**          occurrences of separator in string.
+**      count: maximum number of pieces in the returned array (default is
+**          infinite, must be greater than zero); if smaller than or equal to
+**          the number of separators found in string, the last piece will
+**          contain the remainder of the string to split (a count of 1 produces
+**          a single result in the returned array, equal to the original
+**          string).
+**      keyword "lastnotnull": if present, this stops an empty string being
+**          returned in the last entry of the array if the string to split ends
+**          with the separator. This has the effect of returning an empty array
+**          if the string to split is originally empty. Otherwise, the returned
+**          array will always contain at least one element.
+**      keyword "nonull": if present, this stops an empty string being returned
+**          in any entry of the array. This has the effect of returning an empty
+**          array if the string to split is originally empty, or consists
+**          of separator substrings only.
+**
+** Important note: It should always return at least one entry with key 0
+** unless "lastnotnull" or "nonull" is present.
+**
+** eg
+**     split("", ",") result[0] = ""
+**     split(",", ",") result[0] = "" result[1] = ""
+**     split("1,2", ",") result[0] = "1" result[1] = "2"
+**     split("1,2,", ",") result[0] = "1" result[1] = "2" result[2] = ""
+**
+**     This behavior is specifically important when used to break up
+**     array sub-scripts (unless "lastnotnull" is present)
 */
 
 static int splitMS(WindowInfo *window, DataValue *argList, int nArgs,
@@ -3888,13 +3918,20 @@
     char indexStr[TYPE_INT_STR_SIZE(int)], *allocIndexStr;
     DataValue element;
     int elementLen;
-    
-    if (nArgs < 2) {
-        return(wrongNArgsErr(errMsg));
+    int haveSearchType = False;
+    int haveCount = False;
+    int count = 0;
+    int lastIndex;
+    int lastnotnull = False;
+    int nonull = False;
+    int haveNotNullOpt = False;
+
+    if (nArgs < 2 || nArgs > 4) {
+        return wrongNArgsErr(errMsg);
     }
     if (!readStringArg(argList[0], &sourceStr, stringStorage[0], errMsg)) {
         *errMsg = "first argument must be a string: %s";
-        return(False);
+        return False;
     }
     if (!readStringArg(argList[1], &splitStr, stringStorage[1], errMsg)) {
         splitStr = NULL;
@@ -3906,49 +3943,90 @@
     }
     if (splitStr == NULL) {
         *errMsg = "second argument must be a non-empty string: %s";
-        return(False);
+        return False;
     }
-    if (nArgs > 2 && readStringArg(argList[2], &typeSplitStr, stringStorage[2], errMsg)) {
-      	if (!StringToSearchType(typeSplitStr, &searchType)) {
+
+    /* pick up count, search type and option (if any) */
+    searchType = SEARCH_LITERAL;
+    for (indexNum = 2; indexNum < nArgs; indexNum++) {
+        if (!readStringArg(argList[indexNum], &typeSplitStr,
+                          stringStorage[indexNum], errMsg)) {
+            *errMsg = "non-scalar arguments not allowed: %s";
+            return False;
+        }
+        if (strcmp(typeSplitStr, "lastnotnull") == 0) {
+            if (lastnotnull || nonull)
+                haveNotNullOpt = True;
+            lastnotnull = True;
+        } else if (strcmp(typeSplitStr, "nonull") == 0) {
+            if (lastnotnull || nonull)
+                haveNotNullOpt = True;
+            nonull = True;
+        } else if (StringToSearchType(typeSplitStr, &searchType)) {
+            if (haveSearchType) {
+                *errMsg = "split search type supplied more than once: %s";
+                return False;
+            }
+            haveSearchType = True;
+        } else if (!haveCount &&
+                   readIntArg(argList[indexNum], &count, errMsg)) {
+            haveCount = True;
+            if (count < 1) {
+                *errMsg = "split maximum count must be greater than 0: %s";
+                return False;
+            }
+            lastIndex = count - 1;
+        } else {
             *errMsg = "unrecognized argument to %s";
-            return(False);
+            return False;
+        }
+        if (haveNotNullOpt) {
+            *errMsg = "\"lastnotnull\" specified more than once: %s";
+            return False;
         }
     }
-    else {
-    	searchType = SEARCH_LITERAL;
-    }
-    
+
+    /* now we can do the work */
     result->tag = ARRAY_TAG;
     result->val.arrayPtr = ArrayNew();
 
     beginPos = 0;
-    lastEnd = 0;
+    foundEnd = 0;
     indexNum = 0;
     strLength = strlen(sourceStr);
     found = 1;
-    while (found && beginPos < strLength) {
-        sprintf(indexStr, "%d", indexNum);
-        allocIndexStr = AllocString(strlen(indexStr) + 1);
-        if (!allocIndexStr) {
-            *errMsg = "array element failed to allocate key: %s";
-            return(False);
+
+    while (found && beginPos <= strLength) {
+        /* hold on to end of last separator */
+        lastEnd = foundEnd;
+
+        /* find next separator if appropriate */
+        if (haveCount && lastIndex == indexNum) {
+            found = 0;
+        } else if (beginPos >= strLength) {
+            found = 0;
+        } else {
+            found = SearchString(sourceStr, splitStr, SEARCH_FORWARD,
+                        searchType, False, beginPos, &foundStart, &foundEnd,
+                        NULL, NULL, GetWindowDelimiters(window));
         }
-        strcpy(allocIndexStr, indexStr);
-        found = SearchString(sourceStr, splitStr, SEARCH_FORWARD, searchType,
-            False, beginPos, &foundStart, &foundEnd,
-	        NULL, NULL, GetWindowDelimiters(window));
-        elementEnd = found ? foundStart : strLength;
-        elementLen = elementEnd - lastEnd;
-        element.tag = STRING_TAG;
-        if (!AllocNStringNCpy(&element.val.str, &sourceStr[lastEnd], elementLen)) {
-            *errMsg = "failed to allocate element value: %s";
-            return(False);
+        if (!found) {
+            foundStart = foundEnd = strLength;
         }
 
-        if (!ArrayInsert(result, allocIndexStr, &element)) {
-            M_ARRAY_INSERT_FAILURE();
-        }
+        /* hold onto end positions of substring to store in array */
+        elementEnd = foundStart;
+        elementLen = elementEnd - lastEnd;
+
+        /* debugging:
+         *  fprintf(stderr, "split %sstring[%d] = \"%.*s[%.*s]<%.*s>%s\"\n%s",
+         *          found ? "=" : "#", strLength, lastEnd, sourceStr,
+         *          elementLen, sourceStr + lastEnd,
+         *          foundEnd - foundStart, sourceStr + elementEnd,
+         *          sourceStr + foundEnd, found ? "" : "\n");
+         */
 
+        /* prepare for next iteration */
         if (found) {
             if (foundStart == foundEnd) {
                 beginPos = foundEnd + 1; /* Avoid endless loop for 0-width match */
@@ -3958,70 +4036,33 @@
         } else {
             beginPos = strLength; /* Break the loop */
         }
-        lastEnd = foundEnd;
-        ++indexNum;
-    }
-    if (found) {
+
+        /* do we skip storing what we found in the array? */
+        if (nonull && elementLen == 0)
+            continue;
+        if (lastnotnull && lastEnd == strLength)
+            break;
+
+        /* OK: store what we have in the array an increase the index */
         sprintf(indexStr, "%d", indexNum);
         allocIndexStr = AllocString(strlen(indexStr) + 1);
         if (!allocIndexStr) {
             *errMsg = "array element failed to allocate key: %s";
-            return(False);
+            return False;
         }
         strcpy(allocIndexStr, indexStr);
         element.tag = STRING_TAG;
-        if (lastEnd == strLength) {
-            /* The pattern mathed the end of the string. Add an empty chunk. */
-            element.val.str.rep = PERM_ALLOC_STR("");
-            element.val.str.len = 0;
-
-            if (!ArrayInsert(result, allocIndexStr, &element)) {
-                M_ARRAY_INSERT_FAILURE();
-            }
-        } else {
-            /* We skipped the last character to prevent an endless loop. 
-               Add it to the list. */
-            elementLen = strLength - lastEnd;
-            if (!AllocNStringNCpy(&element.val.str, &sourceStr[lastEnd], elementLen)) {
-                *errMsg = "failed to allocate element value: %s";
-                return(False);
-            }
-
-            if (!ArrayInsert(result, allocIndexStr, &element)) {
-                M_ARRAY_INSERT_FAILURE();
-            }
+        if (!AllocNStringNCpy(&element.val.str, &sourceStr[lastEnd], elementLen)) {
+            *errMsg = "failed to allocate element value: %s";
+            return False;
+        }
 
-            /* If the pattern can match zero-length strings, we may have to
-               add a final empty chunk. 
-               For instance:  split("abc\n", "$", "regex")
-                 -> matches before \n and at end of string
-                 -> expected output: "abc", "\n", ""
-               The '\n' gets added in the lines above, but we still have to
-               verify whether the pattern also matches the end of the string,
-               and add an empty chunk in case it does. */
-            found = SearchString(sourceStr, splitStr, SEARCH_FORWARD, 
-                searchType, False, strLength, &foundStart, &foundEnd, 
-                NULL, NULL, GetWindowDelimiters(window));
-            if (found) {
-                ++indexNum;
-                sprintf(indexStr, "%d", indexNum);
-                allocIndexStr = AllocString(strlen(indexStr) + 1);
-                if (!allocIndexStr) {
-                    *errMsg = "array element failed to allocate key: %s";
-                    return(False);
-                }
-                strcpy(allocIndexStr, indexStr);
-                element.tag = STRING_TAG;
-                element.val.str.rep = PERM_ALLOC_STR("");
-                element.val.str.len = 0;
-
-                if (!ArrayInsert(result, allocIndexStr, &element)) {
-                    M_ARRAY_INSERT_FAILURE();
-                }
-            }
+        if (!ArrayInsert(result, allocIndexStr, &element)) {
+            M_ARRAY_INSERT_FAILURE();
         }
+        ++indexNum;
     }
-    return(True);
+    return True;
 }
 
 /*
