--- title: "MULTIFILE+" id: 54576 type: "computer_media" slug: "multifile" url: "http://localhost/computer_media/multifile/" markdown_url: "http://localhost/computer_media/multifile.md" published_at: "2024-06-02T17:47:54+00:00" modified_at: "2026-03-30T21:44:13+00:00" author: "David Anderson" featured_image: url: "http://localhost/wp-content/uploads/2024/06/SCR-20240714-ehwh.png" excerpt: "A full-featured string database manager with Quicksort, multi-field search, record deletion, and tape save — all packed into a single BASIC program." category: - name: "Archived Media" slug: "archived-media" taxonomy: "category" url: "http://localhost/category/archived-media/" - name: "Database" slug: "database" taxonomy: "category" url: "http://localhost/category/software/database/" 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/" indiv: - name: "Algis Gedris" slug: "algis-gedris" taxonomy: "indiv" url: "http://localhost/indiv/algis-gedris/" 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: 55923 title: "Timex Sinclair Public Domain Library Tape 2003" type: "computer_media" url: "http://localhost/computer_media/timex-sinclair-public-domain-library-tape-2003/" media_type: "Program" programmers: - name: "Algis Gedris" slug: "algis-gedris" taxonomy: "indiv" url: "http://localhost/indiv/algis-gedris/" mediadate: "198x" images: - url: "http://localhost/wp-content/uploads/2024/06/SCR-20240714-ehwh.png" related_content: - id: 13385 title: "Multifile" type: "product" url: "http://localhost/product/multifile/" media_type_tags: "Database" --- # MULTIFILE+ MULTIFILE+ is a multi-field string database manager that stores records in a three-dimensional string array, supporting initialization, data entry, browsing, searching, editing, deletion, sorting, and tape save/verify operations. Records are organized as variable-length fields packed into a single 3D array `a$(1,x,fieldstart TO fieldend)`, with cumulative field offsets tracked in the numeric array `n()`. The program implements an in-place Quicksort (lines 1880–2130) using explicit stack arrays `l()` and `h()` to avoid recursion, which BASIC does not support. A decorated menu screen is built using block graphic strings and INVERSE attributes, and an optional printer channel (channel 2) can be toggled on from the menu for printed output. The program saves itself back to tape with `SAVE y$ LINE 1180` so the title and current data are preserved together. Based on the original [MULTIFILE](http://localhost/product/multifile/) written for the ZX81 and marketed by [BUG-BYTE](http://localhost/company/bug-byte-software/). Modified to handle strings and has been enhanced to provide additional routines. *** ## Program Analysis ### Program Structure The program is divided into functional regions separated by `GO TO` jumps from a central menu dispatcher at lines 1180–1410. Line 110 immediately jumps to 1180, which renders the initialization code at line 120 unreachable on first load — it is only entered via menu option 1. The overall flow is: 1. **Lines 1–2:** Credit REMs. 2. **Line 110:** Bootstrap jump to menu. 3. **Lines 120–400:** Initialization — prompts for field headings, dimensions arrays, computes capacity. 4. **Lines 410–630:** Record creation (data entry loop). 5. **Lines 640–860:** Step/browse records, optional printer output. 6. **Lines 870–1110:** Search — selects a heading, then scans all records for a matching field value. 7. **Lines 1120–1170:** Change (modify) a record. 8. **Lines 1180–1410:** Main menu display and dispatcher. 9. **Lines 1420–1560:** Date entry and tape save/verify. 10. **Lines 1570–1580:** Full reset and re-save of bare program. 11. **Lines 1600–1810:** Record deletion with compaction. 12. **Lines 1820–1870:** Sort setup — field selection and parameter computation. 13. **Lines 1880–2130:** Quicksort implementation. 14. **Lines 2140, 2150, 9998:** Diagnostic print loop, attribution REM, and final `SAVE`. ### Data Storage Model All record data is stored in the three-dimensional string array `a$(1, recordIndex, characterPosition)`. The first dimension is fixed at 1, making `a$` effectively a 2D structure: rows are records and columns are concatenated field characters. Field boundaries are tracked cumulatively in `n(z)`: after initialization, `n(z)` holds the last character position of field `z`, so field `z`‘s data occupies `a$(1, x, n(z-1)+1 TO n(z))`. This packing avoids a separate array per field at the cost of fixed-width field allocation. ### Capacity Calculation Line 300 computes the maximum number of records as `x = INT(FREE - 1000) / tot`, where `tot` is the total characters per record and `FREE` is the available memory keyword. The 1000-byte safety margin is intended to leave headroom for program variables. The result is stored in both `x` (later used as a loop counter) and `w` (the permanent capacity ceiling checked throughout the program). ### Quicksort Implementation The sort routine (lines 1880–2130) is a non-recursive Quicksort using explicit stack arrays `l(20)` and `h(20)` for partition boundaries, since BASIC has no call stack for recursion. The stack pointer is `ii`, initialized to 2 at line 1910 (with `l(1)=1` and `h(1)=n` as the first partition). The pivot is chosen as the last element of each partition (`il=j=h(ii)`). Swaps are performed via the temporary string `k$`, operating on entire record rows `a$(1,i)` and `a$(1,j)`. Comparisons use the field slice `a$(1,i,a TO b)` where `a` and `b` are set by lines 1840–1850 depending on which field was selected for sorting. ### Menu Screen Construction The menu (lines 1220–1290) is drawn using pre-built block-graphic border strings `x$`, `v$`, and `w$` defined at lines 150–170. The title `y$` is displayed with `INVERSE 1`. Status fields show record count `n`/`w`, sort state `p$`, date `d$`, and printer state with conditional `PAPER` coloring (`PAPER 2` for ON, `PAPER 4` for OFF). The right and left borders are drawn by a `FOR` loop at line 1270 printing block-graphic characters at columns 0 and 31. ### Printer Channel Handling Menu option 9 sets `chan=1` and returns to the menu display. When `chan` is set, browse (line 670) and search (line 940) open channel 2 to `"p"` (the printer) before outputting records. Line 750 closes the channel after each record display. This allows selective printer output without restructuring the display logic. ### Tape Save Strategy The program saves itself back to tape at line 1550 with `SAVE y$ LINE 1180`, where `y$` is the user-supplied title (up to 10 characters). Because the array `a$` containing the database records is part of the program’s variable area, this single `SAVE` preserves both the program code and all entered data. Line 1570 performs a full reset (`CLEAR`) and re-saves the bare program shell for distribution or reuse. ### Title Storage via POKE Lines 350–370 store the title string `y$` into memory starting at address 26715 using `POKE 26715+z-1, CODE y$(z)`. This address falls within the program area and is used to persist the title across saves — the value is read back by the `SAVE y$` statement because `y$` is updated from those POKEd bytes by line 1580 on restore. `POKE 23658,8` at line 360 enables caps lock for the title input. ### Record Deletion and Compaction Delete (lines 1600–1810) shifts all records above the deleted index down by one position using `LET a$(1,i)=a$(1,i+1)` in a `FOR` loop (line 1740), then decrements `n`. If the deleted record is the last one, it simply clears that row (line 1730). The `mo` flag (modify mode) is set to 1 before calling the display subroutine at line 1660 so the record’s contents are shown before confirmation. ### Notable Techniques and Idioms - `PAUSE 0` followed by an `INKEY$` poll loop is used as an efficient keypress wait at lines 590–630, 1300–1410, 1680–1720, 1770–1810. - Conditional string concatenation with `AND` is used in the menu (e.g., `"ON " AND chan=1`) to display status text conditionally without `IF` statements. - The `mo` (modify mode) flag is shared between the change and delete routines to reuse the data-entry loop at line 440 and the display subroutine at line 680. - `VAL "number"` is not present; all `GO TO` targets are literal line numbers. - Line 1820 uses a conditional string expression in the `INPUT` prompt: `"not " AND p$<>"unsorted"` appends “not” only when the data has already been sorted. ### Bugs and Anomalies - Line 1910 initializes `ii=2` but the Quicksort loop condition at line 1920 checks `ii<=1` to return — since `ii` starts at 2, the first iteration always proceeds, which is the intended behavior. However, the stack is never explicitly cleared between sorts, so stale values in `l()` and `h()` from a previous sort could cause incorrect behavior on a second sort of a smaller dataset. In practice this is mitigated because `l(1)` and `h(1)` are always reset at lines 1890–1900. - Line 620 checks `INKEY$<>"y"` after line 610 has already matched `"y"`; since INKEY$ is sampled again, there is a race: if the key is released between lines 610 and 620, line 620’s condition is true and control falls through to 630, which re-polls. This is a common INKEY$ debounce pitfall. - Line 1410 is a dead fallthrough: after the cascading `IF INKEY$=...` checks at 1320–1400 cover all digits 1–9, line 1410 `GO TO 1320` is unreachable in normal operation. - The `CLOSE #2` at line 750 is called unconditionally even when `chan=0` and no channel was opened; closing an unopened channel is harmless on this platform but is technically unnecessary. ## Source Code ``` 1 REM THIS PROGRAM WAS DONATED BY TORONTO TIMEX SINCLAIR USER'S GROUP 2 REM MODIFIED AND COPIED BY ALGIS E. GEDRIS DECEMBER 20, 1986 100 REM MULTIFILE+ 110 GO TO 1180 120 LET free=~-1000: LET save=0: CLS 130 INPUT "How many string headings ?"'s 140 LET t=0: LET g=1180 150 LET x$="\:'\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\''\':" 160 LET v$="\:.\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\..\.:" 170 LET w$="\: \ :" 180 IF save THEN RETURN 190 IF s<1 THEN GO TO 220 200 DIM n$(s,12) 210 DIM n(s) 220 DIM l(20): DIM h(20) 230 CLS 240 FOR z=1 TO s 250 INPUT ("Name of string heading ";z)' LINE n$(z) 260 INPUT "Maximum number of characters ?"'n(z) 270 CLS 280 LET t=t+n(z): LET n(z)=t 290 NEXT z 300 LET tot=t: LET x=INT (free/tot): LET w=x 310 PRINT #1;"There is room for ";x'"records." 320 PAUSE 120 330 IF s>0 THEN DIM a$(1,x,t) 340 REM titling 350 LET y$=" ": FOR z=1 TO LEN y$: POKE 26715+z-1,CODE y$(z): NEXT z 360 POKE 23658,8: INPUT "Title ? (Max: 10 characters)"' LINE y$: POKE 23658,0 370 FOR z=1 TO LEN y$: POKE 26715+z-1,CODE y$(z): NEXT z 380 IF save THEN RETURN 390 LET d$="": LET n=0 400 GO TO 1180 410 CLS 420 LET n=n+1: LET x=n 430 IF n>w THEN GO TO 1520 440 IF mo THEN PRINT ;"Enter new data for each heading or press ENTER to leave as is. " 450 FOR z=1 TO s 460 PRINT INVERSE 1;n$(z);"? " 470 IF mo AND z=1 THEN PRINT a$(z,x, TO n(1)) 480 IF mo AND z>1 THEN PRINT a$(1,x,n(z-1)+1 TO n(z)) 490 INPUT LINE z$ 500 IF z$="" THEN GO TO 550 510 IF LEN z$>n(z) THEN LET z$=z$( TO n(z)) 520 IF z=1 THEN LET a$(z,x, TO n(1))=z$ 530 IF z>1 THEN LET a$(1,x,n(z-1)+1 TO n(z))=z$ 540 PRINT PAPER 2;"Changed to " AND mo;z$ 550 NEXT z 560 PRINT #0;AT 0,0;"Record # ";x 570 IF mo THEN PAUSE 120: RETURN 580 PRINT #0;AT 1,0; FLASH 1;" Another record ? Y/N" 590 PAUSE 0 600 IF INKEY$="" THEN GO TO 600 610 IF INKEY$="y" THEN GO TO 410 620 IF INKEY$<>"y" THEN GO TO 1180 630 GO TO 590 640 CLS 650 INPUT "Starting from what record # ?"'x 660 CLS 670 IF chan THEN OPEN #2,"p" 680 FOR z=1 TO s 690 IF x=n+1 THEN CLS : PRINT AT 10,10;"Nothing found": PAUSE 120: GO TO 1180 700 PRINT INVERSE 1;n$(z) 710 IF z=1 THEN PRINT a$(1,x, TO n(1)) 720 IF z>1 THEN PRINT a$(1,x,n(z-1)+1 TO n(z)) 730 NEXT z 740 PRINT #0;AT 0,0;"Record # ";x 750 CLOSE #2 760 IF mo THEN RETURN 770 PRINT #1;AT 1,0;"1=Continue; 2=Menu" 780 LET u$=INKEY$ 790 IF u$="2" THEN GO TO 1180 800 IF u$="1" AND j=1 THEN GO TO 860 810 IF u$="1" THEN GO TO 830 820 GO TO 780 830 LET x=x+1 840 IF x>w THEN GO TO 1180 850 GO TO 660 860 RETURN 870 CLS 880 LET j=1 890 LET h$="Enter heading." 900 LET e$="Enter search item." 910 PRINT "Headings:"'': FOR z=1 TO s: PRINT TAB 3;n$(z): PRINT : NEXT z 920 INPUT (h$)' LINE z$ 930 IF z$="" THEN GO TO 1180 940 IF chan THEN OPEN #2,"p" 950 LET y=1 960 IF z$=n$(y, TO LEN z$) THEN GO TO 1030 970 IF y=s THEN GO TO 1000 980 LET y=y+1 990 GO TO 960 1000 PRINT #0;AT 0,5; FLASH 1;i$ 1010 PAUSE 120 1020 GO TO 920 1030 INPUT (e$)' LINE g$ 1040 LET g1=LEN g$ 1050 IF y=1 THEN GO TO 1460 1060 LET x=1 1070 IF g$=a$(1,x,n(y-1)+1 TO n(y-1)+g1) THEN GO SUB 660 1080 IF x=n THEN CLS : PRINT AT 10,10;"Nothing found": PAUSE 120: GO TO 1180 1090 IF x=w THEN GO TO 1180 1100 LET x=x+1 1110 GO TO 1070 1120 CLS : REM change 1130 INPUT "Enter record # to be changed"'x 1140 LET mo=1 1150 GO SUB 440 1160 PRINT #0;AT 0,0; FLASH 1;" Changes complete " 1170 PAUSE 120 1180 LET chan=0: CLOSE #2: BORDER 0: PAPER 0: INK 9: CLS : LET g=1180 1190 LET i$=" INVALID HEADING ! " 1200 LET mo=0 1210 POKE 23658,0 1220 PRINT AT 1,12; INVERSE 1;y$ 1230 PRINT 'x$;w$;AT 4,13;" MENU "'w$;w$;AT 6,1;"Press...";AT 6,18; INVERSE 1;"Status";TAB 31 1240 PRINT AT 8,0;" 1>> Initialize"'AT 9,0;" 2>> Create";AT 9,18; INVERSE 1;n;"/";w;TAB 31; INVERSE 0;AT 10,0;" 3>> Change";AT 11,0;" 4>> Search" 1250 PRINT AT 12,0;" 5>> Step";AT 13,0;" 6>> Sort";AT 13,18; INVERSE 1;p$;TAB 31; INVERSE 0;AT 14,0;" 7>> Delete";AT 15,0;" 8>> Date/Save";AT 15,18; INVERSE 1;d$;TAB 31 1260 PRINT " 9>> Print-out";AT 16,18; INVERSE 0; PAPER 2;"ON " AND chan=1; PAPER 4;"OFF" AND NOT chan 1270 FOR i=7 TO 17: PRINT AT i,0;"\: ";AT i,31;"\ :": NEXT i: PRINT v$ 1280 PRINT 1290 LET j=0 1300 PAUSE 0 1310 IF INKEY$<"1" OR INKEY$>"9" THEN GO TO 1300 1320 IF INKEY$="1" THEN GO TO 120 1330 IF INKEY$="9" THEN LET chan=1: GO TO 1220 1340 IF INKEY$="2" THEN GO TO 410 1350 IF INKEY$="5" THEN GO TO 640 1360 IF INKEY$="7" THEN GO TO 1600 1370 IF INKEY$="4" THEN GO TO 870 1380 IF INKEY$="6" THEN GO TO 1820 1390 IF INKEY$="8" THEN GO TO 1420 1400 IF INKEY$="3" THEN GO TO 1120 1410 GO TO 1320 1420 CLS 1430 DIM d$(6) 1440 INPUT "Enter date (YYMMDD)"; LINE d$ 1450 GO TO 1550 1460 LET x=1 1470 IF g$=a$(1,x, TO g1) THEN GO SUB 660 1480 IF x=n THEN CLS : PRINT AT 10,10;"Nothing found": PAUSE 120: GO TO 1180 1490 IF x=w THEN GO TO 1180 1500 LET x=x+1 1510 GO TO 1470 1520 CLS 1530 PRINT AT 10,10;"No room left !" 1540 PAUSE 120 1550 SAVE y$ LINE 1180: PRINT FLASH 1;"rewind for verify": VERIFY "" 1560 GO TO 1180 1570 CLEAR : LET p$="unsorted": LET save=1: GO SUB 150: GO SUB 350: LET g=1180: LET w=0: LET n=0: LET d$="": SAVE y$ LINE 1180: VERIFY "": LIST 1580 FOR z=1 TO LEN y$: POKE 26715+z-1,CODE y$(z): NEXT z: RETURN 1590 REM delete 1600 CLS 1610 LET mo=1 1620 INPUT "Record # ? (0=menu)"'x 1630 IF x=0 THEN GO TO 1180 1640 IF x>n THEN PRINT #0;" Invalid input ": PAUSE 60: GO TO 1620 1650 PRINT "Record # ";x;" is:"'' 1660 GO SUB 680 1670 PRINT #0;AT 0,0;"1=delete;2=menu" 1680 PAUSE 0 1690 IF INKEY$="" THEN GO TO 1690 1700 IF INKEY$<>"1" THEN GO TO 1180 1710 IF INKEY$="1" THEN PRINT #0;AT 0,0;"Deleting: Stand by....": GO TO 1730 1720 GO TO 1680 1730 IF x>n-1 THEN LET a$(1,x)="": GO TO 1745 1740 FOR i=x TO n-1: LET a$(1,i)=a$(1,i+1): NEXT i 1745 LET n=n-1 1750 PAUSE 120 1760 CLS : PRINT #1;"Record # ";x;" has been deleted": PRINT #0;"1=more deletions;2=menu" 1770 PAUSE 0 1780 IF INKEY$="" THEN GO TO 1780 1790 IF INKEY$<>"1" THEN GO TO 1180 1800 IF INKEY$="1" THEN GO TO 1600 1810 GO TO 1770 1820 CLS : FOR i=1 TO s: PRINT i,n$(i): NEXT i: INPUT ("choose sort type"'"not " AND p$<>"unsorted";p$ AND p$<>"unsorted")'st 1830 IF st>s THEN CLS : PRINT "invalid input": PAUSE 60: CLS : GO TO 1820 1840 IF st=1 THEN LET a=1: LET b=n(st) 1850 IF st>1 THEN LET a=1+n(st-1): LET b=n(st) 1860 CLS : PRINT FLASH 1;"Sorting..."'"by ";n$(st): LET p$=n$(st): GO SUB 1880 1870 GO TO 1180 1880 REM quicksort 1890 LET l(1)=1 1900 LET h(1)=n 1910 LET ii=2 1920 IF ii<=1 THEN RETURN 1930 IF l(ii)>=h(ii) THEN LET ii=ii-1 1940 IF l(ii)>=h(ii) THEN GO TO 1920 1950 LET i=l(ii)-1 1960 LET j=h(ii) 1970 LET il=j 1980 IF i>=j THEN GO TO 2070 1990 LET i=i+1 2000 IF a$(1,i,a TO b)1 THEN IF a$(1,j,a TO b)>a$(1,il,a TO b) THEN GO TO 2010 2030 IF i