NCL 204: Taking Strings Apart
In NCL 203, we learned how to find where one string appears inside another.
For example:
#separator r0
#message "EMMA:hello"
SFIND #separator #message ":"
D.TXT #separator
D.BLT
The program displays:
4
because the : begins at position 4:
EMMA:hello
0123456789
^
4
Knowing where something appears is useful.
But what if we want the text on either side of it?
Extracting part of a string
NCL provides SSUB for extracting part of a string.
For example:
#name s0
#message "EMMA:hello"
SSUB #name #message 0 3
D.TXT #name
D.BLT
The program displays:
EMMA
SSUB takes a starting position and an ending position:
SSUB #name #message 0 3
Both positions are inclusive.
For:
EMMA:hello
0123456789
^^^^
positions 0 through 3 contain:
EMMA
So SSUB stores that part of the original string in #name.
The original string is not changed.
Using a position we found
Hardcoding positions works when we already know exactly where everything is.
But we know how to let the program find a position for itself.
Consider:
#separator r0
#end r1
#name s0
#message "EMMA:hello"
SFIND #separator #message ":"
MOVE #end #separator
DEC #end
SSUB #name #message 0 #end
D.TXT #name
D.BLT
Let's follow it.
First:
SFIND #separator #message ":"
finds the : at position 4:
#separator = 4
We don't want the : itself, so the last position we want is one position before it.
MOVE #end #separator
DEC #end
produces:
#end = 3
Then:
SSUB #name #message 0 #end
extracts positions 0 through 3:
EMMA:hello
^^^^
and stores:
EMMA
in #name.
Now change:
#message "EMMA:hello"
to:
#message "ALICE:hello"
The separator has moved from position 4 to position 5, but the program still extracts:
ALICE
We don't need to know where the separator will be when we write the program.
SFIND discovers that position, and SSUB uses it.
Counting from the end
String positions can also be negative.
A negative position counts backward from the end of the string.
The final character is at position -1.
The character before it is at position -2, and so on.
For example:
EMMA:hello
0123456789
^
-1
This makes -1 useful when we want to extract everything through the end of a string.
#text s0
#message "EMMA:hello"
SSUB #text #message 5 -1
D.TXT #text
D.BLT
The program displays:
hello
Position 5 is the h.
Position -1 is the final o.
So:
SSUB #text #message 5 -1
extracts everything from position 5 through the final character.
We don't need to know how long the string is.
For example:
#message "EMMA:hello there!"
with:
SSUB #text #message 5 -1
extracts:
hello there!
Extracting both sides
We can now combine these techniques to extract both sides of our separator.
#separator r0
#leftEnd r1
#rightStart r2
#name s0
#text s1
#message "EMMA:hello"
SFIND #separator #message ":"
MOVE #leftEnd #separator
DEC #leftEnd
MOVE #rightStart #separator
INC #rightStart
SSUB #name #message 0 #leftEnd
SSUB #text #message #rightStart -1
D.TXT #name
D.TXT " / "
D.TXT #text
D.BLT
The program displays:
EMMA / hello
For our original string:
EMMA:hello
0123456789
^
separator = 4
the program calculates:
#leftEnd = 3
#rightStart = 5
That gives us two ranges:
EMMA:hello
^^^^ ^^^^^
The first SSUB extracts positions 0 through 3:
EMMA
The second extracts positions 5 through -1:
hello
We've taken one structured string and split it into two useful strings.
There is, however, an important problem with this program.
When the indexes cross
SSUB has an unusual behavior when its starting position comes after its ending position.
It does not simply return an empty string.
Consider:
#result s0
#text "ABCDEFG"
SSUB #result #text 5 2
D.TXT #result
D.BLT
The positions are:
ABCDEFG
0123456
Normally, we read an SSUB range from left to right.
But here, the starting position is 5 and the ending position is 2.
When the indexes cross like this, SSUB treats the range differently.
It keeps the characters on either side:
ABCDEFG
^^^ ^^
and produces:
ABCFG
You can think of this as an exclusion range. Instead of selecting the characters between the two positions in the usual direction, the characters between them are excluded.
This behavior can be useful.
It can also produce surprising results when positions are calculated by a program.
A separator at the beginning
Consider:
:hello
The separator is at position 0:
:hello
012345
^
Our splitting program calculates the end of the left side with:
MOVE #leftEnd #separator
DEC #leftEnd
Starting from 0 and decrementing gives:
#leftEnd = -1
The extraction then becomes:
SSUB #name #message 0 -1
But position 0 through position -1 means the entire string.
Instead of an empty name, we get:
:hello
Our calculation produced a valid SSUB range, but not the range we intended.
If our format requires text before the separator, we need to check for this case before extracting anything.
A separator at the end
The other end has a similar problem.
Consider:
EMMA:
The positions are:
EMMA:
01234
^
The separator is at position 4.
Our program calculates the beginning of the right side with:
MOVE #rightStart #separator
INC #rightStart
which gives:
#rightStart = 5
But the original string ends at position 4.
There is no text after the separator.
Trying to use:
SSUB #text #message #rightStart -1
does not give us the ordinary non-empty range our program expects.
Once again, we should check the position before trying to extract the pieces.
Checking the separator first
For a string in the form:
NAME:text
we want the separator to satisfy three conditions:
- it must exist;
- it must not be the first character;
- it must not be the last character.
We already know how to check all three.
First, find the separator:
SFIND #separator #message ":"
If it wasn't found, SFIND returns -1:
BEQ $invalid #separator -1
If it is at the beginning, its position is 0:
BEQ $invalid #separator 0
To find the position of the final character, we can use the string length:
SLEN #length #message
MOVE #last #length
DEC #last
For:
EMMA:hello
the length is 10, so the final position is 9.
Then we can check whether the separator is there:
BEQ $invalid #separator #last
Putting those checks together:
#separator r0
#length r1
#last r2
#message "EMMA:hello"
SFIND #separator #message ":"
BEQ $invalid #separator -1
BEQ $invalid #separator 0
SLEN #length #message
MOVE #last #length
DEC #last
BEQ $invalid #separator #last
D.TXT "Valid"
JUMP $done
$invalid
D.TXT "Invalid"
$done
D.BLT
Try several messages:
| Message | Separator | Result |
|---|---|---|
EMMA:hello |
4 | Valid |
ALICE:test |
5 | Valid |
:hello |
0 | Invalid |
EMMA: |
4 | Invalid |
hello |
-1 | Invalid |
Once these checks have passed, we know there is at least one character on both sides of the separator.
Only then do we calculate the ranges and use SSUB.
Splitting safely
Now we can put everything together:
#separator r0
#length r1
#last r2
#leftEnd r3
#rightStart r4
#name s0
#text s1
#message "EMMA:hello"
SFIND #separator #message ":"
BEQ $invalid #separator -1
BEQ $invalid #separator 0
SLEN #length #message
MOVE #last #length
DEC #last
BEQ $invalid #separator #last
MOVE #leftEnd #separator
DEC #leftEnd
MOVE #rightStart #separator
INC #rightStart
SSUB #name #message 0 #leftEnd
SSUB #text #message #rightStart -1
D.TXT "Name: "
D.TXT #name
D.TXT " / Text: "
D.TXT #text
JUMP $done
$invalid
D.TXT "Invalid message"
$done
D.BLT
With:
#message "EMMA:hello"
the program displays:
Name: EMMA / Text: hello
Try changing the message to:
#message "ALICE:Good morning!"
The separator moves, the lengths of both pieces change, and the program still extracts them correctly.
Then try the invalid forms:
#message ":hello"
#message "EMMA:"
#message "hello"
Each should be rejected before SSUB tries to split it.
Try it
Start with the completed program and change the format from:
NAME:text
to something using a different separator.
For example:
filename.txt
Use:
.
as the separator and extract:
filename
and:
txt
Then try names of different lengths:
hello.ncl
STARTUP.ncl
notes.txt
The program should not depend on the separator appearing at any particular position.
Finally, try:
.hidden
and:
filename.
Think about why the same boundary checks are useful for these strings.
SSUB is powerful because its boundaries can come from values the program calculates while it runs. But that also means those boundaries are worth checking before we use them.
We can now find boundaries inside a string and extract the pieces on either side.
In NCL 205: Comparing and Interpreting Strings, we'll start making decisions based on what those extracted strings contain.