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.