UNIFILE II is a general-purpose flat-file database manager that stores records in a large string buffer of 28,000 characters and maintains a sorted index using a binary search tree encoded in the string variable Y$. Records are composed of typed fields, where field types are named in a 50-element string array A$, and fields can reference a type by embedding a “#” marker followed by a type-code character. The system supports inserting, searching (normal, special substring, first-character, and multiple-item), amending, and deleting records, with deletion implemented by a “telescope” routine that physically compacts the buffer and updates all pointers. The program saves and verifies itself to tape, preserving both the data buffer and index on reload.
Program Analysis
Program Structure
The program is organized as a menu-driven dispatcher at lines 1000–1140, which branches to four main subsystems via GO SUB. Each subsystem is clearly delimited by REM banners.
| Lines | Subsystem |
|---|---|
| 1000–1200 | Main menu, initialization, save/verify, stop |
| 1210–1430 | File structure setup (DIM, pointer init) |
| 1440–1650 | Normal data entry loop |
| 1660–1910 | Binary-search insert into sorted index |
| 1930–2160 | Amend/change an existing entry |
| 2180–2740 | Search (normal, III, SSS, MMM modes) |
| 2780–2900 | Utility subroutines: input, display |
| 2920–3090 | Special (substring) search |
| 3130–3390 | Telescope (delete + compact) routine |
| 3400–3520 | Item-type name editor |
Data Storage Model
All record data is stored sequentially in a single large string B$ dimensioned to 28,000 characters (line 1350). Each field within a record is a length-prefixed string: the first byte is CHR$ (LEN field), followed by the field content. Records are terminated by a field whose second byte is "*". The variable P tracks the next free position in B$.
The sorted index is kept in string Y$, which holds 2-byte big-endian pointers into B$ for each record. Record S starts at position 256*CODE Y$(2*S-1) + CODE Y$(2*S), exactly what FN A() computes.
Field types are held in A$(50,10), a 50-element fixed-width string array. A field ending with "#" followed by a type-code byte uses the type name from A$ as a label when displaying.
Key Function Definitions
DEF FN A() = 256*CODE Y$(2*S-1) + CODE Y$(2*S)— decodes the 2-byte big-endian pointer for sorted slotSinto an absolute offsetCintoB$.DEF FN A$() = B$(C TO C+CODE B$(C)-1)— returns the length-prefixed field string at positionC, including the length byte.
Binary Search Insertion (Lines 1730–1910)
New records are inserted into the sorted index using a binary search. The algorithm computes POWER = INT(LN(N-1)/LN 2) to find the depth, then iterates from the most significant bit downward, adjusting S left or right depending on whether the new key T$ is greater or less than the existing entry at each midpoint. After locating the insertion point, the pointer array Y$ is expanded by splicing in two new bytes at position 2*S.
The use of LN to compute an integer logarithm base 2 is an idiom forced by the absence of a native integer-log function.
Telescope (Delete and Compact) Routine (Lines 3130–3390)
Deleting a record is a two-phase operation. First, the data bytes of the target record are physically overwritten by shifting all subsequent bytes in B$ down using a chunked copy loop with chunk size SHIFT=1000. Second, all 2-byte pointers in Y$ that pointed beyond the deleted record are decremented by C2 (the byte-length of the deleted record), and the index slot for the deleted entry is removed from Y$ by string slicing at line 3270.
The chunked copy (line 3210–3260) avoids the need to copy the entire tail of B$ in one operation, which could be slow or cause memory pressure. The final chunk is handled by adjusting SHIFT at line 3220.
Input Encoding Subroutine (Lines 2780–2800)
The GO SUB 2780 routine reads a raw input into Q$ and then prepends a length byte: Q$ = CHR$(LEN Q$ + 1) + Q$. This converts all user input into the same length-prefixed format used internally in B$, so the rest of the program can handle user input and stored data uniformly.
Lines 2785–2787 perform a two-pass check for trailing "#" followed by one or two decimal digits. If found, the digit string is converted via VAL to a CHR$ code, encoding a field type reference compactly as a single byte.
Search Modes
- Normal: Sequential scan comparing full fields via
FN A$(). - III (first-character): Scans forward checking only
B$(C+1)against the fifth character ofS$. - SSS (special/substring): For each record, scans all byte positions within the record for a substring match against
S$(5 TO ). - MMM (multiple): Accepts up to
SEARCHitems into arrayM$, then repeatedly calls the SSS search for each term, requiring all to match within the same record group.
Notable Techniques and Idioms
RANDOMIZEis used as a no-op separator afterIFto allow a second statement on the same line without usingANDor a colon — a common Spectrum BASIC idiom to chain conditions.- The sentinel record
CHR$ 2 + CHR$ 255at the start ofB$(line 1360) acts as an end-of-file marker; searches terminate when they encounter it. - The initial
Y$(line 1400) seeds two dummy pointer entries pointing to positions 0 and 3, bracketing the sentinel and providing stable boundary nodes for the binary search. - Record display (line 2860–2870) re-evaluates
FN A$()multiple times in a singlePRINTstatement to extract different substrings — each call re-reads throughC, which must not change during the line.
Bugs and Anomalies
- Line 1190 contains a typo:
"Press any ey to VERIFY"— the word “key” is missing the letter “k”. - Lines 2130–2160 appear unreachable in the change-entry subroutine, since all paths through lines 2050–2120 either loop back to line 2000 or return before reaching line 2130.
- The
FN A$()function is called multiple times in single expressions (e.g., lines 2860–2870), relying onCnot being a side-effectful call — this is safe here but fragile if the function definition were changed. - The multiple-search loop at lines 2313–2318 has a subtle flow dependency:
S1is set once before the innerFOR Kloop, but the SSS search modifiesS; if any term fails to match (C4=0), the routine returns immediately rather than advancing to the next record, which may cause the outer loop to stall. - Line 3505 uses
NEXT Iinside anIFbranch, which advances the outerFOR Iloop from within the innerFOR Jloop body — this is legal in Sinclair BASIC but unconventional and can skip theRETURNon line 3505, falling through correctly only because the loop exhaustion itself causes a return to the calling context.
Content
Source Code
100 REM "UNIFILEII"
1000 PAPER 7: CLS : BORDER 7: INK 6: PAPER 0: PRINT PAPER 2;" UNIFILE "
1010 PRINT '"FUNCTIONS AVAILABLE:"
1020 PRINT '" 1)SET UP NEW FILE"
1030 PRINT '" 2)ENTER INFORMATION"
1040 PRINT '" 3)SEARCH/DISPLAY/CHANGE"
1045 PRINT '" 4)TYPE NAMES"
1050 PRINT '" 5)STOP"
1060 PRINT ''"PLEASE ENTER WHICH YOU REQUIRE."
1070 INPUT Z$
1080 CLS
1090 IF Z$="1" THEN GO SUB 1210
1100 IF Z$="2" THEN GO SUB 1440
1110 IF Z$="3" THEN GO SUB 2180
1115 IF Z$="4" THEN GO SUB 3400
1120 IF Z$="5" THEN GO SUB 1150
1130 CLS
1140 GO TO 1000
1150 PRINT AT 10,5; INK 7; PAPER 2;"FILING SYSTEM CLOSED"
1160 BEEP 2,2
1180 INPUT "Have you input new information you wish to save? (Y/N)";Q$: IF Q$="N" THEN STOP
1190 SAVE "UNIFILE": PRINT '"Rewind tape, then Press any ey to VERIFY": PAUSE 0: VERIFY "UNIFILE": STOP
1200 REM ***********************
1210 REM ENTRY STRUCTURE
1220 REM ***********************
1230 DIM A$(50,10)
1350 DIM B$(28000)
1360 LET B$(1 TO 4)=CHR$ 2+CHR$ 0+CHR$ 2+CHR$ 255
1370 DEF FN A()=256*CODE Y$(2*S-1)+CODE Y$(2*S)
1380 DEF FN A$()=B$(C TO C+CODE B$(C)-1)
1390 LET P=5
1400 LET Y$=CHR$ 0+CHR$ 1+CHR$ 0+CHR$ 3
1410 LET N=2
1420 RETURN
1430 REM ***********************
1440 REM NORMAL INPUT
1450 REM ***********************
1460 LET R$=""
1470 PRINT PAPER 2;" ENTRIES "
1480 PRINT '"COMMANDS AVAILABLE:"
1490 PRINT '">ENTER ITEM SPECIFIED"''">""ZZZ"" TO QUIT"
1500 PRINT "********************************"
1510 PRINT "FILE SIZE:";P-1;"/";LEN B$
1540 GO SUB 2780
1550 IF Q$(LEN Q$-1)="#" THEN PRINT A$(CODE Q$(LEN Q$));":";
1580 PRINT Q$(2 TO LEN Q$-2*(Q$(LEN Q$-1)="#"))
1590 IF LEN Q$>=4 THEN RANDOMIZE : IF Q$(2 TO 4)="ZZZ" THEN RETURN
1600 LET R$=R$+Q$
1610 IF Q$(2)<>"*" THEN GO TO 1520
1620 CLS
1630 GO SUB 1660
1640 GO TO 1440
1650 REM ***********************
1660 REM PLACE DATA IN FILE
1670 REM ***********************
1680 IF P+LEN R$-1<LEN B$ THEN GO TO 1730
1690 PRINT AT 14,10;"FILE NOW FULL"
1700 PRINT ''" Press any key to continue"
1710 PAUSE 0
1720 RETURN
1730 LET POWER=INT (LN (N-1)/LN 2)
1740 LET S=2^POWER
1750 LET T$=R$(2 TO CODE R$(1))
1760 FOR K=POWER-1 TO 0 STEP -1
1770 LET C=FN A()
1780 LET U$=FN A$()(2 TO )
1790 LET S=S+(2^K)*(T$>U$)-(2^K)*(T$<U$)
1810 IF S>N-1 THEN LET S=N-1
1820 IF S<2 THEN LET S=2
1830 NEXT K
1840 LET C=FN A()
1850 LET U$=FN A$()(2 TO )
1860 IF T$<U$ THEN LET S=S-1
1870 LET B$(P TO P+LEN R$-1)=R$
1880 LET N=N+1
1890 LET Y$=Y$(1 TO 2*S)+CHR$ INT (P/256)+CHR$ (P-256*INT (P/256))+Y$(2*(S+1)-1 TO )
1900 LET P=P+LEN R$
1910 RETURN
1920 REM ***********************
1930 REM CHANGE ENTRY
1940 REM ***********************
1950 LET S=S-1
1960 LET C=FN A()
1970 LET R$=""
1980 PRINT "ENTRY ";S-1;":-"
2000 IF FN A$()(2)="*" THEN LET R$=R$+CHR$ 2+"*": GO SUB 3130: GO SUB 1660: RETURN
2010 PRINT FN A$()(2 TO LEN FN A$()-1);
2015 IF FN A$()(LEN FN A$()-1)="#" THEN PRINT CODE (FN A$()(LEN FN A$()))
2017 IF FN A$()(LEN FN A$()-1)<>"#" THEN PRINT FN A$()(LEN FN A$())
2020 PRINT AT 16,0; PAPER 2;" AMEND "
2030 PRINT "COMMANDS AVAILABLE:"
2040 PRINT ">""ENTER"" LEAVES ITEM UNCHANGED"'">""ZZZ"" DELETES WHOLE ENTRY"'">CHANGED ITEM"'">NEW ITEM ENDING WITH ""*"""
2050 GO SUB 2780
2060 IF LEN Q$=1 OR Q$(LEN Q$)="*" THEN LET R$=R$+FN A$()
2070 LET C=C+CODE B$(C)
2080 CLS
2090 IF LEN Q$=1 THEN GO TO 2000
2100 IF Q$(2 TO )="ZZZ" THEN GO SUB 3130: RETURN
2105 IF Q$(LEN Q$)="*" THEN LET Q$=Q$(2 TO LEN Q$-1): GO SUB 2785
2110 LET R$=R$+Q$
2120 GO TO 2000
2130 GO SUB 3130
2140 IF Q$(2 TO )="ZZZ" THEN RETURN
2150 GO SUB 1660
2160 RETURN
2170 REM ***********************
2180 REM SEARCH
2190 REM ***********************
2200 LET S=2
2210 PRINT PAPER 2;" SEARCH "
2220 PRINT ''"COMMANDS AVAILABLE:"
2230 PRINT ">INPUT ITEM FOR NORMAL SEARCH"'">PRECEDE WITH ""SSS"" FOR"'" SPECIAL SEARCH"'">PRECEDE WITH ""III"" TO SEARCH"'" FOR FIRST CHARACTER OF ENTRY"'">""ENTER"" FOR FIRST ITEM ON FILE"
2235 PRINT ">""MMM"" FOR MULTIPLE SEARCH"
2240 PRINT "********************************"
2250 PRINT ''"INPUT SEARCH ITEM:";
2260 GO SUB 2780
2270 PRINT Q$(2 TO )
2280 LET S$=Q$
2290 IF LEN S$=1 THEN GO TO 2510
2300 LET C=FN A()
2310 IF LEN S$<5 THEN GO TO 2430
2311 IF S$(2 TO 4)<>"MMM" THEN GO TO 2320
2312 PRINT "NUMBER OF SEARCH ITEMS? ";: INPUT SEARCH: PRINT SEARCH: DIM M$(SEARCH,10): FOR K=1 TO SEARCH: PRINT "SEARCH ITEM ";K;": ";: GO SUB 2780: PRINT Q$(2 TO ): LET M$(K)=Q$: NEXT K
2313 LET S1=S
2314 FOR K=1 TO SEARCH: LET S$="MMM"+M$(K,2 TO CODE M$(K,1)): GO SUB 2940: IF C4=0 THEN RETURN
2316 IF S=S1 THEN NEXT K: GO TO 2510
2318 GO TO 2313
2320 IF S$(2 TO 4)<>"III" THEN GO TO 2390
2330 FOR I=S TO N
2340 LET S=I
2350 LET C=FN A()
2360 IF B$(C+1)=S$(5) THEN GO TO 2510
2370 NEXT I
2380 RETURN
2390 IF S$(2 TO 4)<>"SSS" THEN GO TO 2430
2400 GO SUB 2920
2410 IF C4=1 THEN GO TO 2510
2420 RETURN
2430 IF FN A$()=S$ THEN GO TO 2510
2450 IF FN A$()=CHR$ 2+CHR$ 255 THEN RETURN
2460 LET C=C+CODE B$(C)
2470 IF B$(C-1)<>"*" THEN GO TO 2430
2480 LET S=S+1
2490 LET C=FN A()
2500 GO TO 2430
2510 LET C=FN A()
2520 LET C4=0
2530 IF FN A$()=CHR$ 2+CHR$ 255 THEN RETURN
2540 CLS
2550 PRINT "ENTRY ";S-1;":-"
2560 GO SUB 2850
2570 LET S=S+1
2580 PRINT AT 16,0; PAPER 2;" SEARCH "
2590 PRINT "COMMANDS AVAILABLE:"
2600 PRINT ">""ENTER"" TO DISPLAY NEXT ITEM"'">""ZZZ"" TO QUIT FUNCTION"'">""AAA"" TO AMEND"'">""CCC"" TO CONTINUE SEARCH"
2610 INPUT P$
2620 CLS
2625 IF LEN S$>=4 THEN RANDOMIZE : IF P$="CCC" AND S$(2 TO 4)="MMM" THEN GO TO 2313
2630 IF P$="CCC" THEN GO TO 2300
2640 IF P$="" THEN GO TO 2510
2650 IF P$<>"AAA" THEN GO TO 2710
2660 LET C=FN A()
2670 CLS
2680 GO SUB 1930
2710 IF P$="ZZZ" THEN RETURN
2720 IF P$="AAA" THEN RETURN
2730 CLS
2740 GO TO 2260
2750 REM ***********************
2760 REM FUNCTIONAL SUBROUTINES
2770 REM ***********************
2780 INPUT Q$
2785 IF LEN Q$>1 THEN RANDOMIZE : IF Q$(LEN Q$-1)="#" THEN LET Q$=Q$( TO LEN Q$-1)+CHR$ (VAL Q$(LEN Q$))
2787 IF LEN Q$>2 THEN RANDOMIZE : IF Q$(LEN Q$-2)="#" THEN LET Q$=Q$( TO LEN Q$-2)+CHR$ (VAL Q$(LEN Q$-1 TO ))
2790 LET Q$=CHR$ (LEN Q$+1)+Q$
2800 RETURN
2810 PRINT A$(I,2 TO CODE A$(I,1));":";
2820 RETURN
2850 IF FN A$()(2 TO )="*" THEN RETURN
2860 IF FN A$()(LEN FN A$()-1)="#" THEN PRINT A$(CODE FN A$()(LEN FN A$()));":";
2870 PRINT FN A$()(2 TO LEN FN A$()-2*(FN A$()(LEN FN A$()-1)="#"))
2880 LET C=C+CODE B$(C)
2890 GO TO 2850
2900 RETURN
2910 REM ***********************
2920 REM SPECIAL SEARCH
2930 REM ***********************
2940 LET C4=0
2950 FOR H=S TO N-1
2960 LET S=H
2970 LET C=FN A()
2980 LET C1=C
3000 LET C1=C1+CODE B$(C1)
3010 IF B$(C1-1)<>"*" THEN GO TO 3000
3020 FOR J=C+1 TO C1-LEN S$+5
3030 IF B$(J TO J+LEN S$-5)<>S$(5 TO ) THEN GO TO 3060
3040 LET C4=1
3050 RETURN
3060 NEXT J
3070 NEXT H
3080 LET C4=0
3090 RETURN
3100 REM ***********************
3110 REM TELESCOPE FILE
3120 REM ***********************
3130 LET C=FN A()
3140 LET SHIFT=1000
3150 LET C1=C
3160 LET C3=C
3180 LET C1=C1+CODE B$(C1)
3190 IF B$(C1-1)<>"*" THEN GO TO 3180
3200 LET C2=C1-C
3210 FOR I=C1 TO LEN B$-1 STEP SHIFT
3220 IF LEN B$-I+1<SHIFT THEN LET SHIFT=LEN B$-I+1
3230 LET S$=B$(I TO I+SHIFT-1)
3240 LET B$(C TO C+SHIFT-1)=S$
3250 LET C=C+SHIFT
3260 NEXT I
3270 LET Y$=Y$(1 TO 2*(S-1))+Y$(2*(S+1)-1 TO )
3280 FOR I=1 TO N-1
3290 LET S=I
3300 LET C=FN A()
3310 IF C<=C3 THEN GO TO 3350
3320 LET C=C-C2
3330 LET Y$(2*I-1)=CHR$ INT (C/256)
3340 LET Y$(2*I)=CHR$ (C-256*INT (C/256))
3350 NEXT I
3360 LET P=P-C2
3370 LET N=N-1
3380 RETURN
3400 REM ***********************
3410 REM ITEM TYPES
3420 REM ***********************
3430 FOR I=1 TO 50 STEP 10
3440 CLS : FOR J=I TO I+9
3450 PRINT J;")";A$(J)
3460 NEXT J
3470 PRINT '''"COMMANDS:"
3480 PRINT '">ZZZ=QUIT"
3490 PRINT ">III=ITEM"
3495 PRINT ">NNN=NEXT PAGE"
3500 INPUT Q$: IF Q$="ZZZ" THEN RETURN
3505 IF Q$="NNN" THEN CLS : NEXT I: RETURN
3510 IF Q$="III" THEN INPUT "NUMBER? ";TYPE: INPUT "TYPE NAME? ";Q$: LET A$(TYPE)=Q$: CLS : GO TO 3440
3520 GO TO 3500
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.
