[comp.lang.forth] Forth String Handling Words

brunjes@isctsse.UUCP (Roy Brunjes) (04/06/89)

In article <1868@umbc3.UMBC.EDU> cs472226@umbc5.umbc.edu.UUCP (David Wood (CS472226)) writes:
>   I'm looking for string-handling words, including variables,
>constants, LEFT$, RIGHT$, MID$, etc... Could anyone tell me where to get
>a collection like that? Are there any books with collections of routines
>in FORTH that I should know about but don't because I'm lazy?
>
>                                                 -David Wood

I have written some words for working with strings (both counted strings
and null-terminated strings are supported) for the Amiga.  The most
current version I have is written for JForth (I switched from Multi-Forth
to JForth).  I would be glad to send them to anyone interested, but I
am unsure about how to best do that.  I have never posted large files to
any newsgroup.  This code may need to be modified to run under non-JForth
systems, so be ready for some of that.

Roy Brunjes    ...rochester!kodak!kadsma!brunjes

Disclaimer:  These thoughts are mine.  My employer would not agree with
	     them on principle!

cs472226@umbc5.umbc.edu (David Wood (CS472226)) (04/07/89)

In article <183@isctsse.UUCP> brunjes@isctsse.UUCP (Roy Brunjes) writes:
>In article <1868@umbc3.UMBC.EDU> cs472226@umbc5.umbc.edu.UUCP (David Wood (CS472226)) writes:
>> (request for FORTH String-Handling words deleted)
>>
>>                                                 -David Wood
>
>I have written some words for working with strings (both counted strings
>and null-terminated strings are supported) for the Amiga...
>          ...I would be glad to send them to anyone interested, but I
>am unsure about how to best do that.  I have never posted large files to
>any newsgroup.  This code may need to be modified to run under non-JForth
>systems, so be ready for some of that.
>
>Roy Brunjes    ...rochester!kodak!kadsma!brunjes
>
   Absolutely I'm interested in seeing both sets of words. In fact, if
you use both a length byte and null after, can't you access both types
of string depending on what you send as an address?

   Since there's some concern, we ought to have a standard for placing
FORTH code on here.
   May I suggest? (A moot question, since I'm going to anyway ;) )
   1. If the code is system-specific (makes ROM or specialized toolbox
calls), the system for which it was designed and the make and model of
the FORTH used to create it should be included as a header to the
batch of screens.
   2. The screens should be placed on UseNet with screen numbers
and a total screen count ("Screen 1 of 4," "Screen 2 of 9," etc.).
   3. Each line of the screen should be numbered from 1 to 16 (some
systems may choke on a line 0). Blank lines should also be included.
   4. Any non-standard words ought to be documented (explanations
outside the above defined screen structure should be acceptable) or
defined, and any words which make system calls should be clearly
documented as to what they do (so either words can be written to patch
it, or similar system calls can be used on other systems).

   As an example (TOTALLY NONFUNCTIONAL! Do Not Use...)
   
   System: APPLE IIGS+       FORTH: CrashFORTH v1.87
   Screen 1 of 1
01> ( This is just a demonstration FORTH block. )
02> : Zekto 2 + * ;
03> : Mnoga 45 16 ." Garter Snake" ;
04> : Kiplek Mnoga Zekto Drop ;
05>
06>
07>
08>
09>
10>
11>
12>
13>
14>
15>
16>

   It doesn't look like a really usable format at the moment, but if you
write a FORTH program to read and strip off the headers, and then write
the code into a proper block file, then you've got something. So the
FORTH system in question (i.e. anyone) can read it, the System, FORTH,
and Screen information ought to have standard tabs (May I suggest 3, 40,
and 3, respectively?). The line numbers are used mainly as a checksum,
so you and/or your system knows that some lines are missing and that
compiling this code will likely result in disaster. The code itself
should start writing at tab(4) and each line should run no longer than
64 characters (not including the line number, which won't be in the
final block file). In fact, most lines shouldn't exceed 63 characters
because some FORTHs handle the ends of lines funny.
   If anyone would care to distill this proposed standard for USENet
FORTH code into something people can READ, please do so. I'd like to see
what I wrote, too. :^)
                                                      -David Wood
   Portal gets me into UseNet? Great! How do I get to Portal?

brunjes@isctsse.UUCP (Roy Brunjes) (04/10/89)

In article <1880@umbc3.UMBC.EDU> cs472226@umbc5.umbc.edu.UUCP (David Wood (CS472226)) writes:
>   Absolutely I'm interested in seeing both sets of words.

It's one set of words in a text file (non-block) format.  Hope this is ok.

> In fact, if you use both a length byte and null after, can't you access both
> types of string depending on what you send as an address?

Actually, I don't use both a count byte and a null after, but the effect is the
same -- you can almost always use either type of string with these words (there
are a few exceptions, but they are noted).

> Suggested format for submitting Forth source code to net deleted ...

The biggest problem I see with that is that not all of us use block files!
Straight text files seem easier for sending to the net as well as being my
preferred (slightly -- I like block files only a little less) method for
storing Forth source code.

Anyway, since there is significant (for this newsgroup) interest in my code,
here it comes.  It runs on an Amiga running JForth, but ports to other Forths
shouldn't be that bad (famous saying, eh?  -- I believe that to be close to
accurate though).  Enjoy!

Roy Brunjes   ....rochester!kodak!kadsma!brunjes


----- CUT HERE -------

\ --------------------------------------------------------------------------
\
\  StringPkg  Version 1.1  by  Roy E. Brunjes               September 1988
\
\ --------------------------------------------------------------------------

Include? 0COUNT      JF:Strings+
Include? {           JU:Locals

\ Note:  The debug options, when active, cost you about 5570 bytes of
\ ----   dictionary space.

.NEED String.Debug
    Variable String.Debug
    String.Debug ON           \ By default this is TRUE (i.e. ACTIVE)

    >newline ." Setting STRING.DEBUG to TRUE" CR
.ELSE
    >newline ." Leaving STRING.DEBUG set to " String.Debug @ . CR
.THEN


.NEED User.Debug
      Variable User.Debug
      User.Debug ON           \ By default this is TRUE (i.e. ACTIVE)

    >newline ." Setting USER.DEBUG   to TRUE" CR
.ELSE
    >newline ." Leaving USER.DEBUG   set to " USER.DEBUG @ . CR
.THEN


ANEW StringPkg

\  Functionality provided with this word set:
\
\      Words StringVar and StringVar0 for defining counted character strings
\            and null-terminated character strings respectively.
\      SubString (Leftmost portion, Rightmost portion, Middle portion)
\      Concatenation of two strings.
\      Assignment of String Variable's value to another String Variable
\      Current and Maximum allowed lengths of a string.
\      Keyboard input of values for strings.
\      Conversion of string to a numeric value.
\      Convert a string to lower case.  Convert a string to upper case.
\      Locate a string within another string.
\      Fetch and Store into specific character positions within a string.
\
\  Perhaps the nicest thing about this wordset (for me) is the indifference
\  with which both counted and null-terminated strings are treated.  All
\  words can take either unless explicitly specified otherwise.  Also, for
\  those Forthers who need counted strings of > 255 characters, you're in
\  luck!  You can create so-called LONG strings with lengths up to 64K
\  bytes.  And they use the same words that their little counterparts use.
\
\  This wordset behaves somewhat differently based on the value of two
\  compile-time variables:  STRING.DEBUG and USER.DEBUG.  It is expected
\  that USER.DEBUG be a variable that has meaning outside of this String
\  Package.  STRING.DEBUG is a debugging flag specifically for this package.
\  In either case, this code will be fatter and slower (but much more
\  tolerant of mistakes!) when either or both of these flags are TRUE
\  (which amounts to "non-FALSE" in Forth).
\
\  So, to have more forgiving code, issue a USER.DEBUG ON or a
\  STRING.DEBUG ON before compiling these words.  Note that this is done
\  for you by default if either of the two variables are not present in the
\  dictionary at compile time.  If you don't like the default, CHANGE IT!
\  Personally, I hate the GURU so I leave the debug options on until I am
\  SURE that my code is behaving!
\
\  Note that USER.DEBUG is intended to be used by other Forth code you write
\  or use.  I couldn't count on anyone else defining it, so I defined it for
\  you.  If you already have a USER.DEBUG variable, use it and remove my
\  creation of it.  And then, set USER.DEBUG when you want ALL
\  "USER.DEBUG-aware" code you compile to be active, and set STRING.DEBUG ON
\  in those cases where you are confident that non-string code works but you
\  aren't sure about your string-related code.  Enough comments!  On with
\  the code!

Base @                           \ Save this on the stack to restore at end
Decimal                          \ I have only 10 fingers today ...

  1 CONSTANT  Value.Too.Small    \ Internally used by Get.Number
  2 CONSTANT  Value.Too.Big      \ Internally used by Get.Number
  3 CONSTANT  Not.A.Number       \ Internally used by Get.Number

  0 CONSTANT  Null.String        \ Useful for enhancing code readability.
                                 \ e.g.  xyz0  Null.String  Smart.Length
                                 \ is better than xyz0  0  Smart.Length

  1 CONSTANT  Short.String       \ See comments for Null.String.
                                 \ e.g.  xyz   Count.String  Smart.Length
                                 \ Has one-byte length field, allowing
                                 \ counted strings of 255 characters.

  2 CONSTANT  Long.String        \ See comments for Null.String.
                                 \ e.g.  xyz   Long.String   Smart.Length
                                 \ Has two-byte length field, allowing
                                 \ counted strings of 65535 characters.

\ Note:  It is NOT coincidence that the values of Short.String and
\        Long.String are the same as the # of bytes in the string length
\        "field".  This relationship is utilized by the code below, so
\        don't change it without examining the side effects!


20000 CONSTANT LowMem            \ Used to tell if stack set up wrong for
                                 \ Get.String.

512   CONSTANT Too.Long.To.Print \ Used to see if some words should print
                                 \ out the value of a string or not.

ASM 3+ ( n -- n+3 )
   addq.l #$3,tos
   rts
END-CODE


ASM 3- ( n -- n-3 )
   subq.l #$3,tos
   rts
END-CODE


ASM 5+ ( n -- n+5 )
   addq.l #$5,tos
   rts
END-CODE


ASM 5- ( n -- n-5 )
   subq.l #$5,tos
   rts
END-CODE


: @$Type  ( addr.of.1st.char -- Type.Flag )
\
\  Pronounced: "Fetch String Type"
\
\  Fetch the string descriptor (or type flag as I have called it here) for
\  the string address that is passed on the stack.  Possible values are
\  NULL.STRING, SHORT.STRING, and LONG.STRING .
\
   1- C@ inline ;


: Length0       ( Ptr.to.first.string.char -- length.of.string )
\ 
\ This word need never be called.  It is provided only for those who may
\ find a use for it.  The usual way to compute the length of a CHAR0 string
\ is to execute its name and then call Length.  This word is for
\ null-terminated strings. Return length of CHAR0 string.
\
  0COUNT SWAP DROP ;


: Length        ( addr.of.1st.char -- length.of.string )
\
\  Return the length of the string whose ptr is passed on the stack.
\
  DUP @$Type                     \ Stack: addr type.flag
  CASE    NULL.STRING  OF
                 Length0
               ELSE
          SHORT.STRING  OF
                 2-              \ back up to current length field
                 C@
               ELSE
          LONG.STRING  OF
                 3-              \ back up to current length field
                 W@
               ELSE
                 CR ." Length Error.  Address passed is not a valid string."
                 CR ." Address passed: " .
                 CR ." Length Aborting."  0SP  ABORT
  ENDCASE  ;


: @$Info ( addr.of.1st.char -- addr.of.1st.char curr.length type.flag )
\
\  Pronounced: "Fetch-String-Info"
\
\  Return the pointer to the 1st char of string (unchanged from what was
\  passed in) under the current length of that string, under a type flag
\  indicating what type of string this is (NULL.STRING, SHORT.STRING, or
\  LONG.STRING .
\
   DUP Length                    \ Stack: addr curr.length
   SWAP DUP @$Type               \ Stack: curr.length addr type.flag
   ROT  SWAP   ;                 \ Stack: addr curr.length type.flag


: $Type  ( addr.of.1st.char -- )
\
\  Pronounced "String Type"
\
\  Print out string (CHAR, CHAR0, or LONG.CHAR) that is passed on stack.
\  Usage:  stringvariable $TYPE
\
\  Note: This redefines $TYPE from Delta Research.
\
   DUP   Length   TYPE  ;


: Check.YN  ( n -- flag )
\
\  Returns TRUE if n is the character 'Y' or the character 'y'.  Returns
\  FALSE otherwise.
\
   DUP
    89 =              \ Ascii  89 is Y
   SWAP
   121 =              \ Ascii 121 is y
   OR ;


: ERASED ( n -- )
    HERE OVER ERASE ALLOT ;  \ Erase given # of bytes then allot them.

\  A note about how the following defining words set up shop in the
\  dictionary is in order:
\
\  Defining Word         Dictionary Format
\  -------------         -----------------
\
\  CHAR0                 First:  Maximum Length (2 Bytes)
\                        Second: String-type (NULL.STRING : 1 byte)
\                        Third:  The string itself
\
\  CHAR                  First:  Maximum Length (1 Byte)
\                        Second: Current Length (1 Byte)
\                        Third:  String-type (SHORT.STRING : 1 byte)
\                        Fourth: The string itself
\
\  LONG.CHAR             First:  Maximum Length (2 Bytes)
\                        Second: Current Length (2 Bytes)
\                        Third:  String-type (LONG.STRING : 1 byte)
\                        Fourth: The string itself
\
\  It is MANDATORY that the first thing before the first character be the
\  string-type byte.  Other things may change, but this can't!
\

: CHAR0

  \ Please don't use this word.  Use StringVar0 instead.  It is safer and
  \ won't change much from release to release.

  \ A defining word that defines a null-terminated string variable.
  \
  \  Compile-Time Behavior:
  \
  \  Usage: 20 char0 xyz0
  \
  \  This causes the definition of a 20 character string called xyz0 to be
  \  compiled into the dictionary.  It is intended to be a null-terminated
  \  string.  The first 2 bytes hold the max length (20 in this case).
  \  The remaining bytes are for the string itself (with one extra for a
  \  null terminator.  These bytes are ALLOTted from the dictionary.  In
  \  the example, 2 bytes for the max length + 20 bytes for string + 1 for
  \  null terminator = 23 bytes for this string.  BUT, there appears to be
  \  a bug in EXPECT, so this word reserves 4 extra bytes for a buggy
  \  EXPECT. (See my comments further along in this file about EXPECT.)
  \
  \  Helpful Hint:  Name all CHAR0 words xxxx0 to emphasize their
  \                 null terminators.
  \
  \  Run-Time Behavior:
  \
  \  Usage: xyz0      ( -- addr.of.first.char )
  \
  \  This causes the address of the first character in string xyz0 to be
  \  placed on the stack.

  ( Likely way to print a CHAR0 string:  stringvar0 $type )

\ Possible enhancements for this word:
\
\   Since 64K is a lot of string and would take up a lot of dictionary
\   space, consider allocating memory from the heap for these critters,
\   perhaps only if size > constant 0STRING.IN.HEAP .  Coming in V2.0!
\

  ALIGN
  CREATE             \ Creates a new dictionary entry (e.g. xyz0)
  DUP W,             \ Stash the max length first
  NULL.STRING C,     \ Let us know that this is a null terminated string
                     \ by checking this value at run-time.
  1+                 \ One more byte for null at end.
  4 + ERASED         \ Zero-out this chunk of memory first, then allot.
                     \ Add one extra to hold null at end if string is full
                     \ length!
                     \ Add 4 extra bytes to cover a bug in EXPECT which
                     \ is very fond of adding 4 bytes of Hex 00's to the
                     \ end of the area it is storing the chars into.
  ALIGN
  DOES>              \ Marks the end of the compile-time behavior of char0,
  3+   ;             \ Stack: addr.of.1st.char


: CHAR ( see CHAR0 above, this is the same except this is for counted )
       ( strings i.e. string length is stored in first byte of string )

  \  Runtime Usage: xyz      ( -- addr.of.first.char )

  \ Please don't use this word.  Use StringVar instead.  It is safer and
  \ won't change much from release to release.

  ( We have to make sure that no max lengths are > 255 since only 1 byte )
  ( of dictionary space is used to store the length of counted strings.  )
  ( Likely way to print a CHAR string: stringvar $type                   )

  ALIGN CREATE             \ Create the string name in dictionary
  DUP C, 0 C,              \ Set max. length to n, Curr length to 0
  SHORT.STRING C,          \ Flag this string as a Short (255 char max)
  ERASED                   \ Allot n bytes of memory & zero it out
  ALIGN
  DOES>  
  3+    ;                  \ Stack: addr.of.1st.char


: LONG.CHAR ( see CHAR0 above, this is the same except this is for counted )
            ( strings up to 65535 chars long )

  \ Runtime Usage: xyz      ( -- addr.of.first.char )

  \ Please don't use this word.  Use StringVar instead.  It is safer and
  \ won't change much from release to release.

  ( Have to make sure that no max lengths are > 65535 since only 2 bytes )
  ( of dictionary space is used to store the length of these strings.    )
  ( Likely way to print a LONG.CHAR string: Lstringvar $type             )

\ Possible enhancements for this word:
\
\   Since 64K is a lot of string and would take up a lot of dictionary
\   space, consider allocating memory from the heap for these critters,
\   perhaps only if size > constant STRING.IN.HEAP .  Coming in V2.0!
\

  ALIGN CREATE             \ Create the string name in dictionary
  DUP W, 0 W,              \ Set max. length to n, Curr length to 0
  LONG.STRING C,           \ Flag this string as a LONG (64K max chars)
  4 +                      \ Due to bug in EXPECT, allot 4 extra bytes
                           \ at end of string (not counted as part of str)
  ERASED                   \ Allot n bytes of memory & zero it out
  ALIGN
  DOES>  
  5+    ;                  \ Stack: addr.of.1st.char


: StringVar  ( count -- )                       ( Compiling )
             (       -- addr.of.1st.char )      ( Run-Time  )
\
\  Create a counted string variable.  The type (CHAR or LONG.CHAR) depends
\  on the max length [count].  At any rate, count chars are set aside for
\  this string (plus a few overhead bytes).
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF
  DEPTH 0=
  IF                    \ TRUE if no data is on the stack
    CR ." Fatal Error in String.Var.  No length supplied on stack."
    CR ." This error checking just saved you a visit with the GURU!"
    CR ." String.Var Aborting."  0SP  ABORT
  THEN

  DUP 65535 >
  IF              ( TRUE if user-specified max length is > 65535 )
    cr ." String.Var Error.  You can't define a string with a length"
    cr ."                    greater than 65535 characters!"
    cr ." Length of string you tried to create = " . 
    cr ." String.Var Aborting." 0SP ABORT
  THEN

.THEN

   DUP
   256 <
   IF
     CHAR
   ELSE
     LONG.CHAR
   THEN   ;


: StringVar0 ( count -- )                       ( Compiling )
             (       -- addr.of.1st.char )      ( Run-Time  )

\
\  Pronounced "String Variable Zero"
\
\  Create a null-terminated string variable.  'Count' chars are reserved
\  for the string (this excludes any overhead bytes; i.e. overhead bytes
\  are NOT part of the count bytes reserved).
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF
  DEPTH 0=
  IF                    \ TRUE if no data is on the stack
    CR ." Fatal Error in StringVar0.  No length supplied on stack."
    CR ." This error checking just saved you a visit with the GURU!"
    CR ." StringVar0 Aborting."  0SP  ABORT
  THEN

  DUP 65535 >
  IF              ( TRUE if user-specified max length is > 65535 )
    cr ." Error in StringVar0 -- You can't define a string with a length"
    cr ."                        greater than 65535 characters."
    cr ." Length of string you tried to create = " . 
    cr ." StringVar0 Aborting." 0SP  ABORT
  THEN

.THEN

   CHAR0  ;

\ Some synonyms (and the words I use most often)
: $VAR  StringVar  ;
: $VAR0 StringVar0 ;

255 StringVar   GetNumWorkStr      \ Internally used by Get.Number

: Max.Length    ( addr.of.1st.char --  max.string.length )
\
\  Returns the maximum length of string (as declared by programmer)
\
   DUP @$Type
   CASE    NULL.STRING  OF
                 3-              \ back up to max length field
                 W@
               ELSE
           SHORT.STRING  OF
                 3-              \ back up to max length field
                 C@
               ELSE
           LONG.STRING  OF
                 5-              \ back up to max length field
                 W@
               ELSE
                 CR ." Max.Length Error.  Address passed is not a valid "
                    ." string."
                 CR ." Address passed: " .
                 CR ." Max.Length Aborting."  0SP  ABORT
   ENDCASE  ;


: $Extend?  ( n addr.of.1st.char -- flag )
\
\  Pronounced "String Extend Question"
\
\  Return TRUE if string can be extended by n characters, FALSE otherwise.
\
   DUP  Max.Length               \ Stack: n addr1 max.length
   SWAP     Length               \ Stack: n max.length curr.length
   ROT                           \ Stack: max.length curr.length n
   +                             \ Stack: max.length proposed.length
   <                             \ TRUE if proposed.length > max.length
   NOT  ;


: $Extend  { n str1 -- }
\
\  Pronounced "String Extend"
\
\  Add n to current length of string str1 (but does NOT actually add chars!)
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF
   n str1
   $Extend?
   NOT IF                        \ TRUE if we can't extend by that much
         CR ." $Extend Error.  String cannot be extended by " n .
            ." characters."
         CR ." Maximum length of string = " str1 Max.Length .
         CR ." Current length of string = " str1 Length .
         CR ." $Extend Aborting."  0SP  ABORT
       THEN
.THEN

   ( At this point, we assume we CAN extend by n, so just bump curr length )

   str1 @$Type
   CASE    NULL.STRING   OF
                 0 str1 DUP Length
                 + n + C!             \ Put NULL at end of new dest string
               ELSE
           SHORT.STRING  OF
                 str1 Length n +
                 str1 2-
                 C!                   \ Update current length field
               ELSE
           LONG.STRING   OF
                 str1 Length n +
                 str1 3-
                 W!                   \ Update current length field
               ELSE
                 CR ." $Extend Error.  Invalid string passed!"
                 CR ." String must be one of: StringVar, StringVar0"
                 CR ." String address passed : " str1 .
                 CR ." $Extend Aborting." 0SP  ABORT
  ENDCASE  ;
   

: $Type?  ( addr.of.1st.char -- )
\
\  Report to console the status of a null terminated string passed in.
\
  >newline
  DUP @$Type
  CASE    NULL.STRING  OF
      ." This is a NULL.STRING (null-terminated string) with max.length = "
               ELSE
          SHORT.STRING  OF
      ." This is a SHORT.STRING (counted string) with max.length = "
               ELSE
          LONG.STRING  OF
      ." This is a LONG.STRING (counted string) with max.length = "
               ELSE
                 CR ." $Status Error.  Address passed is not a valid string."
                 CR ." Address passed: " .
                 CR ." Length Aborting."  0SP  ABORT
  ENDCASE
  DUP                                ( Stack: addr addr )
  Max.Length  .                      ( Stack: addr      )
  cr ." Current Length = " DUP       ( Stack: addr addr )
  Length DUP .                       ( Stack: addr len  )
  DUP Too.Long.To.Print >
  IF
    CR
    ." This string is long.  Do you really want to see its value? (y/n) "
    KEY Check.YN          ( comes back TRUE if yes )
    IF
      CR ." Current Value  = " TYPE
    ELSE
      2DROP
    THEN
  ELSE
    CR ." Current Value  = "  TYPE
  THEN  ;


\ And now, a few likely desired synonyms for $Type?
: $Status $Type? ;
: ?$ $Type? ;


: Get.String { addr charcount -- }
\
\ Accept characters from keyboard until you see a Carriage Return or
\ charcount chars have been entered.  Then store value into addr.
\
\ addr is the address of the first character position in the string
\ charcount is the # of characters you want to get (maximum)
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF
    addr charcount <
    IF
      CR ." Get.String Error.  You probably switched the address and count"
      CR ." on the stack."
      CR ." You passed : Address = " addr . ." and Count = " charcount .
      CR ." Get.String Aborting."  0SP  ABORT
    THEN

    addr LowMem <
    IF
      CR ." Get.String Error.  You wanted to put the string in the kernal's"
      CR ." memory!  You probably mixed up the order of the stack items"
      CR ." before calling Get.String."
      CR ." You passed : Address = " addr . ." and Count = " charcount .
      CR ." Get.String Aborting."  0SP  ABORT
    THEN

    addr Max.Length                  \ Compute max length of string
    charcount <
    IF
        CR
        ." Error in Get.String"
        ."  -- Max. # of chars entered would cause string overflow." CR
        ." Number of characters asked for = " charcount . CR
        ." Maximum length allowed for string = "  addr Max.Length . CR
        ." Get.String Aborting."   0SP  ( wipe out stack )
        ABORT
    THEN
.THEN

\ EXPECT puts 00 at end of memory (4 bytes worth). Why?
\ addr charcount should work, but EXPECT smashes 4 bytes at end of string.
\ Are you reading this Delta Research?

    addr @$Type
    CASE    NULL.STRING  OF
                  addr charcount
                  EXPECT
                  0               \ Here's the null to put at end of string
                  addr SPAN @ +   \ Here's where to put the null in memory
                  C!              \ Now we have a "safe" string
                ELSE
            SHORT.STRING  OF
                  PAD charcount
                  EXPECT             \ Wait for count chars from terminal;
                                     \ store @ PAD
                  PAD addr SPAN @
                  CMOVE              \ Move string from PAD to String
                                     \ Variable's Address
                  SPAN @ addr 2- C!  \ Store new length into current
                                     \ length byte
                ELSE
            LONG.STRING  OF
                  addr charcount
                  EXPECT             \ Wait for count chars from terminal;
                                     \ store @ addr
                  SPAN @ addr 3- W!  \ Store new length into current
                                     \ length byte
                ELSE
                  CR ." Get.String Error.  Address passed not a string."
                  CR ." You passed: Address = " addr .
                  CR ." Get.String Aborting." 0SP  ABORT
    ENDCASE  ;

\ Synonym(s) for Get.String
: Get$   Get.String ;
: $Get   Get.String ;


: LEFT$ { first n  --  ptr.to.1st.char n }

\ This is a trivial word, but is included for completeness.  It can do some
\ error checking which can save you from strange characters when you go to
\ TYPE the string.
\
\ Return the leftmost n characters of the string whose ptr is 'first'.
\
\ Usage:  stringvar 7 Left$   Meaning:  Give me first 7 chars of stringvar.
\
\ Note: first    = ptr to 1st char in string
\       n        = # of chars you want to be returned
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF
    n 0 <
    IF
      CR ." LEFT$ Error!  You asked for the leftmost " n .
         ." chars from the string!"
      CR ." LEFT$ Aborting."  0SP  ABORT
    THEN
.THEN

    ( first check to see if he wants more chars than string has )
    n first Length >
    IF                            ( If TRUE, wants more chars than we have )
      first DUP Length            ( Return whole string )
    ELSE                          ( User doesn't want whole string )
      first n                     ( All ready for TYPE to be issued )
    THEN   ;


: RIGHT$ { first n -- ptr.to.leftmost.char n }

\ This should return a ptr to first character in string that begins a
\ substring of the rightmost n characters of the string.
\
\ Usage:  stringvar0 7 Right$  Meaning: Give me last 7 chars of stringvar0
\
\ Note:   first    = ptr to 1st char in string
\         n        = # of chars you want to be returned
\         

[  String.Debug @
   User.Debug   @
   OR           ]
.IF
    n 0 <
    IF
      CR ." RIGHT$ Error!  You asked for the rightmost " n .
         ." chars from the string!
      CR ." RIGHT$ Aborting."  0SP  ABORT
    THEN
.THEN

    ( first check to see if he wants more chars than string has )

    n first Length >
    IF                       \ If TRUE, he wants more chars than we have
      first dup Length       \ Return whole thing
    ELSE                     \ NOT asking for more chars than we have
      first dup Length +     \ Pointer to last char + 1
      n - n
    THEN  ;


: MID$ { first start n -- substring.ptr.start n }
\
\ This word should return a ptr to the substring starting at start and
\ continuing for n chars.
\
\ Usage: stringvar 8 4 MID$   Meaning: Return 4 chars starting at 8th char
\                                      in string stringvar.
\
\ Note:  first    = ptr to 1st char in string
\        start    = index into string; the point at which you want to start
\                   looking at the substring.
\        n        = The # of chars you want back.
\
\        substring.ptr.start is a pointer to the first character in the
\                            resulting substring.
\
\  In the example, start at 8th char in string and continue for 4 chars.
\  8 is start.  4 is n.
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF
    start 0> NOT
    IF          ( user has passed us a non-positive starting position )
      CR
      ." Error in MID$ -- Requested substring has start BEFORE beginning "
      ." of string." CR
      ." Starting value you passed to MID$ is: " start . CR
      ." MID$ Aborting."   0SP
      ABORT
    THEN

    n 0> NOT
    IF          ( user has passed us a non-positive character count )
      CR
      ." Error in MID$ -- You cannot have a non-positive character count!" CR
      ." Character count you passed to MID$ is: " n . CR
      ." MID$ Aborting."   0SP
      ABORT
    THEN

    ( Check to see if start is past end of string )

    first Length  start
    < IF       ( if TRUE, start IS past end of string )

\       0 0    ( Return a null string )
\ Notice that the above line is commented out.  That line shows the "true"
\ behavior of MID$ on most systems.  I prefer to get an error message here
\ because to me it is too difficult to track down what goes wrong in a
\ program due to some wild value for a pointer or whatever.  So this
\ version of the StringPkg stops you dead with an error here.  Change it
\ if you like!  Perhaps rewrite so that user.debug flag can have levels and
\ based on the level set by the user, this package in general will behave
\ strictly about such situations or ignore them all-together.

        CR
        ." Error in MID$ -- Requested substring has start past end of "
        ." string." CR
        ." Current length of string = " first Length . CR
        ." You requested start of substring at character position: "
        start . CR
        ." MID$ Aborting."  0SP
        ABORT
    THEN
.THEN

    ( Start is NOT past end of string )

    first start + 1-          ( compute addr of start of substring )
    first Length              ( length of source string )
    start - 1+                ( # of chars from start to end of string )
    DUP n <=
    IF                        ( Not n chars left in substring starting )
                              ( at start.  Return from start on to end )
    ELSE
       DROP n                 \ There are at least n chars left
    THEN  ;


: Concat { ptr1  ptr2  -- }

\ Concatenate string2 to the end of string1.  The resulting string is
\ stored in string1.
\
\ Usage:  xyz0 abc Concat
\ Result: String xyz becomes string xyz immediately followed by the
\         string value of abc.  The resulting string is of the same type
\         as xyz0.
\
\ Note:   ptr1  = ptr to 1st char in string1
\         ptr2  = ptr to 1st char in string2
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF

  ( First determine is string1 has a max. length big enough to hold result )
  ( of concatentation. )

  ptr2 Length ptr1 $Extend?
  NOT IF                          \ TRUE if string1 too short
      CR
      ." Error in Concat -- Result of Concat would be too long." CR
      ." Maximum length of destination string is: "
      ptr1 Max.Length . CR
      ." Proposed concatenation would require string of length: "
      ptr1 Length  ptr2  Length  + . ." bytes." CR
      ." Concat Aborting." 0SP  ABORT
  THEN
.THEN

  ( We know that the destination string [string1] is long enough )
  ( Let's do the concatenation. )

  ptr2                        \ Start moving from this spot
  ptr1 DUP Length +           \ Put the characters here (1 past end of str)
  ptr2 Length                 \ Move this many characters
  CMOVE                       \ Move the data

  ( Now we must fix up the little stuff concerning the new string )
  ( What we do depends on the type of string that string1 is )

  ptr2 Length ptr1 $Extend  ;  \ Bump length of str1 by length(str2) bytes


\ Some synonyms for Concat (useable only within colon definitions!)
\ : $+      compile Concat ; immediate
\ : $Concat compile Concat ; immediate

\ Synonyms for Concat useable anywhere, but slightly slower than Concat.
: $+      Concat ;
: $Concat Concat ;


: $! { source.ptr  dest.ptr  -- }

\ Pronounced "String Store"
\
\ This word will take a ptr to the first character of a string and copy
\ it to the string pointed to by dest.ptr.  It is a string assignment word.
\
\ Usage:    45 StringVar abc
\           astring abc $!  ( where astring already has a value )
\
\ Note:     source.ptr = ptr to 1st char in source string
\           dest.ptr   = addr to store the first char of source string at
\                        This MUST be the addr returned from a NULL.STRING,
\                        SHORT.STRING, or LONG.STRING!!
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF

  ( First see if destination string can contain the source string )

  dest.ptr Max.Length                   ( Max. Length of dest. string )
  source.ptr Length                     ( Length of source string )
  < IF                                  ( TRUE if source string too long )
      CR ." Error in $! -- Source string too long for destination string."
      CR ." Maximum length of destination string is: "
      dest.ptr Max.Length .
      CR ." Current length of source string is " source.ptr Length .
      CR ." $! Aborting." 0SP  ABORT
    THEN
.THEN

  source.ptr dest.ptr source.ptr Length
  CMOVE                             \ Perform the assignment

  source.ptr Length  dest.ptr
  $Extend  ;                        \ Bump length to correct value


: $$!   { str1 index len str2 -- }
\
\  Pronounced: "String to String Store" or "String String Store"
\
\  Store 'length' characters starting from the indexth position in str1
\  into str2.
\
\  Usage: string1 15 4 string2 $$!
\
\  str1  =  Ptr to 1st character of string1
\  index =  Offset from 1st character of string1 to use as starting point
\           of source string
\  len   =  # of characters to store into string2
\  str2  =  Ptr to 1st character of string2
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF

  index 1 <
  IF
    CR ." Error in $$! -- Start position in string can't be before string!"
    CR ." Start position you requested: " index .
    CR ." $$! Aborting." 0SP  ABORT
  THEN

  index str1 Length >
  IF
    CR ." Error in $$! -- Start position can't be past end of string!"
    CR ." Start position you requested: " index .
    CR ." Current length of source string: " str1 Length .
    CR ." $$! Aborting." 0SP  ABORT
  THEN

  len 0 <
  IF
    CR ." Error in $$! -- You cannot copy a negative # of bytes!"
    CR ." Number of bytes requested to be copied: " len .
    CR ." $$! Aborting." 0SP  ABORT
  THEN
.THEN

  \ Note this code is compiled whether DEBUGGING or NOT!!!

  str1 Length index - 1+      \ # of chars from index to end of string
  DUP len <
  IF                          \ TRUE if asked for more chars than can have
    -> len                    \ Change len to # chars left from index on
  ELSE
    DROP
  THEN

[  String.Debug @
   User.Debug   @
   OR           ]
.IF

  ( See if destination string can contain the source string )

  str2 Max.Length                       ( Max. Length of dest. string )
  len                                   ( Length of source string )
  < IF                                  ( TRUE if source string too long )
      CR ." Error in $$! -- Source string too long for destination string."
      CR ." Maximum length of destination string is: "
      str2  Max.Length .
      CR ." You requested to transfer " len . ." bytes of source string."
      CR ." $$! Aborting." 0SP  ABORT
    THEN
.THEN

  ( Now we know we can move len bytes from str1[index] into str2. )

  str1 index + 1- str2 len CMOVE    \ Perform the assignment
  len str2 $Extend   ;              \ Bump current length field


: $Null ( addr.of.1st.char -- )
\
\  Pronounced "String Null"
\
\  Make string a null string.
\
\  Usage:   45 $var xyz
\           " abcd" xyz $LIT!
\           (now, xyz has the value "abcd")
\           xyz $Null
\           ( Now, xyz is a null string )
\
   DUP @$Type
   CASE     NULL.STRING  OF
                 0 SWAP C!         \ Put NULL at end of string
               ELSE
            SHORT.STRING OF
                 2- 0 SWAP C!      \ Update current length field
               ELSE
            LONG.STRING  OF
                 3- 0 SWAP W!      \ Update current length field
               ELSE
                 CR ." $Null Error.  Destination string has invalid type!"
                 CR ." Address of destination string: " .
                 CR ." $Null Aborting." 0SP  ABORT
  ENDCASE  ;


: $LIT! { source.ptr  dest.ptr -- }
\
\  Pronounced: "String Literal Store" or "String Lit Store"
\
\  Store a counted literal string into dest.ptr (addr of 1st char of a
\  string of type StringVar or StringVar0 .
\
\  source.ptr = pointer to a count byte of a normal JForth counted string
\  dest.ptr   = pointer to 1st char of a StringVar or StringVar0
\
\  Usage:    50 StringVar MyString
\            " This is a string" MyString $LIT!
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF

  ( First see if destination string can contain the source string )

  dest.ptr Max.Length                   ( Max. Length of dest. string )
  source.ptr C@                         ( Length of source string )
  < IF                                  ( TRUE if source string too long )
      CR ." Error in $LIT! -- Source string too long for dest. string."
      CR ." Maximum length of destination string is: "
      dest.ptr Max.Length .
      CR ." You requested to transfer " source.ptr C@ .
         ." bytes of source string."
      CR ." $LIT! Aborting." 0SP  ABORT
    THEN
.THEN

  source.ptr 1+               \ Start moving from this spot
  dest.ptr                    \ Put the characters here
  source.ptr C@               \ Move this many characters
  CMOVE                       \ Move the data

  source.ptr C@ dest.ptr
  $Extend   ;                 \ Bump current length field


: Val ( ptr.to.1st.char -- d TRUE | FALSE )
\
\   ptr.to.1st.char = the address of the first character of a string.
\
\   Take that string and try to convert it to a number in the current base.
\   If successful, a TRUE is returned on top with a double length number
\   (the number that was represented by the string) under it.
\   If not successful, a FALSE is returned with nothing underneath.
\
\   Usage : 30 StringVar MyString                 ( Create MyString )
\           MyString 20 Get.String                ( Get a String )
\           MyString Val                          ( Convert to a number )
\           IF
\             you have a double length number to use here
\           ELSE
\             ." Bad number conversion in current base!"
\             ( But pick nicer wording than that! )
\           THEN
\
\   For those of you who don't need a double number to work with (you know
\   the value is < 2^32), just do a drop upon returning from VAL and that
\   will throw away 32 bits of the value 0, leaving a single length number
\   on the stack.
\
\   This only works for counted strings that have a maximum length of 255
\   or less!
\
    @$Info
    DUP
    LONG.STRING =
    IF
      CR ." Error in VAL.  Only SHORT.STRINGs can be used with VAL!"
      CR ." You attempted to use VAL with a LONG.STRING."
      CR ." VAL Aborting." 0SP  ABORT
    THEN

    NULL.STRING =
    IF
      CR ." Error in VAL.  Only SHORT.STRINGs can be used with VAL!"
      CR ." You attempted to use VAL with a NULL.STRING."
      CR ." VAL Aborting." 0SP  ABORT
    THEN

    ( We know type.flag indicates a SHORT.STRING.  This is good. )

    DROP                      \ Toss the current length

    ( We must switch type flag and current length bytes for a second ... )

    DUP                       \ addr1 addr1  [ addr of 1st byte ]
    2-                        \ addr1 addr.cur.length
    C@ DUP                    \ addr1 curr.length curr.length
    PAD C!                    \ Put curr.length at PAD
                              \ Stack: addr1 curr.length
    PAD 1+ SWAP               \ Stack: addr1 pad+1 curr.length
    CMOVE                     \ Move string to PAD + 1

    PAD                       \ Addr of count byte

    ( WARNING!!  IF YOU HAVE A STRING WITH A HEX DIGIT STRING IN IT AND )
    ( ---------  THE CURRENT BASE IS 16 [I.E. YOU ARE IN 'HEX' MODE],   )
    (            NUMBER? WILL RETURN _FALSE_ IF THE DIGIT STRING IS NOT )
    (            CAPITALIZED!  IS THIS A BUG IN NUMBER? DELTA RESEARCH? )
    (            SO, UPPER.CASE YOUR STRING BEFORE CALLING VAL IF YOU   )
    (            ARE IN A BASE > 10.                                    )
    ( Also:  No leading + sign is tolerated by NUMBER?  I find this     )
    (        undesirable, but I didn't write JForth.  Delta Research?   )
    (        Any chance of changing this?  It's in the kernal.          )

    NUMBER?  ;


: Get.Number { 2 lo 2 hi -- d TRUE OR err# FALSE }
\
\   Get a signed number from user that is between lo and hi
\   inclusive.
\
\   If the user's number attempt is invalid, err# will have a code
\   indicating the nature of the problem with the user's input.
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF
    hi lo D<
    IF
       >newline
          ." Error in Get.Number.  Lo must be LESS than hi." cr
       cr ." NOTE:  If both lo and hi are negative, they should be"
          ." put on the stack with"
       cr ."        the MOST negative number UNDER the LEAST"
          ."  negative number." cr
       cr ." You sent lo = " lo d. ."  and hi = " hi d.
       cr ." Get.Number Aborting."  0SP  ABORT
    THEN
.THEN

    GetNumWorkStr
    19                         \ We want only 19 digits here since a 64-bit
                               \ value can be a max of 19 digits.
    Get.String
    GetNumWorkStr Val          \ Try to convert to number in current base
    IF                         \ TRUE if number is good so far
       hi 1. D+
       D< NOT
       IF                      \ TRUE if value > hi
          Value.Too.Big FALSE
       ELSE                    \ We get here if value <= hi
          GetNumWorkStr Val DROP
          lo
          D<
          IF                   \ TRUE if value is below lo
             Value.Too.Small
             FALSE
          ELSE                 \ We're OK.  Value checks out.
             GetNumWorkStr Val
             DROP              \ This HAS to succeed, it just did above
             TRUE
          THEN
       THEN
    ELSE                       \ We get here if not a number in current base
       Not.A.Number FALSE
    THEN ;


\ Synonym(s) for Get.Number
: Get#  Get.Number ;
: #Get  Get.Number ;


: Instr { adr1 n1 adr2 n2 -- adr.in.str1 | FALSE }
\
\   Pronounced "In String" (Hey, I got it from BASIC, ok?)
\
\   This works pretty much like its BASIC counterpart.  "It searches for
\   string2 within a larger string, leaving the address within string1
\   that matches if found, or FALSE if not found."  (quoted from P 181
\   of JForth V1.2 manual).  See match? in the manual, but this takes
\   different stack items!
\
\   The BASIC Instr function searches for the first occurrence of string2
\   in string1, and returns the position at which the match is found.
\
\   adr1   =  address of 1st character in string1.
\   n1     =  # of chars to take from string1 starting at adr1.
\   adr2   =  address of 1st character in string2.
\   n2     =  # of chars to take from string2 starting at adr2.
\    
    adr1 n1 adr2 n2 match? ;


: $COMPARE    { adr1 adr2 #bytes -- 0 | 1 | -1 }
\
\   Compare string starting at adr1 to string at adr2 for #bytes bytes.
\   See JForth manual for documentation on COMPARE (P. 124 for V 1.2)
\
\   Usage: string1 string2 5 $COMPARE       compare string1 to string2 for
\                                           5 bytes.

    adr1 adr2 #bytes COMPARE ;


: Lower.Case ( adr -- )
\
\   Converts chars at adr to lower case for total of count characters.
\
    DUP Length
    0 DO
        DUP                \ Save a copy of adr for next time through loop
        I +                \ Address of nth character in string
        DUP
        C@                 \ The ASCII code of the nth character in string
        DUP ?LETTER        \ Is it a letter?  If not, leave it alone!
        IF                 \ TRUE if it is a letter
          5 SET-BIT        \ Set the fifth bit (making it lower case)
          SWAP C!          \ Store the new character back into string
        ELSE               \ It was not a letter, leave it alone
          2DROP            \ Toss the unchanged value and its address
        THEN
      LOOP DROP   ;        \ We no longer need the adr, we're done!


: Upper.Case ( adr -- )
\
\   Converts chars at adr to upper case for total of count characters.
\
    DUP Length
    0 DO
        DUP                \ Save a copy of adr for next time through loop
        I +                \ Address of nth character in string
        DUP
        C@                 \ The ASCII code of the nth character in string
        DUP ?LETTER        \ Is it a letter?  If not, leave it alone!
        IF                 \ TRUE if it is a letter
          5 CLR-BIT        \ Set the fifth bit (making it lower case)
          SWAP C!          \ Store the new character back into string
        ELSE               \ It was not a letter, leave it alone
          2DROP            \ Toss the unchanged value and its address
        THEN
      LOOP DROP   ;        \ We no longer need the adr, we're done!


: $C!  { char str1 index -- }
\
\  Pronounced "String Character Store"  or  "String C Store"
\
\  Store char into str1 at offset.
\
\  Usage: ASCII G  xyz  4  $C!   -->  Store "G" at 4th position in xyz.
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF

   index
   str1 Length >
   IF
     CR ." $C! Error.  Index past end of string!"
     CR ." You passed index: " index .
     CR ." Length of string passed: " str1 Length .
     CR ." $C! Aborting."  0SP  ABORT
   THEN

   index 1 <
   IF
     CR ." $C! Error.  You cannot have an index less than 1!"
     CR ." You passed index: " index .
     CR ." $C! Aborting."  0SP  ABORT
   THEN
.THEN

   + 1- C! ;


: $C@   { str1 index -- char }
\
\  Pronounced "String Character Fetch"  or  "String C Fetch"
\
\  Return the character at string1[index] (the indexth char of string1).
\

[  String.Debug @
   User.Debug   @
   OR           ]
.IF

   index
   str1 Length >
   IF
     CR ." $C@ Error.  Index past end of string!"
     CR ." You passed index: " index .
     CR ." Length of string passed: " str1 Length .
     CR ." $C@ Aborting."  0SP  ABORT
   THEN

   index 1 <
   IF
     CR ." $C! Error.  You cannot have an index less than 1!"
     CR ." You passed index: " index .
     CR ." $C! Aborting."  0SP  ABORT
   THEN
.THEN

   str1 index + 1- C@ ;


Base !                     \ Put base back to what it was before we started.

kallaus@leadsv.UUCP (Jerry Kallaus) (04/15/89)

[]

Thanks for posting your string package.  Looks good!

I noticed your use of JForth assemlby code.  With JForth's
forward Motorola format assembler, the only way that I've
found to get inline code generation is the following:

ASM 3+
    addq.l  #3,tos
    FORTH{ both }
END-CODE

where "both" is the JForth word that indicates the current word
being defined may compile inline code when referenced if it's
size is no more than specified in the global variable max-inline,
otherwise "call" the word.  Actually, there's more involved, but
that's the basic idea.

Anyway, this seems to work; if any JForth'ers here see anything
questionable about this, I'd like to know about it.
-- 
Jerry Kallaus         {pyramid.arpa,ucbvax!sun!suncal}leadsv!kallaus
(408)742-4569
     "Funny, how just when you think life can't possibly get
      any worse, it suddenly does." - Douglas Adams