Skip to content

Strings & text

Strings make small programs feel human. They also sit at important boundaries: the console, forms, files, database values, network messages, menus, and drag items all carry text. D/BASIC keeps the familiar $ toolkit while making the byte/text boundary explicit enough to travel safely.

Declare and join strings

DIM GivenName$
DIM FamilyName AS STRING

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

+ concatenates two strings. D/BASIC does not silently turn a number into text or text into a number inside an expression; use STR$ or VAL where conversion is intentional.

String literals use double quotes and have no backslash escape language:

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; empty text refuses
one byte from a code CHR$(Code%) one-byte string; 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%]) 1-based bounded slice
find INSTR([Start%,] Text$, Needle$) 1-based position, or 0
ASCII uppercase/lowercase UCASE$, LCASE$ transformed copy
trim ASCII whitespace LTRIM$, RTRIM$ transformed copy
spaces SPACE$(Count%) repeated spaces
repeated byte STRING$(Count%, Pattern) first byte of pattern repeated
number to text STR$(Number) canonical numeric presentation
text to number VAL(Text$) numeric value

The slice functions clamp to what exists. A start past the end produces an empty string; a requested length past the end returns the available suffix.

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 bounded formats:

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 searching should begin later:

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

An empty needle is found at the requested valid start. A start below 1 is treated as 1. For an empty needle, the position immediately after the final byte—LEN(Text$) + 1—is also valid; a larger start returns 0. For a nonempty needle, a start beyond the text returns 0.

Replace without changing length

MID$ can also be an assignment target:

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

This overwrites the available span; it does not insert bytes or change the destination length. The replacement is clipped by the explicit length and the remaining room in the destination.

That makes a pre-sized text buffer useful when its final bound is known:

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

For ordinary prose, clear concatenation is usually better. Reach for a pre-sized buffer when the fixed columns are themselves part of the output format.

Conversion should be visible

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

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

STR$ retains the language's canonical sign and numeric formatting rules; a positive result may include a leading sign space, which is why compact labels often use LTRIM$. Use PRINT USING when the presentation needs a declared picture rather than ad-hoc trimming.

Never use VAL as validation for an untrusted structured document. Parse the bounded format you actually accept and reject leftover or missing fields.

STRING values and fixed record text are different

An ordinary STRING is variable-length managed storage. A STRING * n field inside a TYPE is exactly n inline bytes:

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

Writing a fixed field truncates longer input and space-pads shorter input. That is valuable in a packed record or wire-shaped value; it is rarely the right choice for editable prose. LEN(Row) measures the complete packed record, while LEN(Row.Title) measures the declared field width after it is loaded as text.

Bytes, UTF-8, and characters

D/BASIC's core string operators are deliberately byte-oriented:

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

This gives files, protocols, digests, retro text, and every runtime one exact answer. It also means a byte slice can split a multi-byte UTF-8 character.

For human text, keep committed UTF-8 whole. Let TextCommit and Composition events, widgets, DATA values, and native bridges carry semantic text; do not use MID$ as a cursor engine for an arbitrary language. A runner may upgrade the font used to draw that text, but it may not change the stored bytes.

Limits and refusal

A source string literal is bounded to 32,767 bytes. A live string is bounded by the DBC/runtime string contract and available admitted memory. Negative fill lengths, an empty STRING$ pattern, ASC(""), byte codes outside 0..255, or a result beyond the string ceiling refuse instead of wrapping or truncating silently.

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

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

Done:
END

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 intentionally boring in the best way: every runner performs the same byte operations and produces the same report.


Next: Expressions →