@q
@program lib-numrng
1 99999 d
i
 
( lib-numrngs    v1.0    Jessy @ FurryMUCK    3/00
  
  This library provides functions that take a single string value 
  containing space- or comma- separated numbers or ranges of numbers, 
  and returns their values as a stack range.
  
  An example: In most word processors, you can specify pages to print by
  typing something like "1-4, 8, 12" or "1-4 8 12" to print pages 1, 2,
  3, 4, 8, and 12. Lib-numrngs does something similar. If passed "1-4,
  8, 12", it would put: 
  
     12 8 4 3 2 1 6
  
  on the stack. That is, the numerical values indicated in the string,
  followed by a range indicator of how many numbers are returned. Its
  intended use is with mail and bulletin board programs and the like.
  "mail #d 2-5" might delete messages 2, 3, 4, and 5.
  
  Since the string passed to lib-numrngs will usually be generated by
  user input, and since trapping bad input can be difficult in this
  situation, I've put quite a bit of effort into making it robust.
  The library can handle positive and negative numbers, in ascending or
  descending order. Spaces, commas, " and ", or " & " may be used as
  separators; dashes or " to " can be used to indicate ranges. Stack
  height is checked as the arg string is parsed and expanded, to prevent
  stack overflows from input like "1-100000". Input that would overflow
  the stack returns 0, false, and no range values. Non-number input is
  discarded. That is, if the input consists entirely of non-number input,
  0 will be returned. If a single value or range contains a non-number
  value, that portion of the range will be omitted; the rest will be
  returned as normal. Wilful garbage input -- that is, crazy combinations
  of numbers, non-number, commas, spaces, and dashes intended to confuse
  the parser -- will either return false or accurate but useless data.
  
  The basic lib-numrng function is ParseRange. This returns the numbers
  indicated in the arg string, unsorted, as integers. 
    
  ParseRange :: Unsorted, Duplicates OK, Integers
   
    "6-4, -1 to 1, 5" ParseRange -- 5, 1, 0, -1, 6, 5, 4, 7
  
  ~
  Alternate forms may be used to specify how to handle sorting,
  duplications, and data type:
  
  ParseRange-SDI :: Sorted, Duplicates OK, Integers
   
    "6-4, -1 to 1, 5" ParseRange-SDI -- 6, 5, 5, 4, 1, 0, -1, 7
  
  ParseRange-SDS :: Sorted, Duplicates OK, Strings
   
    "6-4, -1 to 1, 5" ParseRange-SDI -- "6", "5", "5", "4", "1", "0", "-1", 7
  
  ParseRange-SNI :: Sorted, No Duplicates, Integers
    
    "6-4, -1 to 1, 5" ParseRange-SNI -- 6, 5, 4, 1, 0, -1, 6
  
  ParseRange-SNS :: Sorted, No Duplicates, Strings
   
    "6-4, -1 to 1, 5" ParseRange-SNI :: "6", "5", "4", "1", "0", "-1", 6
  
  ParseRange-UDI :: Unsorted, Duplicates OK, Integers
    
    "6-4, -1 to 1, 5" ParseRange-UDI -- 5, 1, 0, -1, 6, 5, 4, 7
  
  ParseRange-UDS :: Unsorted, Duplicates OK, Strings
    
    "6-4, -1 to 1, 5" ParseRange-UDS -- "5", "1", "0", "-1", "6", "5", "4" 7
  
  ParseRange-UNI :: Unsorted, No Duplicates, Integers
    
    "6-4, -1 to 1, 5" ParseRange-UNI -- 5, 1, 0, -1, 6, 4, 6
  
  ParseRange-UNS :: Unsorted, No Duplicates, Strings
    
    "6-4, -1 to 1, 5" ParseRange-UNS -- "5", "1", "0", "-1", "6", "4" 6
  
  Sorts are performed with a bubble sort. This is efficient for the small
  ranges that users will be specifying in most situations. The bubble sort
  is ineffecient and *slow* for large ranges. If large ranges -- more than 
  100 numbers, say -- are a realistic possibility for your program, con-
  sider using the unsorted functions, either working with the data as is
  or sorting it within your own program by some other method.
  
  INSTALLATION:
   
  Port lib-numrng and register it as lib/numrng. Set the program Link_OK.
  Lib-numrng will function at Mucker Level 2, but may crash due to max
  instruction count exceeded when working with large, sorted ranges. 
  It may be safely set M3 or Wizard.
  
  Set the definition props as indicated below:
  
@set lib-numrng=_defs/ParseRange:"$lib/numrng" match "ParseRange" call
@set lib-numrng=_defs/ParseRange-SDI:"$lib/numrng" match "ParseRange-SDI" call
@set lib-numrng=_defs/ParseRange-SDS:"$lib/numrng" match "ParseRange-SDS" call
@set lib-numrng=_defs/ParseRange-SNI:"$lib/numrng" match "ParseRange-SNI" call
@set lib-numrng=_defs/ParseRange-SNS:"$lib/numrng" match "ParseRange-SNS" call
@set lib-numrng=_defs/ParseRange-UDI:"$lib/numrng" match "ParseRange-UDI" call
@set lib-numrng=_defs/ParseRange-UDS:"$lib/numrng" match "ParseRange-UDS" call
@set lib-numrng=_defs/ParseRange-UNI:"$lib/numrng" match "ParseRange-UNI" call
@set lib-numrng=_defs/ParseRange-UNS:"$lib/numrng" match "ParseRange-UNS" call
  
  Lib-numrngs may be freely ported. Please comment any changes.
)
$define Tell me @ swap notify $enddef
  
lvar libRangeCounter
  
: SortInts  ( {int-rng} i -- {int-rng}' i )     (* sort range of ints *)
  
  dup
  begin                                           (* begin outer loop *)
    dup while
      over
      begin                        (* begin inner loop: compare pairs *)
        dup 1 > while                   (* pull two values from range *)
        dup 3 + pick over 3 + pick  
        over over < if           (* if 'first' value is greater, swap *)
          swap
        then
        3 pick 3 + put           (* put the pair back in sorted order *)
        over 3 + put
        1 -
      repeat                                        (* end inner loop *)
      pop
    1 -
  repeat                                            (* end outer loop *)
  pop pop                 (* pop last value checked and empty counter *)
  libRangeCounter @                 (* put range length back on stack *)
;
 
: RemoveDups ( {int-rng} i -- {int-rng}' i )  (* remove dups in range *)
  
  begin                                           (* begin outer loop *)
    dup while                        (* get a value to check for dups *)
    dup 1 + pick over 1 -
    begin                                         (* begin inner loop *)
      dup while
      dup 3 + pick
      3 pick = if     (* compare value being checked to rest of range *)
        dup 3 + rotate pop                    (* pop duplicates found *)
        libRangeCounter @ 1 - libRangeCounter !     (* decr rng total *)
        rot 1 - rot rot
      then
      1 -
    repeat                                          (* end inner loop *)
    pop pop
    1 -
  repeat                                            (* end outer loop *)
  pop libRangeCounter @
;
 
: ConvertToStrings ( {int-rng} i -- {str-rng} i ) 
                            (* convert all values in range to strings *)
  
  begin
    dup while
    dup 1 + pick
    intostr over 1 + put
    1 -
  repeat
  pop libRangeCounter @
;
 
: FindTokens  ( s -- s' )         (* subst to make a parseable string *)
  
  ""     "^"     subst
  " to"  "- to"  subst
  " to " " - "   subst
  ","    " and " subst
  "-"    " to "  subst
  "--"   "-- "   subst
  ","    "&"     subst       (* & indicates a leading negative number *)
  ","    " "     subst
  ","    ",,"    subst
  "^^^-" "--"    subst
  "^^^-" "-,-"   subst
  ","    ",,"    subst
  "-"    "-,"    subst
  "-"    "--"    subst
  "^^^"  ",^^^"  subst   (* ^^^ indicates a range. replaces 'to' or - *)
  "^^^"  ",^^^"  subst
  "^^^"  "&^^^"  subst
  dup "^^^" stringpfx if
    begin
      dup "^^^" stringpfx while
      3 strcut swap pop
    repeat
  then
;
 
: CondenseMultiRange ( s -- s' )          (* get rid of linked ranges *)
                                      (* "2-4-6" would become "2^^^6" *)
  
  dup "-" instr 1 - strcut
  dup "-" rinstr strcut
  swap pop 
  "^^^" swap strcat strcat
  "^^^" "^^^^^^" subst
;
 
: ParseRange  ( s -- {str-rng} i )             (* parse s for numbers *)
  
                                  (* bail out if nothing passed to us *)
  dup not if pop 0 exit then
  
  FindTokens                               (* put s in parseable form *)
   
  dup "-" stringpfx if               (* double check leading negative *)
    1 strcut swap pop
    "&" swap strcat
  then
  
  "," explode                    (* break into comma-separated chunks *)
  dup if                                        (* check stack height *)
    dup 509 > if                     (* if dangerously high, bail out *)
      begin
        dup while
        swap pop
      repeat
      exit
    then
    dup libRangeCounter !      (* otherwise, store current range size *)
  else
    exit           (* exit if we didn't get anything from the explode *)
  then
  begin                                   (* begin outer parsing loop *)
    dup while
    dup 1 + pick string? not if                       (* trap garbage *)
      1 - continue
    then
    dup 1 + rotate                                   (* pull one item *)
    dup not if                              (* trap some more garbage *)
      libRangeCounter @ 1 - libRangeCounter !
      pop 1 - continue 
    then
    strip
    dup "-" smatch if
      libRangeCounter @ 1 - libRangeCounter !
      pop 1 - continue
    then
    dup number? if                            (* if a number, convert *)
      atoi swap
    else                               (* otherwise, parse as a range *)
      dup "-" stringpfx if    (* use & to indicate leading num is neg *)
        1 strcut swap pop
        "&" swap strcat
      then
      dup "-" instr if 
        dup "*-*-*" smatch if                    (* trap more garbage *)
          CondenseMultiRange
        then
        dup "^^^" instr if         (* if it's a range, get 1st & last *)
          "^^^" explode pop
        else
          dup "-" instr if
            "-" explode pop
          then
        then                            (* convert &x to -x, a number *)
        "-" "&" subst swap "-" "&" subst swap
        dup number?                     (* make sure both are numbers *)
        3 pick number? and not if
          pop pop
          libRangeCounter @ 1 - libRangeCounter !
          1 - continue
        then
        atoi swap atoi         (* convert range bound strings to ints *)
        "marker" rot rot    (* insert a place marker below range ints *)
        over over > if             (* fill range with contiguous ints *)
          begin
            over over = not while
            over 1 - swap
            depth 509 > if             (* keep an eye on stack height *)
              begin                     (* clear and exit if too high *)
                dup string? not while
                pop
              repeat
              pop
              libRangeCounter @ - 1
              begin
                dup while
                swap pop
                1 -
              repeat
              exit
            then
          repeat
          pop
        else
          begin
            over over = not while
            over 1 + swap
            depth 509 > if
              begin
                dup string? not while
                pop
              repeat
              pop
              libRangeCounter @ - 1
              begin
                dup while
                swap pop
                1 -
              repeat
              exit
            then
          repeat
          pop
        then
        1                 (* update libRangeCounter to new range size *)
        begin
          dup pick string? not while
          1 +
        repeat
        1 +
        begin                 (* put our way down to "marker", pop it *)
          dup 1 > while
          dup 4 > if
            libRangeCounter @ 1 + libRangeCounter !
            dup pick 1 + over put
          then
          swap over 
          -1 * rotate
          1 -
        repeat
        pop swap pop
      else         (* do it this way if range evals to a single number *)
        "-" "&" subst
        dup number? if
          atoi swap
        else
          pop 1 - 
          libRangeCounter @ 1 - libRangeCounter !
          continue
        then
      then
    then
  repeat
      (* we've decremented the range indicator to nothing; put it back *)
  pop libRangeCounter @
;
public ParseRange
 
: ParseRange-SDI  ( s -- {int-rng} i ) ( SORTED, DUPS OK, INTEGERS )
  
  ParseRange 
  dup if SortInts then
;
public ParseRange-SDI  
 
: ParseRange-SDS  ( s -- {str-rng} i ) ( SORTED, DUPS OK, STRINGS )
  
  ParseRange 
  dup if SortInts ConvertToStrings then
;
public ParseRange-SDS  ( s -- {str-rng} i )
 
: ParseRange-SNI  ( s -- {int-rng} i ) ( SORTED, NO DUPS, INTEGERS )
  
  ParseRange 
  dup if SortInts RemoveDups then
;
public ParseRange-SNI
 
: ParseRange-SNS  ( s -- {str-rng} i ) ( SORTED, NO DUPS, STRINGS )
  
  ParseRange 
  dup if SortInts RemoveDups ConvertToStrings then
;
public ParseRange-SNS
 
: ParseRange-UDI  ( s -- {int-rng} i ) ( UNSORTED, DUPS OK, INTEGERS )
  
  ParseRange 
;
public ParseRange-UDI 
 
: ParseRange-UDS  ( s -- {str-rng} i ) ( UNSORTED, DUPS OK, STRINGS )
  
  ParseRange 
  dup if ConvertToStrings then
;
public ParseRange-UDS
 
: ParseRange-UNI  ( s -- {int-rng} i ) ( UNSORTED, NO DUPS, INTEGERS )
  
  ParseRange 
  dup if RemoveDups then
;
public ParseRange-UNI
 
: ParseRange-UNS  ( s -- {str-rng} i ) ( UNSORTED, NO DUPS, STRINGS )
  
  ParseRange 
  dup if RemoveDups ConvertToStrings then
;
public ParseRange-UNS
  
.
c
q
  
@set lib-numrng=L
@set lib-numrng=3
@reg lib-numrng=lib/numrng
@set lib-numrng=_defs/ParseRange:"$lib/numrng" match "ParseRange" call
@set lib-numrng=_defs/ParseRange-SDI:"$lib/numrng" match "ParseRange-SDI" call
@set lib-numrng=_defs/ParseRange-SDS:"$lib/numrng" match "ParseRange-SDS" call
@set lib-numrng=_defs/ParseRange-SNI:"$lib/numrng" match "ParseRange-SNI" call
@set lib-numrng=_defs/ParseRange-SNS:"$lib/numrng" match "ParseRange-SNS" call
@set lib-numrng=_defs/ParseRange-UDI:"$lib/numrng" match "ParseRange-UDI" call
@set lib-numrng=_defs/ParseRange-UDS:"$lib/numrng" match "ParseRange-UDS" call
@set lib-numrng=_defs/ParseRange-UNI:"$lib/numrng" match "ParseRange-UNI" call
@set lib-numrng=_defs/ParseRange-UNS:"$lib/numrng" match "ParseRange-UNS" call