Skip to content

Strings & text

This page covers text in d/BASIC: how to declare strings, join them, measure and slice them, search inside them, turn numbers into text and back, and show them safely. After reading it you will know the whole $ toolkit and the one rule that matters most: d/BASIC counts positions in a string in bytes, not characters.

Strings make small programs feel human. They also sit at every boundary a program has: the console, forms, files, database values, network messages, menus, and dragged items all carry text. d/BASIC keeps the familiar $ functions from classic BASIC, and is explicit about where text is bytes so that a string can travel between machines safely.

Declare and join strings

DIM GivenName$
DIM FamilyName AS STRING

GivenName$ = "ADA"
FamilyName = "LOVELACE"
PRINT GivenName$ + " " + FamilyName

+ joins two strings. d/BASIC does not quietly turn a number into text, or text into a number, in the middle of an expression; use STR$ or VAL where you mean to convert.

String literals use double quotes and have no backslash escapes such as \n:

Quote$ = CHR$(34)
PRINT Quote$ + "HELLO" + Quote$

The working toolkit

Need Form Result
byte length LEN(Text$) INTEGER byte count
first byte value ASC(Text$) 0..255; an empty string is an error
one byte from a code CHR$(Code%) one-byte string; the code must be 0..255
left slice LEFT$(Text$, Count%) at most Count% bytes
right slice RIGHT$(Text$, Count%) at most Count% bytes
middle slice MID$(Text$, Start% [, Count%]) slice starting at position Start% (the first byte is 1)
find INSTR([Start%,] Text$, Needle$) position of the first match (counting from 1), or 0
ASCII uppercase/lowercase UCASE$, LCASE$ a changed copy
trim ASCII whitespace LTRIM$, RTRIM$ a changed copy
spaces SPACE$(Count%) that many spaces
repeated byte STRING$(Count%, Pattern) the first byte of the pattern, repeated
number to text STR$(Number) the number in its standard spelling
text to number VAL(Text$) the numeric value

The slice functions never reach past what exists. A start past the end gives an empty string; a length that runs past the end gives whatever is left.

Code$ = "DBASIC-ALPHA"
PRINT LEFT$(Code$, 6)          ' DBASIC
PRINT RIGHT$(Code$, 5)        ' ALPHA
PRINT MID$(Code$, 8, 99)      ' ALPHA
PRINT INSTR(Code$, "-")       ' 7

Search before splitting

INSTR makes a clear parser for small formats with a known shape:

Pair$ = "theme=amber"
EqualsAt% = INSTR(Pair$, "=")

IF EqualsAt% > 0 THEN
  Key$ = LEFT$(Pair$, EqualsAt% - 1)
  Value$ = MID$(Pair$, EqualsAt% + 1)
  PRINT UCASE$(Key$); " -> "; Value$
END IF

Use the three-argument form when the search should begin later in the text:

SecondSlash% = INSTR(6, "docs/dbasic/index", "/")

An empty needle (the text being searched for) is found at whatever valid start you asked for. A start below 1 is treated as 1. For an empty needle, the position just after the final byte, LEN(Text$) + 1, also counts as valid; a larger start returns 0. For a non-empty needle, a start beyond the end of the text returns 0.

Replace without changing length

MID$ can also be the target of an assignment:

State$ = "RED ALERT"
MID$(State$, 5, 5) = "READY"
PRINT State$                 ' RED READY

This overwrites the bytes that are already there; it does not insert bytes or change the length of the destination. The replacement is cut short by the length you give and by the room left in the destination.

That makes a pre-sized text buffer useful when you know its final size:

Line$ = SPACE$(20)
MID$(Line$, 1) = "NAME"
MID$(Line$, 12) = "READY"
PRINT Line$

For ordinary prose, plain joining with + is usually clearer. Reach for a pre-sized buffer when fixed columns are themselves part of the output.

Conversion should be visible

Count% = 12
Message$ = "COUNT=" + LTRIM$(STR$(Count%))
PRINT Message$

Entered$ = "41.5"
Reading! = VAL(Entered$)
PRINT Reading! + .5

STR$ follows the language's standard rules for signs and number formatting; a positive number gets a leading space where a minus sign would go, which is why compact labels often use LTRIM$. Use PRINT USING when the output needs a declared layout rather than ad-hoc trimming.

Never rely on VAL to check input you do not trust, such as a structured document from outside your program. Parse the exact format you accept and reject anything left over or missing.

STRING values and fixed record text are different

An ordinary STRING is variable-length storage that the language manages for you. A STRING * n field inside a TYPE record is exactly n bytes stored in place:

TYPE CatalogRow
  Code AS STRING * 8
  Title AS STRING * 40
END TYPE

Writing to a fixed field cuts longer input short and pads shorter input with spaces. That is valuable in a packed record or a value shaped for a wire format; it is rarely the right choice for prose someone will edit. LEN(Row) measures the whole packed record, while LEN(Row.Title) measures the declared field width once the field is loaded as text.

Bytes, UTF-8, and characters

d/BASIC's core string functions deliberately work in bytes:

  • LEN, LEFT$, RIGHT$, MID$, and INSTR count byte positions;
  • comparison orders strings byte by byte;
  • ASC, CHR$, and STRING$ work with byte values; and
  • case conversion and trimming use ASCII rules.

This gives files, protocols, checksums, retro text, and every d/OS runtime (the program that runs your compiled code) one exact answer. It also means a byte slice can cut a multi-byte UTF-8 character in half.

For text people type, keep whole UTF-8 characters together. Let the TextCommit and Composition events (what the window system sends when someone finishes typing text, or is still composing it), widgets, DATA values, and the bridges to native code carry text as text; do not use MID$ to move a cursor through text in an arbitrary language. A runtime may draw the text with a better font, but it may not change the stored bytes.

Limits, and what happens when you cross them

A string literal in the source may hold up to 32,767 bytes. A string in a running program is limited by the string size the compiled program file, .dbc, and the runtime agree on, and by the memory the program was accepted with. A negative fill length, an empty STRING$ pattern, ASC(""), a byte code outside 0..255, or a result larger than the string ceiling is declined, and the program is told why, rather than wrapping around or quietly cutting the result short.

ON REFUSAL GOTO BadCharacter
PRINT ASC("")
GOTO Done

BadCharacter:
  PRINT "NO FIRST BYTE. RULE "; ERR
  RESUME Done

Done:
END

ON REFUSAL GOTO names the line to jump to when an operation is declined, and ERR holds the error code that says what went wrong.

A small text report

DIM Raw$(1 TO 3)
Raw$(1) = "  amber"
Raw$(2) = "BLUE  "
Raw$(3) = "  Green  "

FOR I% = LBOUND(Raw$) TO UBOUND(Raw$)
  Clean$ = UCASE$(LTRIM$(RTRIM$(Raw$(I%))))
  PRINT RIGHT$("   " + STR$(I%), 3); "  "; Clean$
NEXT I%
END

The result is boring in the best way: every d/OS runtime performs the same byte operations and prints the same report.


Next: Expressions →