home *** CD-ROM | disk | FTP | other *** search
/ Programming Tool Box / SIMS_2.iso / bp_6_93 / bonus / winer / filesort.bas < prev    next >
BASIC Source File  |  1992-05-12  |  12KB  |  366 lines

  1. '********** FILESORT.BAS - indexed multi-key sort for random access files
  2.  
  3. 'Copyright (c) 1992 Ethan Winer
  4.  
  5. DEFINT A-Z
  6.  
  7. DECLARE FUNCTION Compare3% (BYVAL Seg1, BYVAL Adr1, BYVAL Seg2, BYVAL Adr2, NumBytes)
  8. DECLARE FUNCTION Exist% (FileSpec$)
  9. DECLARE SUB DOSInt (Registers AS ANY)
  10. DECLARE SUB FileSort (FileName$, NDXName$, RecLength, Offset, KeySize)
  11. DECLARE SUB LoadFile (FileNum, Segment, Address, Bytes&)
  12. DECLARE SUB SaveFile (FileNum, Segment, Address, Bytes&)
  13. DECLARE SUB SwapMem (BYVAL Seg1, BYVAL Adr1, BYVAL Seg2, BYVAL Adr2, BYVAL Length)
  14. DECLARE SUB TypeISort (Segment, Address, ElSize, Offset, KeySize, NumEls, Index())
  15.  
  16. RANDOMIZE TIMER                 'create new data each run
  17. DEF FnRand% = INT(RND * 10 + 1) 'returns RND from 1 to 10
  18.  
  19. TYPE RegType                    'used by DOSInt
  20.   AX AS INTEGER
  21.   BX AS INTEGER
  22.   CX AS INTEGER
  23.   DX AS INTEGER
  24.   BP AS INTEGER
  25.   SI AS INTEGER
  26.   DI AS INTEGER
  27.   FL AS INTEGER
  28.   DS AS INTEGER
  29.   ES AS INTEGER
  30. END TYPE
  31.  
  32. DIM SHARED Registers AS RegType 'share among all subs
  33. REDIM LastNames$(1 TO 10)       'we'll select names at
  34. REDIM FirstNames$(1 TO 10)      '  random from these
  35.  
  36. NumRecords = 2988           'how many test records to use
  37. FileName$ = "TEST.DAT"      'really original, eh?
  38. NDXName$ = "TEST.NDX"       'this is the index file name
  39.  
  40. TYPE RecType
  41.   LastName  AS STRING * 11
  42.   FirstName AS STRING * 10
  43.   Dollars   AS STRING * 6
  44.   Cents     AS STRING * 2
  45.   AnyNumber AS LONG         'just to show that only key
  46.   OtherNum  AS LONG         '  information must be ASCII
  47. END TYPE
  48.  
  49. FOR X = 1 TO 10             'read the possible last names
  50.   READ LastNames$(X)
  51. NEXT
  52.  
  53. FOR X = 1 TO 10             'and the possible first names
  54.   READ FirstNames$(X)
  55. NEXT
  56.  
  57. DIM RecordVar AS RecType    'to create the sample file
  58. RecLength = LEN(RecordVar)  'the length of each record
  59. CLS
  60. PRINT "Creating a test file..."
  61.  
  62. IF Exist%(FileName$) THEN   'if there's an existing file
  63.   KILL FileName$            'kill the old data from prior
  64. END IF                      '  runs to start fresh
  65.  
  66. IF Exist%(NDXName$) THEN    'same for any old index file
  67.   KILL NDXName$
  68. END IF
  69.  
  70.  
  71. '---- Create some test data and write it to the file
  72. OPEN FileName$ FOR RANDOM AS #1 LEN = RecLength
  73.   FOR X = 1 TO NumRecords
  74.     RecordVar.LastName = LastNames$(FnRand%)
  75.     RecordVar.FirstName = FirstNames$(FnRand%)
  76.     Amount$ = STR$(RND * 10000)
  77.     Dot = INSTR(Amount$, ".")
  78.     IF Dot THEN
  79.       RSET RecordVar.Dollars = LEFT$(Amount$, Dot - 1)
  80.       RecordVar.Cents = LEFT$(MID$(Amount$, Dot + 1) + "00", 2)
  81.     ELSE
  82.       RSET RecordVar.Dollars = Amount$
  83.       RecordVar.Cents = "00"
  84.     END IF
  85.     RecordVar.AnyNumber = X
  86.     PUT #1, , RecordVar
  87.   NEXT
  88. CLOSE
  89.  
  90. '----- Created a sorted index based on the main data file
  91. Offset = 1                  'start sorting with LastName
  92. KeySize = 29                'sort based on first 4 fields
  93. PRINT "Sorting..."
  94. CALL FileSort(FileName$, NDXName$, RecLength, Offset, KeySize)
  95.  
  96.  
  97. '----- Display the results
  98. CLS
  99. VIEW PRINT 1 TO 24
  100. LOCATE 25, 1
  101. COLOR 15
  102. PRINT "Press any key to pause/resume";
  103. COLOR 7
  104. LOCATE 1, 1
  105.  
  106. OPEN FileName$ FOR RANDOM AS #1 LEN = RecLength
  107. OPEN NDXName$ FOR BINARY AS #2
  108.   FOR X = 1 TO NumRecords
  109.     GET #2, , ThisRecord        'get next record number
  110.     GET #1, ThisRecord, RecordVar   'then the actual data
  111.  
  112.     PRINT RecordVar.LastName;       'print each field
  113.     PRINT RecordVar.FirstName;
  114.     PRINT RecordVar.Dollars; ".";
  115.     PRINT RecordVar.Cents
  116.  
  117.     IF LEN(INKEY$) THEN             'pause on a keypress
  118.       WHILE LEN(INKEY$) = 0: WEND
  119.     END IF
  120.   NEXT
  121. CLOSE
  122.  
  123. VIEW PRINT 1 TO 24                  'restore the screen
  124. END
  125.  
  126. DATA Smith, Cramer, Malin, Munro, Passarelli
  127. DATA Bly, Osborn, Pagliaro, Garcia, Winer
  128.  
  129. DATA John, Phil, Paul, Anne, Jacki
  130. DATA Patricia, Ethan, Donald, Tami, Elli
  131.  
  132. FUNCTION Exist% (Spec$) STATIC  'reports if a file exists
  133.  
  134. DIM DTA AS STRING * 44          'the work area for DOS
  135. DIM LocalSpec AS STRING * 60    'guarantee the spec is in
  136. LocalSpec$ = Spec$ + CHR$(0)    '  DGROUP for BASIC PDS
  137.  
  138. Exist% = -1                     'assume true for now
  139.  
  140. Registers.AX = &H1A00           'assign DTA service
  141. Registers.DX = VARPTR(DTA)      'show DOS where to place it
  142. Registers.DS = VARSEG(DTA)
  143. CALL DOSInt(Registers)
  144.  
  145. Registers.AX = &H4E00           'find first matching file
  146. Registers.CX = 39               'any file attribute okay
  147. Registers.DX = VARPTR(LocalSpec)
  148. Registers.DS = VARSEG(LocalSpec)
  149. CALL DOSInt(Registers)          'see if there's a match
  150.  
  151. IF Registers.FL AND 1 THEN      'if the Carry flag is set
  152.   Exist% = 0                    '  there were no matches
  153. END IF
  154.  
  155. END FUNCTION
  156.  
  157. SUB FileSort (FileName$, NDXName$, RecLength, Displace, KeySize) STATIC
  158.  
  159. CONST BufSize% = 32767  'holds the data being sorted
  160. Offset = Displace - 1   'make zero-based for speed later
  161.  
  162. '----- Open the main data file
  163. FileNum = FREEFILE
  164. OPEN FileName$ FOR BINARY AS #FileNum
  165.  
  166. '----- Calculate the important values we'll need
  167. NumRecords = LOF(FileNum) \ RecLength
  168. RecsPerPass = BufSize% \ RecLength
  169. IF RecsPerPass > NumRecords THEN RecsPerPass = NumRecords
  170.  
  171. NumPasses = (NumRecords \ RecsPerPass) - ((NumRecords MOD RecsPerPass) <> 0)
  172. IF NumPasses = 1 THEN
  173.   RecsLastPass = RecsPerPass
  174. ELSE
  175.   RecsLastPass = NumRecords MOD RecsPerPass
  176. END IF
  177.  
  178. '----- Create the buffer and index sorting arrays
  179. REDIM Buffer(1 TO 1) AS STRING * BufSize
  180. REDIM Index(1 TO RecsPerPass)
  181. IndexAdjust = 1
  182.  
  183.  
  184. '----- Process all of the records in manageable groups
  185. FOR X = 1 TO NumPasses
  186.  
  187.   IF X < NumPasses THEN         'if not the last pass
  188.     RecsThisPass = RecsPerPass  'do the full complement
  189.   ELSE                          'the last pass may have
  190.     RecsThisPass = RecsLastPass '  fewer records to do
  191.   END IF
  192.  
  193.   FOR Y = 1 TO RecsThisPass     'initialize the index
  194.     Index(Y) = Y - 1            'starting with value of 0
  195.   NEXT
  196.  
  197.   '----- Load a portion of the main data file
  198.   Segment = VARSEG(Buffer(1))   'show where the buffer is
  199.   CALL LoadFile(FileNum, Segment, Zero, RecsThisPass * CLNG(RecLength))
  200.   CALL TypeISort(Segment, Zero, RecLength, Displace, KeySize, RecsThisPass, Index())
  201.  
  202.   '----- Adjust the zero-based index to record numbers
  203.   FOR Y = 1 TO RecsThisPass
  204.     Index(Y) = Index(Y) + IndexAdjust
  205.   NEXT
  206.  
  207.   '----- Save the index file for this pass
  208.   TempNum = FREEFILE
  209.   OPEN "$$PASS." + LTRIM$(STR$(X)) FOR OUTPUT AS #TempNum
  210.   CALL SaveFile(TempNum, VARSEG(Index(1)), Zero, RecsThisPass * 2&)
  211.   CLOSE #TempNum
  212.  
  213.   '----- The next group of record numbers start this much higher
  214.   IndexAdjust = IndexAdjust + RecsThisPass
  215.  
  216. NEXT
  217.  
  218. ERASE Buffer, Index             'free up the memory
  219.  
  220.  
  221. '----- Do a final merge pass if necessary
  222. IF NumPasses > 1 THEN
  223.  
  224.   NDXNumber = FREEFILE
  225.   OPEN NDXName$ FOR BINARY AS #NDXNumber
  226.   REDIM FileNums(NumPasses)             'this holds the file numbers
  227.   REDIM RecordNums(NumPasses)           'and this holds the record numbers
  228.  
  229.   REDIM MainRec$(1 TO NumPasses)        'this holds the main record data
  230.   REDIM Remaining(1 TO NumPasses)       'this tracks reading the index files
  231.  
  232.   '----- Open the files and seed the first round of data
  233.   FOR X = 1 TO NumPasses
  234.     FileNums(X) = FREEFILE
  235.     OPEN "$$PASS." + LTRIM$(STR$(X)) FOR BINARY AS #FileNums(X)
  236.     Remaining(X) = LOF(FileNums(X))     'this is what remains
  237.     MainRec$(X) = SPACE$(RecLength)     'load the main data records here
  238.  
  239.     GET #FileNums(X), , RecordNums(X)   'get the next record number
  240.     RecOffset& = (RecordNums(X) - 1) * CLNG(RecLength) + 1
  241.     GET #FileNum, RecOffset&, MainRec$(X) 'and then get the actual data
  242.   NEXT
  243.  
  244.   FOR X = 1 TO NumRecords
  245.  
  246.     Lowest = 1                  'assume this is the "lowest" data in the group
  247.     WHILE Remaining(Lowest) = 0 'Lowest can't refer to a dead index
  248.       Lowest = Lowest + 1       'so seek to the next higher active index
  249.     WEND
  250.  
  251.     FOR Y = 2 TO NumPasses      'now seek out the truly lowest element
  252.       IF Remaining(Y) THEN      'consider only active indexes
  253.         IF Compare3%(SSEG(MainRec$(Y)), SADD(MainRec$(Y)) + Offset, SSEG(MainRec$(Lowest)), SADD(MainRec$(Lowest)) + Offset, KeySize) = -1 THEN
  254.           Lowest = Y ' ^ ------ use VARSEG here with QuickBASIC ------^
  255.         END IF
  256.       END IF
  257.     NEXT
  258.  
  259.     PUT #NDXNumber, , RecordNums(Lowest)        'write to master index file
  260.      
  261.     Remaining(Lowest) = Remaining(Lowest) - 2   'this much less remains
  262.     IF Remaining(Lowest) THEN                       'if index is still active
  263.       GET #FileNums(Lowest), , RecordNums(Lowest)   'get the record number
  264.       RecOffset& = (RecordNums(Lowest) - 1) * CLNG(RecLength) + 1
  265.       GET #FileNum, RecOffset&, MainRec$(Lowest)    'then get the actual data
  266.     END IF
  267.  
  268.   NEXT
  269.  
  270. ELSE
  271.   '----- Only one pass was needed so simply rename the index file
  272.   NAME "$$PASS.1" AS NDXName$
  273. END IF
  274.  
  275. CLOSE                                   'close all open files
  276.  
  277. IF Exist%("$$PASS.*") THEN              'ensure there's something to kill
  278.   KILL "$$PASS.*"                       'kill the work files
  279. END IF
  280.  
  281. ERASE FileNums, RecordNums              'erase the work arrays
  282. ERASE MainRec$, Remaining
  283.  
  284. END SUB
  285.  
  286. SUB LoadFile (FileNum, Segment, Address, Bytes&) STATIC
  287.  
  288.   IF Bytes& > 32767 THEN Bytes& = Bytes& - 65536
  289.  
  290.   Registers.AX = &H3F00         'read from file service
  291.   Registers.BX = FILEATTR(FileNum, 2) 'get the DOS handle
  292.   Registers.CX = Bytes&         'how many bytes to load
  293.   Registers.DX = Address        'and at what address
  294.   Registers.DS = Segment        'and at what segment
  295.  
  296.   CALL DOSInt(Registers)
  297.  
  298. END SUB
  299.  
  300. SUB SaveFile (FileNum, Segment, Address, Bytes&) STATIC
  301.  
  302.   IF Bytes& > 32767 THEN Bytes& = Bytes& - 65536
  303.  
  304.   Registers.AX = &H4000         'write to file service
  305.   Registers.BX = FILEATTR(FileNum, 2) 'get the DOS handle
  306.   Registers.CX = Bytes&         'how many bytes to load
  307.   Registers.DX = Address        'and at what address
  308.   Registers.DS = Segment        'and at what segment
  309.  
  310.   CALL DOSInt(Registers)
  311.  
  312. END SUB
  313.  
  314. SUB TypeISort (Segment, Address, ElSize, Displace, KeySize, NumEls, Index()) STATIC
  315.  
  316. REDIM QStack(NumEls \ 5 + 10)   'create a stack
  317.  
  318. First = 1                       'initialize working variables
  319. Last = NumEls
  320. Offset = Displace - 1           'make zero-based now for speed later
  321.  
  322. DO
  323.   DO
  324.     Temp = (Last + First) \ 2   'seek midpoint
  325.     I = First
  326.     J = Last
  327.  
  328.     DO          'change -1 to 1 and 1 to -1 below to sort descending
  329.       WHILE Compare3%(Segment, Address + Offset + (Index(I) * ElSize), Segment, Address + Offset + (Index(Temp) * ElSize), KeySize) = -1
  330.         I = I + 1
  331.       WEND
  332.       WHILE Compare3%(Segment, Address + Offset + (Index(J) * ElSize), Segment, Address + Offset + (Index(Temp) * ElSize), KeySize) = 1
  333.         J = J - 1
  334.       WEND
  335.       IF I > J THEN EXIT DO
  336.       IF I < J THEN
  337.         SWAP Index(I), Index(J)
  338.         IF Temp = I THEN
  339.           Temp = J
  340.         ELSEIF Temp = J THEN
  341.           Temp = I
  342.         END IF
  343.       END IF
  344.       I = I + 1
  345.       J = J - 1
  346.     LOOP WHILE I <= J
  347.  
  348.     IF I < Last THEN
  349.       QStack(StackPtr) = I             'Push I
  350.       QStack(StackPtr + 1) = Last      'Push Last
  351.       StackPtr = StackPtr + 2
  352.     END IF
  353.  
  354.     Last = J
  355.   LOOP WHILE First < Last
  356.  
  357.   IF StackPtr = 0 THEN EXIT DO          'Done
  358.   StackPtr = StackPtr - 2
  359.   First = QStack(StackPtr)              'Pop First
  360.   Last = QStack(StackPtr + 1)           'Pop Last
  361. LOOP
  362.  
  363. ERASE QStack                    'delete the stack array
  364.  
  365. END SUB
  366.