--- title: "Unifile III" id: 53857 type: "computer_media" slug: "unifile" url: "http://localhost/computer_media/unifile/" markdown_url: "http://localhost/computer_media/unifile.md" published_at: "2024-05-13T19:02:05+00:00" modified_at: "2026-03-30T21:46:09+00:00" author: "David Anderson" featured_image: url: "http://localhost/wp-content/uploads/2019/02/20230809-041332.jpg" excerpt: "A surprisingly capable flat-file database packed into BASIC, using a 28,000-character buffer, binary search insertion, and live record compaction on delete." category: - name: "Archived Media" slug: "archived-media" taxonomy: "category" url: "http://localhost/category/archived-media/" post_tag: - name: "Downloadable" slug: "downloadable" taxonomy: "post_tag" url: "http://localhost/tag/downloadable/" - name: "TS 2068" slug: "ts2068" taxonomy: "post_tag" url: "http://localhost/tag/ts2068/" model: - name: "Timex/Sinclair 2068" slug: "ts-2068" taxonomy: "model" url: "http://localhost/model/ts-2068/" genre: - name: "Database" slug: "database" taxonomy: "genre" url: "http://localhost/type/database/" media_contents: - id: 51214 title: "CATS Library Tape 8" type: "computer_media" url: "http://localhost/computer_media/cats-library-tape-8/" - id: 55361 title: "SINCUS Exchange Tape 102 – Utilities & Business" type: "computer_media" url: "http://localhost/computer_media/sincus-exchange-tape-102-utilities-business/" media_type: "Program" mediadate: "198x" images: - url: "http://localhost/wp-content/uploads/2024/05/unifile.png" media_type_tags: "Database" --- # Unifile III 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 slot `S` into an absolute offset `C` into `B$`. - `DEF FN A$() = B$(C TO C+CODE B$(C)-1)` — returns the length-prefixed field string at position `C`, 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 of `S$`. - **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 `SEARCH` items into array `M$`, then repeatedly calls the SSS search for each term, requiring all to match within the same record group. ### Notable Techniques and Idioms - `RANDOMIZE` is used as a no-op separator after `IF` to allow a second statement on the same line without using `AND` or a colon — a common Spectrum BASIC idiom to chain conditions. - The sentinel record `CHR$ 2 + CHR$ 255` at the start of `B$` (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 single `PRINT` statement to extract different substrings — each call re-reads through `C`, 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 on `C` not 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: `S1` is set once before the inner `FOR K` loop, but the SSS search modifies `S`; 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 I` inside an `IF` branch, which advances the outer `FOR I` loop from within the inner `FOR J` loop body — this is legal in Sinclair BASIC but unconventional and can skip the `RETURN` on line 3505, falling through correctly only because the loop exhaustion itself causes a return to the calling context. ## 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$-1U$)-(2^K)*(T$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$"#" 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+1ZZZ=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 ```