Turbo Pascal for DOS Tutorial Table of Contents Copyright (c) 1996 by Glenn Grotzinger Part 1: The basics. A) Basic discussion of what a program is and some rudimentary operations such as assign statements, simple variables, computations, and screen writes and reads. Part 2: IF statements, FOR loops, and format codes. A) Concepts of the usage of IF statements, FOR loops, and format codes that may be placed with writes. Part 3: WHILE loops, REPEAT loops, CASE statements, string addressing. A) Concepts of the usage of WHILE loops, REPEAT loops, CASE statements. B) Methods on how to address a string or parts of a string. Part 4: Procedures and Functions; TYPE and CONST declarations. A) Methods of defining procedures and functions in a program. B) The usage of TYPE and CONSTant declarations. Part 5: Reading and Writing to Text Files; writing to the printer A) Concepts of reading from text files. B) Concepts of writing to text files and to the printer. Part 6: Arrays and their Usage; ASCII conversion functions A) Concepts and usage of arrays. B) Usage of the functions chr() and ord(). Part 7: Records and their usage; Mathematics Concepts of the Computer. A) Concepts and usage of records. B) Concepts of Mathematics. 1) Mathematics functions offered by TP. 2) Binary and Hexadecimal notation. Part 8: DOS file functions and their usage. A) A basic summary of Turbo Pascal functions and procedures that are equivalent to DOS commands. B) Concepts of most of the procedures and functions in the DOS or WinDOS units. Part 9: Applications Development. A) Concepts of applications development. The usage of pseudocode to solve a problem. Part 10: Reading and Writing to Binary Files; Units and Overlays. A) Concepts of reading binary files. B) Concepts of writing to binary files. C) Concepts and usage of units. D) Concepts and usage of overlays. Part 11: Interfacing with a Common Format; or how is different variables stored by Pascal in memory, and on disk files? A) Discussions of storage formats in memory and disk by Turbo Pascal. B) Interpretation of meanings of data presented on a commonly used format. Part 12: Stacks; Queues A) Concepts and usage of stacks and queues. Part 13: Use of Recursion A) Concepts and usage of recursion. Part 14: The CRT Unit commands not already covered; Reading of extended keys on the keyboard. A) Changing colors on the screen (text mode). B) Working with the PC speaker. C) Controlling the screen appearance and cursor position. Part 15: Three different designed array sorts. A) Definition and concept of usage of the BubbleSort. A) Definition and concept of usage of the QuickSort. B) Definition of the ShellSort. Part 16: Methods of searching arrays for data (binary search). A) Concepts of the Serial Search. B) Concepts of the Binary Search. Part 17: Use of Pointers in variables, and procedures; designing a set exit procedure (exitproc). (Dynamic Variables) A) Concepts of the addressing of pointers in variables. B) Concepts of addressing pointers in procedures and functions. C) Design of an exit procedure (use of exitproc). Part 18: Design and use of chained lists, or linked lists; the linked list sort. A) Concepts of SLLLs, DLLLs, SLCLs, DLCLs. B) Sorting data using a linked-list. Part 19: Descriptions of other types of pointer-linked structures; A) Concepts of a binary tree. B) Usage of a binary tree. Part 20: Linking assembler into Pascal code; special topics. A) Linking OBJ files into Pascal code. B) Hooking a hardware interrupt and writing interrupt code. C) Calling a specified interrupt procedure. D) Including inline statements in pascal code. E) Including assembler statements in pascal code. F) Conditional compiler directives. Part 21: BGI graphics; plotting graphics. A) Concepts of usage of the Borland Graphics Interface. B) Plotting graphics. C) Concepts behind drawing of lines, circles, and other geometric figures. Pascal Tutorial: Learning Pascal by Glenn Grotzinger Part 1 -- Starting Out. all parts copyright (c) 1995-1996 by Glenn Grotzinger. I am writing this tutorial as a means for people to start out and learn a great deal about Pascal. I write this part with the understanding that you have looked at the compiler, and have figured out how to use it to enter a program, how to compile and run a program within the IDE, save a source code file, or compile at the DOS prompt, or in the IDE to create an EXE file; but do not have any understanding of Pascal programming as a whole. Any subsequent parts will be written assuming that you have read prior parts, and fully understand all the examples. Please work through the examples, entering them in yourself, to get used to programming in Pascal. I also recommend that you print all parts out, so you may have reference for any future parts that we see. As I am writing each part as we go, for right now, I will try to have each part written and posted on a weekly basis. About this part =============== This will be a lengthy part, since we are trying to introduce enough material to get to the point of writing a simple program. The basics ========== My intent in this section is to familiarize you with the proper starting structure to remember for to get ready to write a program. Let's look at the short example below. (I numbered the lines with {}'s for purposes of this explanation.) {1} program tutorial1; uses crt; {2} var {3} world_stmt: string; {4} begin {5} world_stmt := 'Hello world!'; {6} writeln(world_stmt); {7} end. This is a relatively simple program for starting out. Let us run through what each line has. {1} this is the programID, and the uses clause. program ; uses ; is the syntax. may be anything that we may decide to call the program. is a specification of additional libraries, or *units* of commands that we may want to use. crt is the name of one library we will use a lot in our programming. The commands that can be accessed in this library will be detailed later on in the tutorial. If we want to use any of the commands in a library, we must tell the compiler to use the library, and the uses statement does this. I will specify if any commands we use will come out of a unit. {2} var is a signal we use at the beginning of a program block to tell the compiler that we want to start defining variables. {3} This is a definition of a string, or sequence of text. Variable defs. will be covered later in this part. We are defining a variable named world_stmt to be a string. {4} begin says we want to begin a block of code. {5} & {6} are some program commands. We will explain them later. {7} end; ends a program block. A few basic commands ==================== I will now discuss variable definitions. string: a section of text. "Hello world!" would be a string. integer: a number which does not have a decimal part. 12 is an integer. char: one part of a string. "G" is a character, while "GG" is not. real: a number which has a decimal part. 3.25 would be a real. Comments. If you wish to make some text as a remark to what something does, use the { key or (* to start and the } or *) to end it. The assign. We use the := to assign a value to a variable. Examples of that would be such as: world_stmt := 'Hello world.'; { a definition of a string. The ' s must be there on each side } choice_char := 'a'; { a definition of a character The ' s must be there on each side } money := 3.25; { a definition of a real } coins := 10; { a definition of an integer } Arithmetic computations. We often have to do arithmetic to program and solve a problem. I will illustrate addition, subtraction, multiplication, and division. sum := 3 + 2; { we're telling the computer to add 3 and 2 and then place 5 in an integer called sum. Assignments can also work with this way } sub := 10 - 7; { we're telling the computer to subtract 7 from 10 and then place 3 in an integer called sub. } mult := 3 * 2; { we're telling the computer to multiply 3 by 2. } divisn := 10 / 2; {dividing 10 by 2 } Any of these can be combined in one statement, with the order of operations being /, *, -, +. ('s may be used to force one group of numbers. For example: answer := 3 + (2 + 6) * 4; Rules: We must use the following idea to determine whether things are OK to do for arithmetic. 1) If we perform an arithmetic operation with anything with a real in it, the receiving variable in the assign must be defined as a real. 2) If we perform a division with two integers that have a chance of dividing to become a real, we must use a real for a receiving variable. 12 / 7 would be an example and be 1.71 (rounded to 2 decimal places). Note: dividing and getting a real or using a real in the other stuff will result in answers such as 3.232133412E+02. I will cover later the way to make that look readable and normal. We will now look at an example of some of the stuff above. program tutorial2; var first_number, second_number: integer; result: integer; begin first_number := 3; {assign first number the value of 3} second_number := first_number * 2; {assign 2nd number-multiply first number by 2} first_number := second_number - 5; {assign 1st number-old value of 1st number - 5} result := first_number + second_number; {assign result to be 1st number + 2nd number} end. A question to understand what is going on. Answer this one, and you understand everything up to this point. What is the value of each and every one of the integer variables as listed in the tutorial2 after all the statements execute? (Answer will appear in the next part). DIV and MOD These are special operators. Div places the whole number of a division in the receiving variable, and MOD places the remainder. For example: whole := 12 div 7; {whole becomes 1} remainder := 12 mod 7; {remainder becomes 5} 7 goes into 12 one time with a remainder of five. reading and writing information. We will stick to use of the keyboard for reading information, and the monitor for writing information right now. Remember for any variable, we must not define it to be the name of a command, when we do this. read(a_number); This command stops the program and waits for the user to input data which will be placed in a_number and does NOT produce a movement to a new line. readln(a_number); This command does exactly as read, but produces a movement to a new line. write(a_number); This command writes the contents of a_number to the screen without a new line. writeln(a_number); This command does exactly as write, but produces a movement to a new line on the screen. These commands can be used with any combination of literal, variable, or arithmetic expression. A literal is a defined statement. 3 is a literal in write(3);. write(3); will write a 3 on the monitor. program tutorial3; var some_text: string; begin write('Type some text and press ENTER when done: '); readln(some_text); writeln('You just typed the following: ', some_text); end. Proper events in this program will be (as it will appear on the screen): Type some text and press ENTER when done: You just typed the following: The readln prompts you to enter text which is rewritten with the last writeln command. If you type the examples in and compile them up to this point (as I recommend -- it will help you learn), you will see exactly how tutorial3 is supposed to work. The End of Part 1 ================= All of my tutorial parts will be formatted much like this one. First I will cover some new topics, giving examples, then as the final act will always be a programming problem, which I will leave you, the reader, to solve, learning on your own. The best thing to learn and get competent in any language is to actually sit down and program. Any of the programming problems I leave you will not involve concepts that I have not covered in previous text, though I will try my best to make them challenging to further the reader's programming ability. A solution to each of the problems in each part will be presented in the next part. My rules to you. These are for your benefit. 1) Do not ask anyone else to help you in any programming sub, or anywhere in programming these little practice programs I give. You will not learn anything, if at all, and your time looking through this will be wasted. 2) Anyone who does know, please do not help anyone who is going through this tutorial. They will not learn if someone else gives them the answer! 3) Try and at least attempt the practice programming problem. Do not just sit and wait until I present a solution. The syntax is easy to learn from having notes and such, but the logic of actually programming some- thing is only gotten by practice. If you wish me to give your code from the practice programming problems in this tutorial a quick look, send 'em to me at ggrotz@2sprint.net. Practice Programming Problem for Tutorial Part 1 ================================================ Write a Pascal program (and entirely Pascal) which will accept two integers from the keyboard, presenting the user with a prompt to enter a number for each integer. Then print out statements which tell us what the two digits add up to, subtract to, and multiply to. To be correct, you must act on the first number and then the second number in your computations. For example, 14 and 7 (in that order) would be treated as 14+7, 14-7, and 14*7. Example Monitor Screen (using 14 and 7): ------------------------------------------------------------------------ Please enter the 1st number: 14 Please enter the 2nd number: 7 Adding 14 and 7 gives 21. Subtracting 7 from 14 gives 7. Multiplying 14 and 7 gives 98. Good luck! And a solution to this problem will appear in part two! Next Time ========= Next time, we will discuss the use of decision-making (IF statements) in Pascal programming, as well as loops which repeat a defined, set number of times (FOR loops). Eventually, when the tutorial is over, we will have covered most, if not all of the data and control structures, and a few special topics of interest to you (in Pascal). If you have any comments please send them to ggrotz@2sprint.net. Turbo Pascal for DOS Tutorial Part 2 -- IF statements and FOR loops. by Glenn Grotzinger all parts copyright 1995-96 (c) by Glenn Grotzinger. Hello again. This part got in a little late (finals in COBOL programming). We'll get started with IF statements and FOR loops after we get the old business taken care of. Before, we saw this set of code: first_number := 3; second_number := first_number * 2; first_number := second_number - 5; result := first_number + second_number; The question was what the value of all the variables would be at the end of the code. Let's look. The first statement is a simple assign statement, assigning first_number the value of 3. The second statement assigns second_ number the value of first_number (3) * 2 which is 6. The third statement then assigns the first_number the value of second_number (6) - 5 which is 1. And then the final statement assigns result to the addition of first and second number. So, the values of all the variables would be first_number := 1; second_number := 6; and result := 7;. The example of a solution of last weeks programming question: program part1; { This program accompanies part1. It is a program designed to take 2 numbers as input from the keyboard and perform an addition, multiplication, and subtraction on the number, writing the results out to the monitor. } var num1, num2: integer; begin { part1 } { take input for the 2 numbers } write('Please enter the 1st number: '); readln(num1); write('Please enter the 2nd number: '); readln(num2); writeln; { place an empty line } writeln('Adding ', num1, ' and ', num2, ' gives ', num1 + num2, '.'); writeln('Subtracting ', num2, ' from ', num1, ' gives ', num1 - num2, '.'); {It is OK to break a command-line of code line this, as long as you don't break it on a literal statement} writeln('Multiplying ', num1, ' and ', num2, ' gives ', num1 * num2, '.'); end. { part1 } OK. Let's move on to other topics.... Format Codes for Read, Write, Readln, and Writeln ================================================= For all of these commands, we can use formatting codes to place our output, or input from any source, or to any source (these are always applicable). Typically, remember that a monitor in text mode has 80 columns and 25 rows. writeln('I am centered on the screen.':62); This is a good first example. What will happen is that writeln will place the end of this text (the .) on screen position 62 columns relative to where the write pointer is before this command is executed. Another example. write(1 / 3 :8:3); write(1 / 4 :8:3); Remember that for the screen placement that write/read will invalidate them if they are not compliant (your code for that line must be greater than the actual # of characters in the statement you write/read). With the example above, we see that it can be done with any type of variable we can write, or read that is useful. Above, I'm using expressions of division to illustrate a point I mentioned last time about regular non-integer division using decimals. We'd get something like 3.3333333333E-01 for the first state- ment, that is if we didn't use the second :# statement. Obviously, if we write a program we want to be understandable, we have to use something to get regular decimal output (incidentally, the 3.33... stuff is how Pascal stores real numbers in memory -- as scientific notation.). So we use the second statement. The number is the number of decimal places we want to use for the number. So, to use those examples above, with a nice little counter rule (so we can see what is happening only) would look like this: 0000000001111111 1234567890123456 0.333 0.250 We see that the first number is kicking in...The last position is the 8th column from the last write position (if we had writeln'd the first one, they would be lined up on 2 separate lines). And the answers are reported for us in decimal format to the 3rd decimal place. Just play around with writing different things with these codes, and you'll get the idea of how to use these codes when writing or reading something. Quick note ========== We use ' marks to enclose statements. What if we want to write one? Pascal recognizes that if you place two of them together inside those ' marks, it will write the actual ' to the output file. Also, if you can locate an ASCII table, you can write ASCII codes (lines and such) to screen like this: write(#225); will write ASCII character 225. You should be able to locate an ASCII chart in your DOS manual, or your Pascal references that came with your compiler. IF statements ============= A lot of times, we need to make decisions on a particular course of action. IF statements are one of those tools. If a condition is true, then perform action is basically the logic behind this statement. We can also assign an alternate action for the condition. We will see with this provisional little example. program tutorial4; var first_number, second_number: integer; begin writeln('Type an integer in, please.'); readln(first_number); writeln('Type another integer in, please.'); readln(second_number); writeln; if first_number > second_number then writeln(first_number, ' is greater than ', second_number, '.') else writeln(first_number, ' is not greater than ', second_number, '.'); end. Basically, we made a decision based on the size of the two numbers and wrote the result of that decision. Note the format of the test statement. You can use any of the symbols you remember to relate things together. Not equals is <> in pascal. Greater than or equal to is >= in pascal. Less than or equal to is <= in pascal. This type also introduces a new type of variable called boolean. It can only have two values: true, and false. This type is often good to test conditions of run-time, such as you see with a lot of programs out there which can be configured. IF statements can be used with boolean variables easily as well. Begin and end (end with a ; after it) can also be used in if-else statements to execute MORE than one line of code if the condition is met, or not met. We use the next example. We also use a multiple-choice option for function. We see also a way (not a good one, since there will be better methods we will cover later.), to allow a catch-all system. This is also an illustration of something we can most definitely do, is nest if-else statements. program tutorial5; var one, two: integer; option: char; begin writeln('Enter an integer.'); readln(one); writeln('Enter another integer.'); readln(two); writeln('Use a mathematical symbol to indicate what you want to do'); writeln('with these two numbers.'); readln(option); if option = '+' then begin writeln(one, ' + ', two, ' = ', one + two, '.'); writeln('See, I can add.'); end else if option = '-' then writeln(one, ' - ', two, ' = ', one - two, '.') else if option = '*' then writeln(one, ' * ', two, ' = ', one * two, '.') else if option = '/' then writeln(one, ' / ', two, ' = ', one / two :0:3, '.') { we want to have the decimal point, so we MUST have the first one as well to be set to 0. } else writeln('Use +, -, *, or / as your operator. Try again.'); end. We can do pretty much as many nested ifs as we can, though we need to mini- mize it as much as possible. Also, keep in mind, we can use AND or OR to multiply conditions. Say, on the division, if we wanted to only honor a division if the first number was greater than the second number For that section of code... if (option = '/') and (one > two) then ... Code that's applicant to the IF statement above will execute ONLY when both conditions are true. If there are problems that come up in a program using this, keep in mind the {$B+} compiler directive. Compiler directives are placed above the program; part of the program. Type it as I typed it in the illustration. What this does is make it evaluate all the statements of what I showed above. Default for Pascal w/o this directive is short-circuit evaluation. If option is not / in the above statement, it will not bother to look at the one > two part of it. This won't hurt the statement I made above, but it may for others. It's something to experiment with...there are no set rules on when to use the {$B+}. FOR loops ========= This is a means to repetively execute commands a SET number of times. They are implemented by an index variable, generally, an integer, but can also be a character. THE INDEX VARIABLE VALUE IS NOT ADDRESSABLE OUTSIDE A FOR LOOP! Always remember that, though one of those index variables may be used as a "clean slate" variable for other things. Examples of FOR loops are... for i := 1 to 7 do { starts at 1, counts to 7, stepping/adding by 1 each execution } ... for letter := 'a' to 'z' do { starts at a, counts through the alphabet, stepping to z by 1 letter } for i := 10 downto 1 do { starts at 10, counts down to 1, stepping/subtracting by 1 } For this short example, keep in mind that the index variables CAN be addressed inside of the for loop for the values we want.... For counter variables, it's generally OK to use some letter like i or j or whatever... Do make your variables DESCRIPTIVE, though. That's a good programming practice... program tutorial6; var i: integer; begin writeln('I''m going to write something 10 times.'); for i := 1 to 10 do writeln('something', '(Time #':15, i, ')'); end. Type this one in and run it, and you'll see what it does. It writes the first statement above, then writes... something (Time #1) something (Time #2) ... something (Time #10) We can see a good, real use for this, and keep in mind that groups of statements can be executed in a for loop, just like if statements by using begin and end; operators, and for loops can be nested as well... Practice Programming Problem Notes ===================================== Have people been able to do them adequately? Please tell me. Always be sure to type the examples I give out to see how they work. Experimentation and doing the practice programming problems are the best things you can do to learn by using this tutorial. Remember, also, to print each of these parts out as reference for later parts. Make as a goal for the problems I present at the end to follow all the guidelines, and try to make them as SHORT as possible in # of lines of code. Also, someone suggested a mailing list to continue this. If someone would tell me how to set it up (hint, hint), we may go to that. Any comments, questions, requests to look at practice problem code, may be sent to ggrotz@2sprint.net. Always keep in mind that there are many different solutions possible to one problem, and don't be upset if your program didn't look EXACTLY like mine. If the output is ACCURATE and your output looks exactly like mine, then your program is good, and correct. Practice Programming Problem #2 =============================== Create a program in Pascal and entirely Pascal that will query the user for a dimension number to be entered from the keyboard which can not be greater than 15 (write an error message if it is), and then write out a multiplication table with the dimension given by the user, with 3 column spaces between each digit and the double-line (you must use the formatting codes to accomplish this) to the screen. Your program must calculate everything, and nothing can be hard-coded (for example: write('1 2 3 4 5 6') or something like that). Example output for this program after execution should be something like this on your screen (if you see garbage around the lookup columns, those are continuous double lines ): -------------------------------------------------------------------------- What dimension number do you want for a table? 4 1 2 3 4 ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ 1º 1 2 3 4 2º 2 4 6 8 3º 3 6 9 12 4º 4 8 12 16 Note how I have the numbers lined up in the table. Your program should do this as well. If a dimension is greater than 9, your program should allow for it by lining up the row counters like you have the answer columns. Treat what you see above for the table literally (the left side of the file as the left side of the screen) For example: 9 10 Good luck, and a solution to this problem will appear in part 3! Remember to document your code (sort of like I did with the part 1 answer), and try to keep it short. The shorter program is the better program, of two programs that give correct output. On this one, cosmetics are tedious, but to get the job done remember that FOR loops are for things that repeat a set number of times (if you want a goal for practice on getting this one the least number of lines possible, my solution for this problem is 50 lines long for code.) Next Time ========= Next time, we will discuss the use of while and repeat loops, and the case statement. By all means, send comments, and queries on problems you may be encountering with problems in this tutorial to ggrotz@2sprint.net. Turbo Pascal for DOS Beginning Tutorial by Glenn Grotzinger Part 3 -- While and Repeat Loops; Case Statements all parts copyright 1995-96 (c) by Glenn Grotzinger. Hello again. I haven't gotten much input about continued interest in this tutorial. I may discontinue it if the interest doesn't pick up. I am planning on taking this tutorial through all of the Pascal data structures, and maybe special topics (advanced ones, too, listen up self-professed Pascal experts -- you may even learn something. :>). I haven't gotten any suggestions on the special topics!!! Please give comments to ggrotz@2sprint.net. An example of a solution of last week's programming problem: program part2; { This program accompanies part 2. It is a program designed to take a number representing the dimensions of a multiplication table from the user, and then write a multiplication table out to the screen. } var dimension: integer; i, j: integer; begin { part 2 } {take input for the dimension} write('What dimension number do you want for a table? '); readln(dimension); writeln; { if dimension not greater than 15, process table } if dimension <= 15 then begin { write top line of table } write(' '); for i := 1 to dimension do write(i:4); writeln; { write top rule line } write(#201:3); for i := 1 to (dimension * 4) do write(#205); writeln; { write rest of table } for i := 1 to dimension do begin if i < 10 then write(' '); write(i,#186); for j := 1 to dimension do write(i*j :4); writeln; end; end { else it's greater than 15, write an error message } else writeln('You must give a dimension that''s 15 or lower.'); end. If there are any questions as to understanding, or difficulties in solving the practice problems I pose (only way to improve your own programming talent is to practice...), write ggrotz@2sprint.net. On to the new stuff.... WHILE loops =========== It is possible to have a loop to perform a set of commands a non-set number of times, until a condition is met. It can be a contrived one (we can code the basic idea of a FOR loop using a while loop or the repeat loop. We see this in the recode of tutorial6 from last time I will make using a while loop instead of a for loop. program tutorial7; var i: integer; begin writeln('I''m going to write something 10 times.'); i := 1; while i <= 10 do begin writeln('something', '(Time #':15, i, ')'); i := i + 1; end; end. As we see when we run this program, it produces the same output as program tutorial6 did. The statements in the while loop function while the condition is true. When i becomes 11, the loop breaks off and the program ends. Like the IF statements, we can place multiple conditions by connecting them like before with the AND or OR identifiers... REPEAT loops ============ This is another loop we can use. The WHILE loop will function while a condition is true. The REPEAT loop stops fun- ctioning when a condition is true. We see the idea of this again, when we reconstruct program tutorial6 using the repeat loop... program tutorial8; var i: integer; begin writeln('I''m going to write something 10 times.'); i := 1; repeat writeln('something', '(Time #':15, i, ')'); i := i + 1; until i > 10; end. The programs tutorial6, tutorial7, and tutorial8 perform the same things, with loops, using different ideas. The difference of the while and repeat loops over the for loop is that UNDER NO CIRCUM- STANCES IS THE INDEX VARIABLE FOR A FOR LOOP TO BE CHANGED WHILE INSIDE THE LOOP. The conditional variable for a while or repeat loop can be easily changed as we saw in tutorial7 and tutorial8. There are many choices and options we can implement with the while and repeat loops. Menu systems are often implemented like this Continue until user wants to quit. is the basic logic.). CASE statement ============== As we saw in tutorial5, we may want to make a choice based on many, multiple options. This statement is analogous to a series of IF statements on the same variable. The CASE statement reduces the wordiness of such a construct, and makes things easier. I was eluding to this statement before when I mentioned then that there is a better way of doing it. Well, here it is. We use the example of tutorial9 below, which is a rewrite of tutorial5, to illustrate the use and syntax of a case statement. Keep in mind that the operator we use in the case statement (like option below) must be a character or an integer... program tutorial9; var one, two: integer; option: char; begin writeln('Enter an integer.'); readln(one); writeln('Enter another integer.'); readln(two); writeln('Use a mathematical symbol to indicate what you want to do'); writeln('with these two numbers.'); readln(option); case option of '+': begin { sub procedures can be coded as well } writeln(one, ' + ', two, ' = ', one + two, '.'); writeln('See, I can add.'); end '-': writeln(one, ' - ', two, ' = ', one - two, '.'); '*': writeln(one, ' * ', two, ' = ', one * two, '.'); '/': writeln(one, ' / ', two, ' = ', one / two :0:3, '.'); else {catch rest} writeln('Use +, -, *, or / as your operator. Try again.'); end; {case includes an implied begin. We MUST end. } end. As I said in the note, case statements include an implied BEGIN. We must say end; to complete the case statement. The case statement is formatted for each of our choices above, and the else can be used as a catch-all in case the user places something in there that we don't account for in the program, so we can write an error message to the user. The syntax of the case statement is basically as above. You see everything that can be done with the case statement under Pascal. Random and Randomize ==================== We can generate random numbers by doing the following. At the beginning of the program, call randomize. Then do random(). What will happen is it will produce an integer from 0 and less than the number you put in. Random(3) will produce a random number from 0-2. Example. program tutorial10; { write 10 random numbers between 1 and 20 } var number: integer; i: integer; begin randomize; { start the random number generator. Only call once, but must call! } for i := 1 to 10 do writeln('Random number (#', i,') = ', random(20) + 1); { produce random number from 1-20 instead of 0-19 like random(20) only does } end. The Upcase and Length Functions, addressing strings ==================================================== upcase(char) command will place a letter into uppercase. This command is useful for input prompts where the user is asked to give input that involves a character or a string. To illustrate the use of this command, which may be placed in a write statement, or an assign (it is what is defined as a function. We will see what that is next time.). write(upcase('c')); will produce a C on the screen. Another example, which uppercases a string. We use the function length(string) which is useful for that purpose. Given a string into that function, it will return an integer length of the string. for i := 1 to length(inputstring) do upcasestring := upcasestring + upcase(inputstring[i]); This for loop will accomplish it. The thing we see also, with the illus- trations of the upcase and length functions, is that we can address any part of the string by stringname[position in string]. For example, the string "Charleston" can be there as inputstring. If we wanted the 5th character of the string, we say inputstring[5]. inputstring[5] would be equal to 'l', since it's the 5th character of the string. Programming Practice Problem Notes ================================== I am beginning to see the problems get more difficult as there are more things we know, and we can do more useful and fun things with our coding. You may even want to start setting out and solving simple mathematical problems for homework, or coding up a simple program that can figure up your checkbook ... it can be done with what you know now, believe it or not. Think about what you can do with your new found knowledge. Do not try and overextend yourself trying to do things you have no knowledge of as of yet. Attempt this practice programming problem as you hopefully have the others. It's a rather fun one, as it's an actual game. We see we are coming far and there is a lot farther road to go. There's lots more concepts we have to learn, which will enable us to do a whole lot more. Look forward to several more parts, hopefully... Practice Programming Problem #3 =============================== Create a program in Pascal and entirely Pascal that will enable the user to play a guessing game using the keyboard and the monitor as input and output. The points to be addressed in programming this game: 1) The number range of the guessing game must be from 1 to 100. 2) The user must be given 6 opportunities to guess the number. 3) After the user guesses at the number, the program is to answer the user by saying whether their guess was high or low and tell them the number of guesses they have remaining. 4) If they guess the correct number, give them a congrats message. If they exhaust their attempts, give them a try again message, revealing the correct number. 5) Give them the opportunity to play again by asking whether they want to play again (Give them a Y/N prompt.). Be sure to take care of all 4 variants of this choice by using the most efficient method you have available to you. 6) Remember as always to code as efficiently as possible. This one can be printed out on one page (38 lines to be exact for my sample of this one). Use that as a coding goal for your learning. 7) If you get stuck, e-mail me about it, and I'll see if I can help. Have faith. You have the ability to do it. sample output ------------------------------------------------------------------------- I'm thinking of a number between 1 and 100. What is it? 50 It's higher. (5 guesses remaining) 75 It's lower. (4 guesses remaining) 63 It's higher. (3 guesses remaining) If right: Congratulations! You got the number right! If wrong: Sorry, you ran out of choices. The number I was thinking of is 68. Play again: Do you want to play again? (Y/N) ------------------------------------------------------------------------- The right / wrong / play again messages are not statically required. I am only giving examples of what they may be. Your goal for this game should be to program it so it is playable and user-friendly. I recommend you to use the prompt structure right above, though. Next Time ========= We will discuss functions and procedures and their use next time. Please refer your comments to ggrotz@2sprint.net. Turbo Pascal for DOS Beginning Tutorial by Glenn Grotzinger Part 4 -- Writing Procedures and Functions all parts copyright 1995-96 (c) by Glenn Grotzinger. Hello, again, and lets get started... An example of a solution of last week's programming problem: program part3; { This program enables a user to play a guessing game. Range: 1-100 Guesses: 6 } var choice: char; randnum, number_given, attempts: integer; begin randomize; { start random # generator } while upcase(choice) <> 'N' do { while we want to play } begin randnum := random(100) + 1; { generate random number for game } attempts := 6; { set initial number of attempts } number_given := 1000; { stuff number_given so while kicks in } writeln('I''m thinking of a number between 1 and 100. What is it?'); while (attempts > 0) and (number_given <> randnum) do { while we still have attempts to guess } begin readln(number_given); if number_given = randnum then writeln('Congratulations! You got the number right!') else begin attempts := attempts - 1; if attempts = 0 then writeln('Sorry, you ran out of choices. ', ' The number I was thinking of is ', randnum, '.') else if number_given > randnum then writeln('It''s lower. (', attempts, ' guesses remaining)') else writeln('It''s higher. (', attempts, ' guesses remaining)'); end; end; writeln; writeln('Do you want to play again? (Y/N)'); readln(choice); end; end. If there are any questions or difficulties, write ggrotz@2sprint.net. On to the new stuff... PROCEDURE writing ================= This is a way a lot of things work in Pascal. For example, the write command is a procedure. We say the procedure name, then a list of parameters which describe what we want to write. Write is defined in Pascal itself, but we can write our own procedures to do things. Before the main part of the program, and after our variable declarations, we place our procedures. Procedures are essentially whole separate programs which perform small tasks in a bigger program. Here is a short example of what I mean: program tutorial11; var one, two: integer; final: integer; procedure addtwonumbers(first, second: integer; var answer: integer); begin answer := first + second; end; begin writeln('Adding two numbers:'); writeln('Type two numbers in (space after each number) to add.'); readln(one, two); addtwonumbers(one, two, final); writeln('The answer is ', final, '.'); end. The procedure enables us to perform more modular, understandable, and easier to debug programs. We always code logical parts of the program together in procedures and functions (will be discussed later). This example is pretty simplistic, as we include only one statement in a procedure (generally a waste of time), which could easily be in the body of the program. We can use one statement procedures in more logical things, such as conversions of one set of unit to another. We describe the parts of the procedure declaration above, and the calling of the procedure. We can define a series of statements as long as they are the same type, and we don't want to keep them without the use of the VAR section of the procedure. We don't care about, and don't modify first, and second, so we place them in there without the var. We modify the variable used as answer, so we MUST have the VAR before it in order to have the variable survive in a modified state by calling the procedure. Play around with what happens in tutorial11 when you remove the var from in front of the third statement. When we call the procedure, we use variables which match and make sense to what we want each variable we want. we use one, and two for the numbers to add, because our procedure is designed to add those first two numbers. Then we put final in the last one, because that is our final answer. We can SAFELY use the same variable names for the procedure and the globals, but it is a good idea not to, as it will not always be possible to correlate our items in that way. We should always have a goal to write procedures/functions with ability to drop them into another program where all the parameters are passed to it. We can easily drop the procedure written above into any other program that needs a similar function. It is possible to have a parameterless procedure as well, if it is predicted as required. Say...In a multi-function menu system or something that does a set thing (clrscr is an example). NOTE: It is always good to keep your old code when you write and use parameter passing in your procedures and functions, so you can easily re-use your code and save time in having to rewrite old code. FUNCTION writing ================ Function declarations are exactly like procedures, except that functions have the capability to return a value. We see in the example, below, which is a rewrite of tutorial11. program tutorial12; var one, two: integer; final: integer; function addtwonumbers(first, second: integer):integer; var answer: integer; begin answer := first + second; addtwonumbers := answer; {we can also do addtwonumbers := first + second;} end; begin writeln('Adding two numbers:'); writeln('Type two numbers in (space after each number) to add.'); readln(one, two); final := addtwonumbers(one, two); writeln('The answer is ', final, '.'); end. In the function, we see we MUST define the final answer to the value of the function. The end part :integer; defines the return value of the function. The way to set up a function should be evident from this example. SETS ==== It's possible in an IF/WHILE/REPEAT to set up a test on a group of statements. For example: IF character in ['a'..'z'] then { if character is in the lowercase alphabet } IF character in ['1','2','3','5'] { if character is 1, 2, 3, or 5 } IF number in [0..23] then { if number is between 0 and 23 } TYPE and CONST statements. ========================== It's possible to type-delineate variables. Such as for example, we may want to limit a string from it's standard 255 characters (if we say just string), to say 10 characters (string[10]), we can not use something like that in a procedure or function (we will see the types used a lot). Therefore, we use the type section to redefine this so we can use them in procedures/ functions. It's also possible to set constants in a pascal program. Say, if we want to set up a constant tax rate, we just define it in a constant section. We see this in an example: program tutorial13; const tax_rate = 0.14; {14% tax rate} type string[15] = string15; { string[15] redefined so we can use it in a procedure/function, though we will not have procedures or functions here. } var total_pay: real; your_name: string15; {if we make type dec, we must carry it across. This variable is a string with a limit of 15 characters. } begin writeln('Who are you? (15 char. max)'); readln(your_name); writeln('How much did you earn this paycheck?'); readln(total_pay); writeln('Assuming, you have a ', tax_rate * 100 :0:0, '% income tax ', 'rate, you will have to pay $', total_pay * tax_rate :0:2, ' in income tax this paycheck.'); end. The use of tax_rate is a prime reason that we would want to define a constant. Instead of using that number everywhere we needed it, we used the reference of tax_rate. Why? If the income tax ever changed in this example, we would not need to go through the whole program and change that number. All we need to do is change that reference. Clearing the screen =================== This will be the first example of a command that we must call a unit for to get. The unit you call (as I did in tutorial1) would be CRT for TP/DOS, and WINCRT for TP/WIN. You use the statement clrscr; for this. This program example clears the screen. program tutorial14; uses crt; begin clrscr; writeln('I cleared the screen for you.'); end. Final Note -========= You should lay out your programs in logical units using functions and procedures, making them parameter passing if possible, and logical. The best may be (especially for programs that do multiple things which are unrelated on code level) to not parameter pass. Whatever works functionally is the best, and if you can't figure out a way to parameter pass to the procedure or function.... Programming Practice Problem Notes ================================== This is the first program, that I would expect you to write using functions and procedures. All programs you should write in the future should strive to use parameter-passed procedures. The reasons are many- fold for this, which I stated before. Be sure you do this. This program should not be any more difficult than any of the previous programming problems I gave. All I expect this one to be is an exercise for you to learn in using procedures and functions. If you want more practice beyond this problem in writing things using procedures and parameters, I suggest you recode the programming problem out of part 2. If you can get it more easier understood to look at, and using parameter-passed procedures for the logical parts of it, and make it work right, then you got it. Practice Programming Problem #4 =============================== Write a program in pascal and entirely Pascal which will present a simple menu driven system (readable, and by pages -- use clrscr in the appropriate spots) which will enable the user by choice to do the following, as indicated by sample output for the menu. sample output for menu ====================== 1. Convert a number of seconds to hours, minutes, and seconds. 2. Convert a given military time to AM/PM time. 3. Quit the program. Please enter your choice now: sample output for option 1 ========================== Please enter a number of seconds: 3600 3600 seconds is 1 hour, 0 minutes, and 0 seconds. Press ENTER to continue: sample output for option 2 ========================== Enter a military time's hours: 14 Enter a miltiary time's seconds: 00 It is 2:00 PM. Press ENTER to continue: ================ Notes: 1. there are 60 seconds in a minute, 60 minutes in an hour. 2. "military time" is 24-hr time. For example, 16:00 would be 4 P.M. 3. The program must continue to function in the menu system until the user wishes to quit. 4. Remember to ask the user for appropriate data that you need. 5. Be sure to pause the screen to enable the user to see the results of their query. You may accomplish this by just calling readln;. It will call the computer to wait until the user presses a key. 6. Be SURE your program is easily understood. The solution to this program will be presented in the next part. Good luck! Next time ========= Next time, we will discuss the usage of text files for reading in data and writing out results. We really are moving along with the concepts rather readily... Turbo Pascal for DOS Beginning Tutorial by Glenn Grotzinger Part 5 -- Reading and Writing to Text Files. All parts copyright 1995-96 (c) by Glenn Grotzinger program part4; uses crt; { This program offers a menu-type system in order to convert a number of seconds to hours, minutes, and seconds; and to convert a military time to AM/PM time } var choice: integer; procedure showmenu; { This procedure shows the main menu. } begin writeln('1. Convert a number of seconds to hours, minutes, and', ' seconds.'); writeln('2. Convert a given military time to AM/PM time.'); writeln('3. Quit the program.'); writeln; writeln('Please enter a choice:'); end; procedure convseconds; var totalseconds: integer; hours, minutes, seconds: integer; temp: integer; begin clrscr; write('Enter a total number of seconds: '); readln(totalseconds); writeln; temp := totalseconds div 60; seconds := totalseconds mod 60; hours := temp div 60; minutes := temp mod 60; writeln; writeln(totalseconds, ' seconds is ', hours, ' hours, ', minutes, ' minutes, ', seconds, ' seconds.'); writeln; writeln('Press ENTER to continue:'); readln; end; procedure convmilitary; { Version 1.1. Bug found and e-mailed to me. It is appreciated. } { Involved the reporting of times from 0000 to 0059...Looks to } { be fixed now. (wrote this in a very dazed state -- post-surgery)} var hours, minutes: integer; meridian: char; begin clrscr; write('Enter a military time''s hours: '); readln(hours); write('Enter a military time''s minutes: '); readln(minutes); if hours >= 12 then begin meridian := 'P'; hours := hours - 12; end else meridian := 'A'; if hours = 0 then hours := 12; writeln; write('It is '); write(hours); write(':'); if minutes < 10 then write('0'); write(minutes); writeln(' ', meridian, 'M.'); writeln; writeln('Press ENTER to continue:'); readln; end; begin choice := 10; while choice <> 3 do begin clrscr; showmenu; readln(choice); case choice of 1: convseconds; 2: convmilitary; end; end; end. OK. On to new stuff... Addressing Files ================ To address a file, we use the assign statement. We use a text variable to refer to the file, and a string to refer to the actual pathname and filename of the file. To open a file, we use a reset(); statement to read it. To write a new file, we use a rewrite(); statement. To close a file after we are done accessing it for read or write, we use the close(). Special Identifiers =================== There are functions defined in standard Pascal which aid us in processing text files. They are eof() and eoln(). They return boolean values. They are signals we can use in loops to aid us in finding our way in text files. EOF signals when we are at the end of a file. EOLN signals when we are at the end of a line of text in a file. NOTE: Keep in mind, we can have as many files defined as we need. The only limitation is that we can only have a maximum of the number of files defined in the FILES= statement of the config.sys of the particular machine we are on open at any one time. It is always prudent to close files after you are done with them. Text file concepts and reading/writing to text files ==================================================== A sample program to illustrate all the items above. We will be doing a character by character read on an input file named DATA.TXT using eoln and eof properly, and uppercasing the output of the file using the standard upcase function. The output will be written to another output file named UPCASED.TXT. program tutorial15; var infile, outfile: text; inputchar: char; begin assign(infile, 'DATA.TXT'); { associate var infile with DATA.TXT } reset(infile); { open DATA.TXT for read } assign(outfile, 'UPCASED.TXT'); { assoc. var outfile with UPCASED.TXT } rewrite(outfile); { open UPCASED.TXT for write } while not eof(infile) do { while we're not at the EOF for infile } begin while not eoln(infile) do { while not EOLN for infile } begin read(infile, inputchar); write(outfile, upcase(inputchar)); { process and output } end; writeln(outfile); { when end of line, advance both files down } readln(infile); { one line } end; close(infile); { we're done with both of these files. Close 'em } close(outfile); { time. } end. This program is primarily for illustration purposes of all the new concepts we need to know in order to access files (there is a lot better way out there of doing it). As long as we remember what exactly is read and considered as each type of variable (integer, char, string[limit], etc, etc), we can read all sorts of data from a text file and write data back out to a text file. To illustrate: If we have the following input file as on disk... 14 23 34 53 32 Glenn Grotzinger 23 23 12 33 23 Clinton Sucks! If we perform ONE read from a varied amount of different types from the first position of the first line, we will see the following: CHARread: 1 INTEGERread: 14 STRING[20]read: 14 23 34 53 32 Glenn STRINGread: 14 23 34 53 32 Glenn Grotzinger readln/writeln goes to the next line, whichever references what we are doing to the text file. We must keep these general rules in mind (I hope you played around with the read and write commands a lot, that playing will help you A LOT!). Another illustration to see usage of files. It's the BETTER rewrite of tutorial15. We must also keep in mind that to read a text file, EOLN is not necessarily required, but EOF is ALWAYS REQUIRED. Improvements: We can use a string and write a function to uppercase the whole string. Plus, there's one little logic error above...Figure out why I do the reads and writes different below and you'll have mastered the idea of reading/writing files...(I intended to just demo the commands earlier, this is a demo of how they should be used logically...) program tutorial16; var inputstring: string; infile, outfile: string; function upstr(instring: string):string; { This function uppercases a whole string } var i: integer; newstr: string; begin newstr := ''; for i := 1 to length(instring) do newstr := newstr + upcase(instring[i]); { we can piece strings together using +. Keep it in mind } end; begin assign(infile, 'DATA.TXT'); reset(infile); assign(outfile, 'UPCASED.TXT'); rewrite(outfile); readln(infile, inputstring); while not eof(infile) do begin writeln(outfile, upstr(inputstring)); readln(infile, inputstring); end; writeln(outfile, upstr(inputstring)); close(infile); close(outfile); end. Remember to play with the logic in accessing text files. And in reading and writing files, BE SURE YOU USE FILES THAT YOU CAN STAND TO LOSE IF YOU ARE NOT COMPLETELY COMFORTABLE WITH PROGRAMMING FOR FILE ACCESS. IF A FILE BY A NAME YOU USE FOR A PROGRAM ALREADY EXISTS ON THE DRIVE, AND YOU REWRITE IT, IT WILL BE COMPLETELY LOST! THIS MEANS ANY UNDELETE PROGRAM WILL *NOT* BE ABLE TO RECOVER THE FILE!!!!!!!!!!! I will cover in a later part how to find out whether files exist on the drive as well as other commands and functions used in Pascal to perform DOS-like functions (delete files, make directories, remove directories, and so forth). Printer Output ============== The printer can basically be treated as a write-only file (you only rewrite it, not reset it). To use the printer? Use the unit printer, like you did the unit crt or wincrt before then write to a text file variable named lst....Printer defines everything you need to write to the printer. Printer assumes LPT1, so if your printer is on something else, you can define the text file variable to be the port address for the printer.... program tutorial17; uses printer; var str: string; infile: text; begin assign(infile, 'PRINTME.TXT'); reset(infile); readln(infile, str); while not eof(infile) do begin writeln(lst, str); readln(infile, str); end; writeln(lst, str); writeln('File sent to printer on LPT1..'); end. Practice Programming Problem Notes ================================== Probably ALL programs I pose on these in the future will involve at least ONE data file off of disk. If it is a binary one that I have created for express purpose of these problems, I will be attaching it to the tutorial message as a binary file attachment. If its a text file, I will probably ask you to create it, giving you the format. If you have been doing source code, you should be able to use a text file editor. Practice Programming Problem #5 =============================== Write a program in Pascal and entirely Pascal which will conduct a worker-pay recording. Be sure the program is modular and uses functions and procedures to the best benefit, as well as efficiently coded (be sure you are using format codes!). We will be reading worker names and data from a file in the current directory with the program named WORKER.TXT. Format of input and output files will be covered later. What we will be doing is figuring out how much to pay each of these employees in our data file. Points to keep in mind: 1) Gross pay is hourly rate * hours-worked for hours worked below 40. 2) 40 hours of time is full-time pay, beyond 40 hours is time and a half. Therefore, we must pay them 1/2 more than we normally would at the hourly computed rate for any hours above 40 from point 1. 3) Income Taxes are 15% of computed gross pay (before taxes, etc). 4) We must indicate all of these deductions by computations in the output. 5) We may have 1 employee, we may have 10,000. We need to give the user some indication as to what we are doing (all we will see is a blank prompt otherwise.) so they won't think our program has hung or crashed. Write a message such as "Processing " to the screen for each employee. Write a report of what our deductions are from each person's salary, and what we are paying each employee to a file named PAYOUT.TXT. Format of WORKER.TXT (I recommend you type this in *EXACTLY* and use it) -------------------- Glenn Grotzinger 44.25 7.34 Joe Schmoe 65.32 4.35 Jim Nabors 40.00 10.01 Sheila Roberts 32.12 6.25 Kathy York 23.21 11.10 -------------------- (the area between all the --- is the WORKER.TXT to use.... -- another note: be sure there aren't any blank lines at the bottom of the file -- those cause problems...) First thing on each line is employee name (max of 20 chars). Second thing is total hours worked. Third thing is pay rate. Format of PAYOUT.TXT -------------------- The International Widget, Inc. PayOut And Deductions Sheet Employee Hours Rate GrossPay Overtime IncomeTax NetPay ======================================================================== Glenn Grotzinger 34.25 7.34 345.23 32.34 15.34 305.23 ... -------------------- (The numbers should be correct, and the file should appear sort of like this, but with more entries (equivalent to the number of entries in the WORKER.TXT file. This is only a sample illustration.) Note: In any program, you should always make accounts for changes in the number of lines of text in an input file. Use EOF. Do not use a defined set loop. Also, as a tip, to get the output the way you want it to look, it doesn't hurt to type it out as a sample, so you know how to space it when you go into the programming part of it. Next Time ========= We will discuss arrays and their usage. If there is a difficulty in inter- preting data files for text files, tell me, and I will probably attach them as binaries in the future. Please send all comments, inquiries, etc, etc to ggrotz@2sprint.net. P.S. Sorry this one ain't too fun...Couldn't think of anything better to get you the practice in using text files... Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 6 -- Arrays and Their Usage. All parts copyright 1995-96 (c) by Glenn Grotzinger Hello again...Let's get started...An example of the solution of the program presented in part 5 is below. program part5; const income_tax = 0.15; { set our 15% tax rate } type string20 = string[20]; { not needed, but good to demonstrate } var employee: string20; hours, rate: real; infile, outfile: text; grosspay, overtimepay, intax, netpay: real; procedure writeheaders; { writes the headers on our report } begin writeln(outfile, 'The International Widget, Inc.':51); writeln(outfile, 'PayOut And Deductions Sheet':49); writeln(outfile); writeln(outfile, 'Employee', 'Hours':17, 'Rate':7, 'GrossPay':10, 'Overtime':10, 'IncomeTax':11, 'NetPay':9); end; function writeline(fillchar: char; strlength: integer):string; { handy function that fills a certain length of a string with a fixed character } var i: integer; str: string; begin str := ''; for i := 1 to strlength do str := str + fillchar; writeline := str; end; procedure processrecord(hrs, rate: real;var gpay, opay, itax, npay: real); { does all of our required calculations for us } var rpay: real; begin if hrs > 40 then { if overtime then } begin rpay := rate * 40; { regular pay up to 40 } opay := (hrs - 40) * rate * 1.5; { overtime rate after 40 } end else begin rpay := hrs * rate; { figure as normal } opay := 0; { set overtime pay to 0, no overtime hours } end; gpay := opay + rpay; { get our gross pay } itax := gpay * income_tax; { get our deduction } npay := gpay - itax; { actual pay to worker } end; begin assign(infile, 'WORKER.TXT'); { prepare input file } reset(infile); assign(outfile, 'PAYOUT.TXT'); { prepare output file } rewrite(outfile); writeheaders; { write headers of report } writeln(outfile, writeline('=', 72)); readln(infile, employee, hours, rate); while not eof(infile) do { while not end of file, read,process&write } begin writeln('Processing ', employee); processrecord(hours, rate, grosspay, overtimepay, intax, netpay); writeln(outfile, employee, hours:5:2, rate:7:2, grosspay:9:2, overtimepay:10:2, intax:10:2, netpay:11:2); readln(infile, employee, hours, rate); end; writeln('Processing ', employee); processrecord(hours, rate, grosspay, overtimepay, intax, netpay); writeln(outfile, employee, hours:5:2, rate:7:2, grosspay:9:2, overtimepay:10:2, intax:10:2, netpay:11:2); close(infile); close(outfile); { close files } end. The Structure of an Array ========================= For a lot of things, it is very useful to know exactly how things are done by the computer in programming. The array is one of them. An array is simply a group, or set of groups of like defined items. An example of such a thing is: firstarray: array[1..3] of integer; What we are doing is defining three integers that we want to indicate by numbers as 1(1st), 2(2nd), and 3(3rd) in the set of integers. Arrays are often used to correlate like items in a program to make them easier to process. Often it is with such data as this: Say we want to work with the high temperatures for each day of one month. It is correlated info, so it is a very good candidate for an array. The numbers between the [] are minimum..maximum index values for the array (to keep track of which part of the array we want). There are 31 days in a month(maximum), so we would store them as something like this: temperatures: array[1..31] of integer; We are defining the total number of integer units we want in the array. Which brings up the question: How is this stored in memory? RULE: ALL MEMORY STORAGE BY COMPUTER OF ANY ITEM IS LINEAR!!!! This rule is always helpful to remember when we work with arrays. It does point out logic rather readily of handling arrays. Now I will illustrate how to access items of the array. Keep in mind that array items can be read from the keyboard or text file, and written just like variables we have worked with before. To read the 5th temperature in the array from the keyboard, we would use something like this: readln(temperatures[5]); Note: in addressing an array, we can even use mathematical expressions... Usage of Arrays =============== If we wished to write out (one per line) to the screen, all the contents in the array, we would do something like this...keep in mind that array variables work exactly the same way as the variables have that we have been working with so far: for i := 1 to 31 do writeln(i, temperatures[i]); Note: often for/while/repeat loops are used to work with arrays to process the whole array using an index variable in the counter. Keep this in mind for your programming. The indexes can be defined to be anything that was valid for use in the for loop, such as: array1: array[1..10] of integer; array2: array[-15..0] of real; array3: array['a'..'z'] of char; This naturally has limits, which you will find if you cross them (compiler warning). What the array is of can be any valid data type. Can we define an array of an array? Yes. Double Level Tables and Above ============================= We can define infinitely, as our memory allows, the number of arrays of arrays we want to define. Say, for our temperature example, we wanted to define the limits of an array for the whole year. We know there are 12 months in the year, so we would need 12 occurences of the array we defined previously for the high temperatures for each day: A definition of this array would be: temperatures: array[1..12] of array[1..31] of integer; This is a very definable array that we could use to store all of our high temperatures for each day of the year. Is there a shortcut, though? Yes, there is. You can use a comma in the first set of [] to define the limits of the 2nd array. Why? We see in the next paragraph. An example of a better definition of this array is: temperatures: array[1..12, 1..31] of integer; We see the parts of each array in the first one there...Remember above that I said computers store things linearly. It helps us when we know this in processing such a thing above. Actually, for double level, triple level, etc, tables, the 2nd array, 3rd array, etc is seen as minor units within the previous defined array. Say, we had this array: array1: array[1..2, 1..2, 1..2] of char; It would be set up in linear memory like this: array1 char char char char char char char char [1,1,1] [1,1,2] [1,2,1] [1,2,2] [2,1,1] [2,1,2] [2,2,1] [2,2,2] array1[1,1] array[1,2] array1[2,1] array1[2,2] array1[1] array1[2] I placed labels on there so we could see the layout. It would continue ad infinitum for a 3rd level or a 4th level on anything. So, as an example of how we can address the array of temperatures above, and work with one, here is a little code that writes 'em all to the screen, 12 columns of integers (one for each month), 31 lines(one for each day in the month): for i := 1 to 12 do begin for j := 1 to 31 do write(temperatures[i, j], ' '); writeln; end; Defining Constant Arrays ======================== It can be done. It's done like this: const months: array[1..12] of string[3] = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'); Ord And Chr =========== These are two functions we can use to work with the ASCII table. They are system functions. Ord(char) gives us an integer that represents the character we put in the ()'s. Chr(integer) gives us a character which corresponds to the ASCII code number that is given in integer. Boolean type ============ A type called boolean exists. It can either be set to true or false. Final Notes =========== As a final note, defining arrays, if you play around with doing that and using them (I hope you do..:>), you may get an error from the compiler that your data structure as defined is too large. The reason is that Pascal defaults to operating and compiling your programs like they were going to work on an 8088 class machine. The maximum addressable memory there is 64KB. If that happens, it means you have too many variables defined. There is a way to get around this, which will be covered in a future part of this tutorial. Don't give up on this, and remember: Experimentation is the best tool for learning how this stuff works... Practice Programming Problem #6 =============================== Once upon a time, a friend of yours did you a really big favor. Now they want you to repay that favor. They are in college and doing a statistical study on the average number of times that each letter of the alphabet is used. To accomplish that, to your unfortunate ears, your friend wants you to sit down with a sheet of paper and a book and start tallying letters on a table that's written on that sheet of paper. You know this is very time consuming and there is probably a better way. You realize that with your programming knowledge that you have and several text files that you have on your hard drive that you can make a program that will accomplish the task much faster. They asked you to count through a 200 page book originally, so you decide that documentation of some shareware (5 text files) that you have are counted and summarized will do sufficiently for what your friend intends. Write a program in Pascal and entirely Pascal that will read no more than 10 text file names from a file named FILES.TXT (you can create it, one filename (full path) per line, be sure you can find them on your drive (I recommend to get them as a whole to be no less than 200KB in size (all files combined), and as large as you want them to be as a whole -- I am only stating a good set of minimums -- the no more than 10 part is a good required part of the program to do.). For each and every file in this list, count the number of times each letter is used (whether the letter is uppercased or lowercased is irrelevant After that, we will have most of the basics covered. That does not mean I will quit. This is, my guess, about 2 parts away from the halfway point (don't quote me on that:>). Anyway, there's lots more to cover, and a few special topics coming up. To illustrate, the tutorial after the records tutorial will involve the use of DOS compatible commands, in other words those things you do in DOS (the first special topic) -- we're gonna go over how to do those things in Pascal. Please send any comments, questions, requests for help with the sample programs (you SHOULD be doing them as you get each of these parts) to ggrotz@2sprint.net. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 7 -- Records usage and Mathematics Concepts of Computers All parts copyright 1995 (c) by Glenn Grotzinger Here is a solution to part 6... {$B+} program part6; type ltrtype = array['A'..'Z'] of longint; {type dec for arrays} var ltrstorage, sums: ltrtype; { hold array and sums array } filelist, counts: text; filename: string; filenums: integer; ltrs: longint; procedure countletters(str: string; var ltrstorage, sums: ltrtype); var i: integer; {65..90} j: char; begin for i := 1 to length(str) do { for length of string } for j := 'A' to 'Z' do { for 26 letters } if upcase(str[i]) = j then begin inc(ltrstorage[j]); {var = var + 1 } inc(sums[j]); ltrs := ltrs + 1; end; end; procedure countfile(filename: string; var ltrstorage:ltrtype; var filenums: integer); var cntfile: text; chkstr: string; begin writeln('Processing ', filename); filenums := filenums + 1; assign(cntfile, filename); reset(cntfile); readln(cntfile, chkstr); while not eof(cntfile) do begin countletters(chkstr, ltrstorage, sums); readln(cntfile, chkstr); end; countletters(chkstr, ltrstorage, sums); close(cntfile); end; procedure writefdata(filename: string; var ltrstorage: ltrtype); var i: integer; j: char; begin { write headers } writeln(counts, 'Alphabetical Count Data':53); writeln(counts, 'for ':40, filename); writeln(counts); writeln(counts, 'Letter':10, 'Count':10, 'Letter':25, 'Count':10); for i := 1 to 13 do writeln(counts, chr(i+64):8, ltrstorage[chr(i+64)]:12, chr(i+77):23, ltrstorage[chr(i+77)]:12); writeln(counts); writeln(counts); for j := 'A' to 'Z' do ltrstorage[j] := 0; { zero back out storage array } end; procedure doend(filenums: integer; ltrstorage: ltrtype); var i: integer; j: char; begin writeln(counts, filenums, ' files processed.'); writeln(counts, ltrs, ' letters processed.'); writeln(counts); writeln(counts); writefdata('all files', sums); end; begin assign(filelist, 'FILES.TXT'); reset(filelist); assign(counts, 'FRNDDATA.TXT'); rewrite(counts); readln(filelist, filename); filenums := 0; while (not eof(filelist)) and (filenums < 9) do begin countfile(filename, ltrstorage, filenums); writefdata(filename, ltrstorage); readln(filelist, filename); end; countfile(filename, ltrstorage, filenums); writefdata(filename, ltrstorage); doend(filenums, ltrstorage); if not eof(filelist) then writeln(counts, 'You gave me more than 10 files to process!'); close(filelist); close(counts); end. On with the show.... Records Definition ================== You can group up different types of data using records. We MUST use the type statement pretty well to use this record type. Here is an example of the defining of a record type in the type section. type employeerecord = record number: longint; surname: string[18]; firstname: string[10]; position: string[10]; yrsworked: integer; end; As you can see, we are grouping many different types of information into one record type. Then for the var section, all we need to do is define ONE variable to be of type employeerecord and we'll have the record variable. Accessing Records in a Program ============================== You can work with sections of a record variable, or the whole record variable. The whole record variable can be accessed just by calling the record variable used. The parts of it require the use of a . followed by the name of the part as specified in the type statement. Also, the WITH command may be used to simplify typing in working with a specific record. The WITH command specifies a specific record variable to work with. An example can be seen below. program tutorial18; type {Employeerecord as above} var workrecord: employeerecord; begin { get workrecord into memory here. } writeln(workrecord.number); writeln(workrecord.surname); writeln('I''m getting sick of typing workrecord all the time!'); with workrecord do writeln(firstname); writeln(position); writeln(yrsworked); end; end. Hopefully, you can see what exactly is going on in working with records. WITH only reduces typing. It's good to use to reduce clutter if you do a lot with one type of record in a section of code. Also, keep in mind that record types can be used in an array.... var workinfo: array[1..10] of employeerecord; Each section of the record works just like ordinary variables, and are addressable like ordinary variables. Also the whole record itself can be addressable and moved around (written possibly to binary files?) to other like records....The above array can be addressed like this: 3rd record in array, surname: workinfo[3].surname Mathematics Concepts in Computers ================================= Hopefully, you got the mathematics lesson from someone, or are already familiar with conversion of number bases. If not, I will run through a quick example... Convert 10 (base 10) to base 2. A base tells us how many numbers are used in counting. Typical numerical usage is base 10 (we use the numbers 0-9 in that order -- we always start from zero in counting. To look at this problem, we look at the right and move to the left with regards to number bases. To use the example of 543 in base 10, it's 5X10^2+4X10^1+3X10^0. Remember, anything to the 0th power is 1. All number bases work this way. The exception is the changing of the multiple. We use 10 in the example above. Using that background, 10 in base 10 is 1X10^1+0x10^0. So, if we go through the process of actually converting a base from base 10.... 10 / 2 = 5 rem 0. (we keep going until the quotient is 0. Right now it is 5.) 5 / 2 = 2 rem 1. 2 / 2 = 1 rem 0. 1 / 2 = 0 rem 1. (We quit here. Then look at the remainders from down to up to get our converted base). 1010 is 10 in base 2....Now, if we want to go to base 10 from another number. Convert 4A (base 16 to base 10). Here, since we want to go to base 10, all we need to do is extend out the base into something we can understand... 4 X 16 + A(10) X 1 = 64 + 10 = 74 (base 10) What does all of this have to do with computers in programming. Let's analyze a few things... If you've seen $ and # and what do they mean? --------------------------------------------- $ is a typical designator in computers that a number is in base 16. # is a typical designator in computers that a number is in base 10. You probably have seen it by now in the ASCII table studies we have done. For example, Z is ASCII character $5A and #90. These are both one in the same. As we see below: 5X16^1+A(10)X16^0 = 80 + 10 = 90 (base 10). 9x10^1+0x10^0 = 5x16^1+10(A)x16^0 (base 16). {90/16 = 5 rem 10} How does my computer store data? ================================ Why do we bother to mess with the different number systems in computing? base 10 is human-understandable, so we use it sometimes. Base 2 is obvious, since this is how the computer talks. (hence BI-nary) All computer storage is actually a series of 1's and 0's or ON's and OFF's. (2 possible combinations, hence base 2). For example, lets convert #65 to base 2 and see how our computers store an A. Let's start from the highest we know we can on the power list for 2's. There are 256 ASCII characters because it was decided sometime in the computer stone age that there would be a total of 8 bits, the elementary unit of storage in a computer, per byte, or next major unit that you all should be familar with in using DOS/ Windows/whatever. If we analyze that using a good permutation scheme.. 2 possible orientations, 8 positions...2^8 or 256 total combinations. So, we will start from 7, since there is no byte #256. 128 goes into 65 0 times. 64 goes into 65 one time with 1 left. 32 goes into 1 0 times. 16 goes into 1 0 times. 8 goes into 1 0 times. 4 goes into 1 0 times. 2 goes into 1 0 times. 1 goes into 1 one time. (stepping down powers of 2.). So typically in expressing a list of bits for a byte, we use 0's for the filler for all 8 spaces, since we NEED to work for 8 bits with a respective byte. So an ASCII related system (there are others) would represent #65 as 0 1 0 0 0 0 0 1. 8 bits, all 0 or 1. If your computer deals with an A, it actually deals with the bit series 01000001. In using a computer, we don't need to know about bits, since each completely meaningful unit to most of us comprises 8 bits. We don't need to normally think of 01000001, the way the computer does it. But for some things in programming, we do, though. Base 16 is also used a lot. Reason? It's a real handy way to define a byte. Let's look at the A. According to the ASCII table, A is $41. 16 = 2^4 so we can see it's a lot handier way to tag around the implied bits of a byte with this... If we look at the bit sequence, a high end of 4 bits would have to be multiplied by 16. So if we look at the meaning of 0100 and 0001, we will see why we use base 16... 0100 is 4. 0001 is 1. Concenate them together, we get 41...Neat, eh? We don't really have all that much concern for base 16 in this tutorial, because in pascal, base 10 will work as well. we do need to be concerned about base 2, though, because the computer uses it. Reasons for knowing what this stuff is... ========================================= We have reasons that we need to know how to convert between the number bases, and be familar with what a bit is...You may be familiar with what is called the high and low orders of a byte. To use the example of the A bit series we converted earlier: 0 1 0 0 0 0 0 1 high order low order There are pascal commands which use the bits for things. Also, some fixed file formats use these (such as compression -- that's actually whats going on -- it works at bit level to compress the number of bytes used) and if you wish to if you ever develop something....) One must have an idea of where the bits are coming from, hence all the stuff I was going through before. If you read through your TP programmer's reference and see it talking about orders of bytes and what goes on with those particular commands, now, you should have the background to know about it and predict what would happen on those commands. First good commands to know about for working with bits ======================================================= We always want to go for the most efficient code available. If we ever need to work with multiplying or dividing powers of 2, we can do it much speedier and easier by having the computer do bit shifts instead of doing an actual multiply or divide command when we are dealing with small numbers. Shifting information around is much easier for the CPU than actually doing the computation. Let us demonstrate. 00000010 (base 2) = 2 (base 10) 00000010 (shift 1 byte to the left) => 00000100 or 4 (base 10) 00000010 (shift 1 byte to the right) => 00000001 or 1 (base 10) Going one way or the other in shifting bytes have the effect of multiplying or dividing by 2^(# of bytes we move). Play with these commands and you will see. It's a bit shift that it does, and is MUCH faster than an actual divide when it comes to any power of 2. Examples: writeln(2 shl 1); {or 2 X 2^1} writeln(2 shr 1); {or 2 / 2^1} Those two commands work this way: command <# of bits to shift> # of bytes to shift turns out to be the power of 2 we do the work on... HI(byte) pulls the high order of the expression. LO(byte) pulls the low order of the expression. swap(byte) swaps the orders. These are all functions. Other Math Functions offered by Pascal ====================================== Abs(X) Takes absolute value of X. Arctan(X) Takes arctangent of X. Cos(X) Takes cosine of X. Exp(X) Takes exponential (base e) of argument. Frac(X) Returns fraction of X. Int(X) Returns integer part of X. Ln(x) Takes base e logarithm of X. Pi Returns value of pi. Round(X) Rounds X(real) to an integer. sin(X) Takes sine of X. sqr(x) Takes square of X. sqrt(x) Takes square root of X. trunc(x) Truncates X w/o rounding. It is a good idea to learn basic trigonometry and analytical geometry in programming. For example, I know from my studies that Tan(x) would be 1/Arctan(x) or sin(X) / cos(X). This stuff is good to know. The reasons? Graphics. All one needs to draw anything, really, is a good spot placement procedure and knowledge of trigonometry, and analytical geometry. Just know and define a resolution and then start drawing knowing your knowledge. As a test, to help out....How would one draw a circle given a central point and radius? (Gotoxy in the CRT or WinCRT unit will be of use. It will place the cursor at a specified position so you can write something.). If anyone wants a text-based solution on this one if they can't figure it out, e-mail ggrotz@2sprint.net. I will place one in the next part. Other stuff that may prove useful ================================= In your programming experience, you may have wondered if there is a way to get a string to an integer, or an integer to a string to do things with it? Well, there is. VAL and STR. Val is used like this: Val(string, integer, errorinteger); string is the string you want to try and convert to an integer. integer is the integer that holds the successful conversion. errorinteger is <> 0 when there is an error in conversion (always check this before you move on after using a VAL!!! Str is used like this: string := str(integer); string is the string you want to hold the conversion in... integer is the integer that we want to convert... Another good command to know is POS: integer := pos(substr, str); It returns 0, if substr isn't there, but it returns a positive value corresponding to the start of the substring in the string, if it's there. For example, if we want to find the first use of the word AT in a string... int := pos('AT', 'THAT'); int will be 3. Conclusion ========== I know all of this mathematics, and talk of bits is probably confusing. If you don't understand it right now, don't worry about it and go back at leisure and study it. It is the basic extent of mathematics behind the actual operation of a computer and what we all as programmers need to keep in the back of our minds for some things. You should know what a bit is, an order of a byte is, and that there are 8 bits in a byte. These things should help out in some matters. You should know the why parts of these things in some cases...You should especially, though, understand the parts about the commands SHR, SHL, HI, LO, and SWAP, and the concepts of orders based on the storage of a byte, as, though we will not see these in this part, we will undoubtedly see them some- time before this tutorial is over, more than likely, even we may not see them. But you may need to work at bit level with bytes sometime, so this information is present here now for both the novices and the experts that may see this. Also, converting the number bases is a needed thing. Practice Programming Problem #7 =============================== The concept of a record is relatively straight forward. So I will not try to come up with a problem for basic usage of records. But the concept of writing code to do some of the number conversions is not. These functions can be very useful for later use in your programming (save them after you write them!). Write a function for your code library that will perform a base 10 to base X number conversion (X being a variable we can define in the function header to be an integer.) on an integer. Also write a function that will perform a base x to a base 10 conversion. Inbed these two functions in a program that will take a number from the keyboard (you can use whatever prompt you deem to be the least confusing as possible) and a base to convert the number to. Put out a prompt as to the answer of what the new base number of the input is, as well as a restatement of the original number inputted using a reverse conversion of base (Show us that both of the functions work and are correct. If the 2nd computed statement DOES not equal what is put in, there is an error.), not a direct reprint of the input variable. To prove we are not writing out the original input number that we placed in the keyboard, place a 0 in the keyboard input variable after you perform the function to convert it to the user's desired base. Sample Output ------------- Enter a number: 10 What base do you want to convert it to? 5 10 (base 10) is 20 (base 5). To check: 20 (base 5) is 10 (base 10). Notes: 1) You will have to build the converted number base as a string, because any base beyond 10 requires the use of the alphabet for parts of the number. 2) The best way I see to handle the "What do we use for the number in the result?" question is to define a constant string in the function to be something like '0123456789ABC...', and address a proper part of the constant string when we build the converted number for conversion. 3) Remember we count from x^0 units on the right!!!!! 4) You will need to probably make the high end limit to be base 36. 5) Hint: the base x to base 10 function will need to input a string for the input number and output a longint, while the base 10 to base x function will need to input a longint and output a string. 6) A side note for usage. You can link these two to get from any base to any base using base 10 as the intermediary. Next Time ========= We will cover the DOS file function commands out of TP. Hold on to your newsreaders because part 8 (right now) is 531 lines, and I'm not through yet. This will be one of our special topics. If you not familiar with the following concepts in DOS, look up in your DOS manual and try and pick it up before next time: Read-only file, hidden file, system file, volume label, command-line parameter, MD, RD, delete (or erase), the use of * and ? as wildcards. As always, any comments, questions, gripes, etc, send them to ggrotz@2sprint.net. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 8 -- DOS file functions (special topic 1) All parts copyright (c) 1995 by Glenn Grotzinger Here's a solution to part 7.... program part7; { demos 2 functions defined in part 7 to be written. Convert base 10 to anybase, and convert anybase to base 10 } function power(int, ord: integer):longint; { support function required for xbase2dec. Simplistic implementation of taking a power. } var i, endit: longint; begin endit := 1; for i := 1 to ord do endit := endit * int; power := endit; end; function dec2xbase(int: longint; base: integer):string; { converts base 10 to any base < 37 } const numguide: string = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; { the guide string I mentioned as a hint } var i: integer; hold, chkprod1, chkprod2: longint; endstr: string; begin if base > 36 then dec2xbase := '!' { a signal character to say our function failed } else begin endstr := ''; hold := int; while chkprod1 <> 0 do begin chkprod1 := hold div base; { using method described when I } chkprod2 := hold mod base; { demoed doing it manually } endstr := numguide[chkprod2 + 1] + endstr; { actual representation } hold := chkprod1; end; dec2xbase := endstr; end; end; function xbase2dec(int: string; base: integer):longint; { converts any base < 37 to base 10 } const numguide: string = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; var i, powr: integer; endresult: longint; intlength: integer; begin if base > 36 then xbase2dec := -1 {signal fucntion has failed } else begin endresult := 0; i := 1; powr := length(int) - 1; while i <> length(int)+1 do { compute back to base 10 } begin endresult := endresult + (pos(int[i], numguide)-1) * power(base, powr); i := i + 1; powr := powr - 1; end; end; xbase2dec := endresult; end; var numread, numbase: longint; convbase: string; begin write('Enter a number (base 10): '); readln(numread); write('What base do you want to convert to? '); readln(numbase); writeln; convbase := dec2xbase(numread, numbase); if convbase = '!' then writeln('Use a base less than 37') else begin writeln(numread, ' (base 10) is ', convbase, ' (base ', numbase, ').'); numread := 0; { required setting of initial read var to 0 to prove our functions work } writeln('To check: ', convbase, ' (base ', numbase, ') is ', xbase2dec(convbase, numbase), ' (base 10).'); end; end. Now on with the show....This is going to be more of a special topics practice thing. As a note, all of the commands I use here will require you to use the dos or windos unit....also, have your TP Programmers Reference handy to look up several of the basic functions that I will list here. A simple if successful... ========================= How do we check if any of these, or our prior reset/rewrite calls are successful? We toggle the I compiler option before a valid file oper- ator, then we check the variable in IOResult. If IOResult <> 0, then we know there is a problem. For example, how do we check if a file exists on the drive before we read from it? function exists(filename: string):boolean; var ourfile: text; { can be defined to be any type we want } begin assign(ourfile, filename); { let the program know about the file } {$I-}reset(ourfile);{$I+} { toggle I, on a reset. Both MUST be there } if IOResult <> 0 { did something go wrong? With reset, if file did not exist, something would be wrong } exists := false { indicate file doesn't exist } else { file exists } begin close(ourfile); { we need to close the file since if it's there, } exist := true; { we just opened it. Also, indicate it's there. } end; end; This method can be used with any file function like that as long as the system is aware of the file, to indicate success, possibilities, etc., in whatever logical means the command indicates. For reset, one logically can say that if something goes wrong with it, the file isn't there, or the path is invalid. For rewrite, one logically can say if the path isn't right or available, or if there's no disk space, or a variety of reasons. The I compiler directive indicates turning ON or OFF input/output checking. The default for most compilers is {$I+}. As compiler directives go, we must toggle it back, because the area after the change is in effect, and we only want it for the case of the command we want to check. If I/O checking is on, the program would end in a run-time error (you may have noticed this, if you have tried to access non-existent files in your experimentation with reading/writing text files). If I/O checking is off ({$I-}, the result of the command is logged in a global variable named IOResult. Being able to subvert run-time errors is good to be able to do, especially for a DOS file function type program, where you even deal with files. If the user specifies an incorrect file to read, you can return an intel- ligible, user-understandable error message that the filename they gave does not exist on the drive. A list of run-time error possibilities may be found on page 260 of the TP7 programmers reference. Straight Forward Things ======================= I will list several commands for working with files and DOS which are relatively straight-forward to use. Unit used, then command, then a short description. Detailed descriptions follow if needed. I will mainly cover the DOS unit variants. If you use TPW, look up the WinDOS variant equivalents...Be sure to look each of these up in any case.... By all means, play with each of these to understand them. Notes on some of these things will appear later. System: ChDir(Str: string); { changes the current directory to path in Str } DOS/WinDos: DiskFree(Drive: byte):longint; { free bytes on Drive } DOS/WinDos: DiskSize(Drive: byte):longint; { total bytes on Drive } DOS/WinDos: DosVersion: word; { tells us what version of DOS we have } DOS: EnvCount: integer; { how many environment strings? } DOS: EnvStr(index: integer): string; {return a specific environment string} System: erase(file: filetype); {erases an external file} System: FilePos(file: filetype): longint; {returns current position in file} System: FileSize(file: filetype): longint; {returns a file's size} DOS: FSearch(filename: PathStr; dirlist: string):Pathstr {search for a file} DOS: FSplit(... {split a filename into a dir, name, and ext} DOS/WinDos: Getdate(... { gets the current date of the operating system.} System:GetDir(drive; str: string); {gets current directory} DOS: GetEnv(... {gets the specified environment variable} DOS/WinDos: GetFAttr (... { returns file attributes of a file } DOS/WinDos: GetFTime (... { get the date and time of a file } DOS/WinDos: GetTime(... { get current time } System: Halt(code); { quits program immediately with an errorlevel } System: MkDir(str: string); { makes a directory named str } DOS/WinDos: PackTime(... { packs a time/date } System: Rename(file: filetype); { renames an external file } System: RmDir(str: string); { removes a dir named str } System: Seek(file: filetype); { finds an element # in a file } DOS/WinDos: SetDate( ... { sets the date on the machine } DOS/WinDos: SetFTime(... { sets the date and time of a file } DOS/WinDos: SetTime(... { set the time on the machine } DOS/WinDos: UnpackTime(... { unpacks a time/date to datetime record } Notes on some of the commands listed above that aren't that self- explanatory --------------------------------------------------------------------- Diskfree(Drive: byte): longint; DiskSize(Drive: byte): longint; Drive is a numerical byte: 0 is the current drive. 1 is A drive. 2 is B drive. 3 is C drive. and so on and so forth. NOTE: DiskFree and DiskSize are limited from what I understand to < 1 gig? (Please correct me if I am not quoting this right). I know there is a limit there somewhere... DosVersion(version: word); You have to use HI and LO functions here as described in part 7. The high order of this expression would be a minor revision number and the low order of this expression would be a major revision number. For example, if you have DOS 6.20, the high order would be 20, and the low order would be 6. Any expression that uses packed and unpacked date and time. There is two of the functions listed above called packtime and unpacktime. There is a special record already defined in DOS called DateTime (or TDateTime in WinDOS). Both of these are defined like this: DateTime { or TDateTime -- they're the same } = record Year, Month, Day, Hour, Min, Sec: word; end; A packed time is stored as a longint; Good use of an erase/rename, etc, etc... Make a procedure that does the assign, and error checks, then do the command, if the command requires the file to be an assigned file variable. For example: procedure deletefile(filename: string); var afile: text; { It can be anything we will find out } begin assign(afile, filename); {$I-}erase(afile);{$I+} if IOResult <> 0 then writeln('Erasure unsuccessful.'); else writeln(filename, ' has been erased from your drive!'); end; Note: This erase command is recoverable by use of an undelete program. The rewrite is not (all you'll see is a 0 byte file that replaces the file you rewritten). Parameter Passing ================= You may have noted that we can get parameters in from the command- line to some programs we have used, such as PKZIP and PKUNZIP. We can write and design our programs to do such a similar thing. Paramcount: integer; { holds the number of command-line parameters used } Paramstr(num: integer): string { specific command-line parameter } An example: Write a text file out to the screen using full error-checking, and taking the filename from the command-line. Describes a command-line parameter describing a single file. program typefile; var param1, instr: string; thefile: text; begin if paramcount <> 1 then { if there is not one command-line parameter } begin { show some help to the user } halt(1); { quit the program right here } end; param1 := paramstr(1); { corresponds to %1 in batch file processing } { always good to do -- found that addressing the function directly as a string causes problems } assign(thefile, param1); {$I-}reset(thefile);{$I+} if IOResult <> 0 then begin writeln('This file doesn''t exist!'); halt(1); end; readln(thefile, instr); { if the file is there, no need to error check reads, if our logic is correct } while not eof(thefile) do begin writeln(instr); readln(thefile, instr); end; writeln(instr); end. Now, what if we want to determine a file based on a command-line parameter. This isn't exactly complete error checking, as we aren't processing whether the file we specify is a directory or not (EVERYTHING to DOS is a file of some sort, including a directory, we can't exactly TYPE a directory...). That was a demo of picking up command-line parameters, here's how we process a filename parameter so we are addressing the correct thing, EVEN a directory, and series of files (Remember wildcards in DOS? The usage of * and ? in specification of files). We use Fexpand, Fsplit, and GetFattr with doing this, as well as some DOS defined constants which distinguish between different attributes of files. They are additive, BTW. File Attribute Constants ------------------------ ReadOnly $01 Hidden $02 System File $04 Volume ID $08 Directory $10 Archive $20 AnyFile $3F Look at the DOS dir command for an example. Vary what you type in, including directory names, and variants (.., \, and just a dirname). See how it acts. That's what we want to emulate. I will lead in to part 9 by placing something used in planning called pseudocode here to do such a thing. EXPAND FILENAME ON COMMAND LINE TO FULL DIR, NAME, AND EXTENSION. IF THE END OF THE FILENAME DOES NOT HAVE A \ THEN GET FILE ATTRIBUTE. IF NO DOSERROR AND THE FILE IS A DIRECTORY ADD A \ TO THE END OF THE FILENAME. END-IF. SPLIT FILENAME INTO PROPER DIR, NAME AND EXT. IF NO NAME, THEN PLACE A * THERE. IF NO EXTENSION, THEN PLACE A .* THERE. FULL FILENAME = DIR, NAME, AND EXTENSION TOGETHER. You will have to interpret this into viable code for the programming problem at the end of the part. Listing a Sequence of Files =========================== Now, to answer the question, what if we want to actually do something with multiple files (specified with * and ?), instead of just one file (if we want just one file, if we do the i/o checking on reset or rewrite, we will get an error if they use * or ? in the name.). We need to define the usage of a record as a searchrec type for DOS (or TSearchRec for WinDos). searchrec = record fill: array[1..21] of byte; attr: byte; { additive from the file attribute constants } time: longint; { Packed Time } size: longint; { Size of file } name: string[12]; { name of file } end; This record, as well as DateTime is defined in the DOS unit, and we don't NEED to define these in our programs...They are used with the FindFirst and FindNext procedures, which are demoed below, listing all filenames in the current directory. program tutorial19; uses dos; var fileinfo: searchrec; begin findfirst('*.*', $37, fileinfo); while doserror = 0 do { 18 = no more files } begin writeln(fileinfo.name); findnext(fileinfo); end; end. As a note: . and .. are filenames. . is the base of the file system, .. is the next directory up.... Executing a Program =================== You can execute a program from your program by using the exec procedure. You also have to use the $M compiler directive. It's a directive to determine the stack size, as well as the minimum and maximum heap size. You must set this for to get the memory to run the program. The format of the $M compiler directive is this: {$M ,,} If you don't set this, maxheapsize defaults to all of your conventional memory, definitely not good if we want to give the memory to a program to even run (nothing will happen if the program doesn't have the memory to run). For example, we will use {$M $4000,0,0}. Here is the format of the EXEC command. exec(, ); command interpreter path -> Where is COMMAND.COM? We can use getenv to get the environment variable named COMSPEC to get this. I've had people try to rebunk me to say that you could just say exec();. It DOES NOT WORK! (the compiler says something about expecting a ,. You must do it this way!) -- I'm only trying to head off this issue. command -> This must be /C + whatever command you call.... Another command used in combination with this is called swapvectors. It is a parameterless procedure which makes sure our program we call doesn't literally stomp all over anything we may have done with the system. We call it before and after our exec procedure. Here's an example: {$M $4000,0,0} program tutorial20; uses dos; var command: string; begin write('Enter a DOS command: '); readln(command); command := '/C' + command; { we must put the /C there to satisfy COMMAND.COM } swapvectors; exec(getenv('COMSPEC'), command); swapvectors; if doserror <> 0 then writeln('Dos Error ', doserror, ' occurred.') else writeln('Successful shell to DOS.'); end. Use DosExitCode in addition to doserror to determine the results of the exec'd procedure. This function returns a word, which we have to use the HI and LO functions on. LO code results correspond to errorlevel returned out of DOS (remember batch file programming?). A list of HI code results follow: 0 Normal Termination 1 Terminated by Ctrl+C 2 Terminated by Device Error 3 Terminated as a TSR. Conclusion ========== We have covered the major issues in DOS file commands, as well as listing some of the straight-forward commands that are in Pascal to work with DOS. If you looked those up, you would have found that most of those things are pretty straight-forward. Here is a practice programming problem that duplicates the function of the DIR command in DOS, showing us files with specified information. It will be a clone with different functions, though. Let us see.... Practice Programming Problem #8 =============================== Write a program in Pascal and entirely Pascal that will show us a listing of all files in a directory given on the command-line. It should also support command line parameters that change function, such as '/?' and '/P'. Those are the two command-line parameters that we should support (be sure we make it case insensitive). /? or -? will show us help and a listing of who wrote the program and then terminate the program. /P or -p should make it so we pause the screen output on each page. It also should handle any error-checking that it may need to perform as presented in this part. Functions: 1) Show us for each filename on one line, a size, file attributes, date and time. 2) For the whole listing of files, show us: The volume label, Size of the drive we listed, bytes free on the drive we listed, total number of files listed, total number of directories listed, and DOS version that is being used at the current time. 3) All integers or longints > 999 should be delinated by commas, or periods, whatever you use. 4) Write r for read-only, a for archive, s for system, h for hidden. Put the proper ones that apply by each file that it lists. 5) For reading command-line parameters, be sure to make the order non-specific. For example, MYDIR c:\windows /p and MYDIR /p c:\windows should accomplish the same thing, viewing the windows directory with page pausing. Hints: 1) If a file with a volumeID attribute exists in the main directory of a drive, the name of the file is the volumeID of the drive. 2) Work with the number as a string and go from the right end to the left end to place commas as strings for function point 3. 3) You may want to use a designed record type to hold the final data you are going to write as you go obtain it to make things easier. 4) This DIR listing command you are writing should function from the command-line with regards to filespec exactly like the DOS dir command. Check this one by playing around with the DOS dir command. 5) Hint for the #5 function. Hold the parameter strings in an array and write some code to differentiate between command-line params and the actual path you want to view. 6) For the string with the attributes, build your string. You can directly address and build a string with certain portions as long as you start with a valid length of something. I will explain this in the next part. Sample output for yourdir command. ---------------------------------- C:\>mydir /? MYDIR (c) 1996 by Glenn Grotzinger. { put your name here, of course } Help: MYDIR / filespec is the filename/dirname(s) we want to list. parameters are ? or P ? --> this help. P --> pause on each screen page. C:\>mydir MYDIR (c) 1996 by Glenn Grotzinger. File listing for: C:\*.* . [DIR] .. [DIR] CONFIG.SYS 122 12-12-95 03:45pm rash DESCRIPT.ION 182 12-28-95 12:14am -a-- ARCHIVE.ZIP 14,432,322 03-24-93 09:15pm r--h Volume label: Total files: 3 Total dirs: 2 DOS Version: 6.22 14,432,626 bytes. 214,232,123 bytes used out of 543,212,123 total bytes. You get the general idea....BTW, for me, this one is 310 lines.. The next practice program given will be in part 10. This one and the one in part 10 will be the longest and probably most complex ones in the whole set of pascal tutorials. It would be good for ANYBODY who wants the practice to do these. Next time ========= We will do the first part of the tutorial covering applications development. In doing that, we will develop this DIR equivalent that I posed above. Do practice, though, and do this one. Any comments, questions, etc, may go to ggrotz@2sprint.net. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 9 -- Applications Development All parts copyright (c) 1995-6 by Glenn Grotzinger Hello. This time, I'm not going to go present the solution to part 8 immediately, because I want to sue the part 8 problem to demonstrate applications development. The basic reason I'm doing this is because programs should be designed and then programmed, normally, and not the other way around. Normally in design, we have different tools available for us to use. The ones I'm aware of are pseudocode and flowcharting. I can't cover flowcharting through text, but I can cover pseudocode. What is pseudocode? =================== Pseudocode is basically English-like descriptions of what is going on. I gave you an example of it in part 8 (with capital letters for emphasis, as I will continue to do so) as it pertains to a good line parsing function. We will go through and design a solution to part 8 in this part. How to start out? ================= It is good to start with the global way of things. Let's start with a few facts we know from our knowledge from part 8. 1) We're wanting listings of files. 2) We're wanting stats on the operating system as well as the drive we access. Knowing our purposes in these two statements, we know we need usage of the DOS or WinDOS unit. Globally, by looking at the way we know the output needs to be, we can figure out that globally, the program is going to have to do the following: 1) Write the ID of the program. {mydir by...} 2) Parse the command-line to determine what needs to be done. 3) Write resultant headers based on results in 2. 4) If successful, while there are valid files, list the files as dictated in 2. 5) Show the global drive and operating system stats. Starting to break down the global stuff and writing pseudocode ============================================================== We know this is the basic idea of things that we will have to do in order to complete this program. We need to move from this down to the specific details. Let's start with 1. We need to write the ID of the program (Global 1). -------------------------------------------------- Also, we need to look at factors for initialization code. We need some filespec variable clear...Also we know we need to count files and dirs, as well as set a pause flag to our default (false). PROCEDURE WRITEID; WRITE PROGRAM NAME AND AUTHOR NAME, COPYRIGHT INFO... SET PAGE COUNT TO # OF LINES USED (page pausing!) SET PAUSE TO FALSE, AND SET #DIRS AND #FILES TO 0. Parse the command-line (Global 2). ---------------------------------- I will start globally, and move down to specifics here. Keep in mind that the functions said that path/filename and parameter should be interchangeable (Function 5 as specified in the problem). Here, what seems logical is to use the most definable command-line parameter and eliminate things down to the least specific. Here, the command params ? and P are the most specific while the path is the least specific. IF PARAMETERS > 2 THEN SHOW SOME HELP AND QUIT. PULL ALL PARAMETERS INTO AN ARRAY (MAX 2). FOR EACH PARAMETER GIVEN TO PROGRAM IN ARRAY IF THE PARAMETER IS 2 CHARACTERS LONG AND THE FIRST CHARACTER IS - OR / IF SECOND CHARACTER OF PARAMETER IS ? SHOW SOME HELP AND QUIT. IF SECOND CHARACTER IS P, SET A PAUSE FLAG TO TRUE. ELSE SHOW SOME HELP AND QUIT. END-IF. ELSE SET FILESPEC TO THE COMMAND-LINE PARAMETER. END-IF. PARSE THE FILESPEC. Now, this is a general description of what's going on in part 2 of our global listing we made. Now for specifics here. The only ones I see are showing the help and quitting, and parsing the filespec. Show help and quit. WRITE THE HELP. SET PAGE COUNT TO NUMBER OF LINES USED (Page count for pause!) HALT THE PROGRAM. Parsing the filespec. I gave you this pseudocode in part 8. Write resultant headers based on 2. (Global 3) ---------------------------------------------- WRITE THE HEADER WITH PARSED PATH INTERPRETATION. List files as dictated by filespec in 2. (Global 4) --------------------------------------------------- We know, basically, in the DIR implementation we want to list all files except ones with the volume lables. We know the file constants are additive but to ease things in typing, we can go ahead and add them up. All files except volumeID in the constants is equal to $37. Also, we will keep in mind the following points that were brought up in the directions. 1) (Function 1) Show us for each filename on one line a size, file attributes, date and time. 2) (Function 3) All integers or longints > 999 should be delineated by commas, or periods, whichever you use. 3) (Function 4) Write r for read-only, a for archive, s for system, and h for hidden. 4) If we need to pause, be sure to implement it! 5) Be sure to indicate if there are no files. START FILE LISTING. WHILE WE STILL HAVE FILES IF A FILENAME IS A DIRECTORY ($10) THEN WRITE DIRECTORY NAME WIHT [DIR] DESIGNATION. INCREMENT # OF DIRS BY 1. ELSE WRITE FILENAME, FILESIZE, DATE, TIME AND ATTRIBUTES. INCREMENT # OF FILES BY 1. INCREMENT SIZE OF FILES IN DIR BY SIZE OF THIS FILE. END-IF. INCREMENT PAGELENGTH BY 1. IF PAGELENGTH > 23 AND PAUSE IS TRUE WRITE PAUSE INDICATOR, READ FOR KEY, AND SET LINE COUNTER BACK TO 0. END-IF. IF THERE IS A DOS ERROR OR THERE ARE NO DIRS AND NO FILES WRITE THAT THERE ARE NO FILES THERE. There's our global listing for part 4. Now we need to consider the individual actions, basically, obtaining numbers with commas, the date, the time, and the file attributes. Numbers with commas (Code explained later) MAKE A STRING OUT OF THE NUMBER. FIND LENGTH OF NUMBER. WHILE THERE ARE MORE THAN 3 DIGITS TO CONSIDER COUNT OFF 3 DIGITS FROM THE RIGHT TO THE LEFT. PLACE A COMMA, AND SUBTRACT 3 DIGITS FROM LENGTH TOTAL. END-WHILE. COUNT OFF REST OF DIGITS. The date and time. Basically, the issue here is pulling the information for the date, except the year, where we must consider the last 2 digits instead of 4 digits. In getting the last 2 digits, we also need to keep in mind that the year 2000 will be coming in 4 years... With the time, it will be in military time, so we may recycle code, say from part 4 (It is always good to save code and copy so you do not have to invent the wheel and then turn around and invent it again...:)). Note the repeated appearances of the strings "Make XXX a string." and "If XXX < 10, pad number with zero." To pad a number, I mean, if I have 9, then padding it with a zero would make it 09. In good programming planning, if repeated code happens, isolate that code as a function or procedure to save lines of code. AT the end, you will see that I have done that. UNPACK THE DATE AND TIME FROM THE SYSTEM. MAKE MONTH A STRING. IF MONTH < 10, PAD NUMBER WITH 0. MAKE DAY A STRING. IF DAY < 10, PAD NUMBER WITH 0. IF YEAR > OR = 2000 2YEAR IS YEAR - 2000 ELSE 2YEAR IS YEAR - 1900 MAKE 2YEAR A STRING. IF YEAR < 10, PAD NUMBER WITH 0. FINAL-FILE-DATE IS MONTH-DAY-2YEAR. (or DAY-MONTH-2YEAR, whichever you prefer) IF HOUR > OR = 12 HOUR IS HOUR - 12 MERIDIAN IS PM. ELSE MERIDIAN IS AM. END-IF. IF HOUR = 0 THEN HOUR = 12. MAKE HOUR A STRING. IF HOUR < 10, PAD STRING WITH 0. MAKE MIN A STRING. IF MIN < 10, PAD STRING WITH 0. TIME IS HOUR:MINmeridian. Get file attributes. I gave hint #6 for this part to build your string and said I'd explain later. A string in pascal (a pascal string), actually is stored using length + 1 bytes. The first byte is a number representing the total length of the string. The rest of it is the string. Since we use a background of -'s, that's all we need to start the string from...Then use proper position to assign things...and again using the file attribute constants, remembering that they are additive. Starting from the largest to smallest...Say, we want to reassign the 3rd character of STR, we just say str[3] := 's' ro something like that. SET STRING TO ----. IF FILEATTR >= $20 THEN FILE HAS ARCHIVE BIT. FILEATTR = FILEATTR - $20. END-IF. IF FILEATTR >= $04 THEN FILE HAS SYSTEM BIT. FILEATTR = FILEATTR - $04. END-IF. IF FILEATTR >= $02 THEN FILE HAS HIDDEN BIT. FILEATTR = FILEATTR - $02. END-IF. IF FILEATTR >= $01 THEN FILE HAS READ-ONLY BIT. FILEATTR = FILEATTR - $01. END-IF. Show the global stats and operating system info (Global 5) ---------------------------------------------------------- Basically, this is a write operation. But we need to know how to get the information... Volume label. Using the hint in the problem. SEARCH FOR FILE WITH VOLUMEID ATTRIBUTE IN ROOT DIR OF DRIVE WE ARE ACCESSING. IF THE FILE EXISTS, FILENAME IS THE VOLUMEID ELSE LEAVE IT BLANK. Total files and total dirs and total size of the files listed are simply writing variables (be sure to run these numbers through the comma delineator function). Total size used on drive. The drive is designated as a number, and the nice thing about our pase_filespec thing is that the first character always ends up being the drive letter of the drive we want to work with. So, to go from drive letter to number, we can always set a constant guide string (sort of like my suggestion in part 7 to do this for bases > 11 as to ease things) defined as: ABCDEFGHIJKLMNOPQRSTUVWXYZ. I know this is probably overkill, but it will cover things OK so we don't have to write a large case statement. Run this one through the delineator. DRIVE NUMBER IS LETTER POSITION IN CONSTANT STRING. FREE-ON-DISK FOR DRIVE NUMBER. Total size on drive. Similar to total siz eused. Uses constant guide string...also ran through delineator. DRIVE NUMBER IS LETTER POSITION IN CONSTANT STRING. TOTAL-ON-DISK FOR DRIVE NUMBER. DOS version. Getting this is exactly like described in part 8. Nothing out of the ordinary. This finishes up my pseudocode description for the part 8 problem. Now, for my best solution that I could come up, with the suggestion (probably could be faster if I didn't do it, but I went ahead and used the save format for the record as some help, and allowed for 999,999,999 as a possible total file size in the layout -- too big to really worry, though it probably slows it up a little...) Keep in mind too, that this pseudocode was revised, after I found out that some of my original statements turned out to be wrong in my logic planning. It's OK to create incorrect pseudocode. It is only a planning tool, and does not have to be correct. The only thing anyone will really care about being correct is your final source code -- correct meaning that it works properly. The source code for my implementation of MYDIR. program part8; uses dos; { a dir command. Supports command params /? and /P. shows filename, filesizes with commas, time, date, attributes. For total, shows drivesize in bytes, bytes used on drive. Volume label of drive, total number of files, total numbers of directories. } type writerec = record { format to write out the dirinfo } filename: string[12]; filesize: string[11]; { largest size => 999,999,999 bytes } filedate: string[8]; filetime: string[7]; fileattr: string[4]; end; var dirinfo: searchrec; writeinfo: writerec; params: array[1..2] of string; i: integer; pause: boolean; filespec: string; dirs, files, totalsize: longint; pagelen: integer; function parse_filespec(filename: string):string; const all_files = $37; { all file constants in base 16 added together but VolumeID } var dir: dirstr; { required types for some of the commands as } name: namestr; { defined in the DOS unit. } ext: extstr; attr: word; { attribute must be a word. } f: text; { required for a command we had to assign file for } begin filename := fexpand(filename); { expand filename } if filename[length(filename)] <> '\' then { if end not \ } begin assign(f, filename); getfattr(f, attr); { get the file attribute } if (doserror = 0) and (attr = $10) then filename := filename + '\'; { if it's a directory put \ } end; fsplit(filename, dir, name, ext); { split filename up. } if name = '' then name := '*'; { if it's still a directory, } if ext = '' then ext := '.*'; { specify ALL FILES } parse_filespec := dir + name + ext; { re-form filename } end; function zero(innum: integer):string; var tstr: string[2]; begin str(innum, tstr); if innum < 10 then tstr := '0' + tstr; zero := tstr; end; procedure showid; begin writeln('MYDIR (c) 1996 by Glenn Grotzinger.'); writeln; pagelen := pagelen + 2; end; procedure writeheader(filespec: string); begin writeln('File listing for: ', filespec); writeln; pagelen := pagelen + 2; end; function number(n: longint):string; var i, j: integer; s, r: string; m: integer; begin str(n, s); { get the longint to a workable string } r := ''; { r is a holding string for our delineated number } i := length(s); { set an integer to the length of our number. We will be going from the right end and moving to the left in placing our delin- eations, and building r from the right to the left. } if i > 3 then begin while i > 3 do { while we don't have 3 numbers left } begin for j := i downto i-2 do { count off 3 digits and move to r} r := s[j] + r; r := ',' + r; { write a comma or period to r } i := i - 3; { subtract 3 digits } end; for j := i downto 1 do { we only have 3 digits left, or less now. Just count off the digits } r := s[j] + r; number := r; { feed r to the function } end else number := s; end; procedure getdatetime(dirinfo: searchrec; var writeinfo: writerec); { type uses datetime record } var dt: datetime; a,b,c: string; { temp strings for use } tmpyr: integer; { we need to define this one to do the year } begin unpacktime(dirinfo.time, dt); { unpack date and time } { start with date } { month } a := zero(dt.month); { day -- same as month } b := zero(dt.day); { year -- reported as 4 digits. We need to get it down to 2. } { keep in mind the valid range that TP will report year. } if dt.year > 1999 then { if year 2000 or above } tmpyr := dt.year - 2000 else { else would be before 2000 } tmpyr := dt.year - 1900; { now that we have our last 2 digit year, perform similar to day & month } c := zero(tmpyr); writeinfo.filedate := a + '-' + b + '-' + c; { set final date } { now start with the time -- it's reported as military time -- we need to deal with it as such } { military time determination } if dt.hour >= 12 then begin dt.hour := dt.hour - 12; c := 'pm'; end else c := 'am'; if dt.hour = 0 then dt.hour := 12; { hours -- deal with as we did days now } a := zero(dt.hour); { minutes } b := zero(dt.min); writeinfo.filetime := a + ':' + b + c; end; function getfileattr(dirinfo: searchrec):string; var left: word; str: string; begin str := '----'; left := dirinfo.attr; if left >= $20 then { archive file } begin str[2] := 'a'; left := left - $20; end; if left >= $04 then { system file } begin str[3] := 's'; left := left - $04; end; if left >= $02 then { hidden file } begin str[4] := 'h'; left := left - $02; end; if left >= $01 then { read-only file } begin str[1] := 'r'; left := left - $01; end; getfileattr := str; end; procedure writefinfo(dirinfo: searchrec;writeinfo: writerec); var i: integer; begin with writeinfo do begin filename := dirinfo.name; filesize := number(dirinfo.size); getdatetime(dirinfo, writeinfo); fileattr := getfileattr(dirinfo); write(filename); for i := length(filename) to 12 do write(' '); writeln(filesize:12, filedate:12, filetime:12, fileattr:12); end; end; function getvolumeID(drive: char):string; { volume label exists as a file in the root directory with a special attribute called VolumeID and the name of the file is the volume label } var fstr: string; dinfo: searchrec; begin fstr := drive + ':\*.*'; findfirst(fstr, VolumeID, dinfo); if doserror = 0 then { Volume label exists so...} getvolumeID := dinfo.name else getvolumeID := ''; { leave it blank if no volume label } end; procedure showhelp; begin writeln('Help:'); writeln(' MYDIR /'); writeln(' filespec is the filename/dirname(s) we want to list.'); writeln(' parameters are ? or P (case insensitive)'); writeln(' ? --> This help.'); writeln(' P --> pause on screen page.'); halt(1); end; function bytesfree(drive: char):longint; const guide = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; var dno: integer; begin dno := pos(upcase(drive), guide); bytesfree := diskfree(dno); end; function bytesthere(drive: char):longint; { in TP, this won't work if you have a partition > 1 gigabyte } const guide = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; var dno: integer; begin dno := pos(upcase(drive), guide); bytesthere := disksize(dno); end; begin { initialization code } showid; { should be the first thing we do } pause := false; filespec := ''; totalsize := 0; dirs := 0; files := 0; { check # of parameters } if paramcount > 2 then showhelp; { pull in parameters } for i := 1 to paramcount do params[i] := paramstr(i); { check 'em for ? and P parameters, and filespec } for i := 1 to paramcount do if (length(params[i]) = 2) and (params[i][1] in ['-','/']) then case upcase(params[i][2]) of '?': showhelp; 'P': pause := true; else showhelp; end else filespec := params[i]; filespec := parse_filespec(filespec); { go into start } writeheader(filespec); findfirst(filespec, $37, dirinfo); while doserror = 0 do begin if dirinfo.attr = $10 then begin write(dirinfo.name); for i := length(dirinfo.name) to 12 do write(' '); writeln('[DIR]':49); inc(dirs); end else begin writefinfo(dirinfo, writeinfo); inc(files); totalsize := totalsize + dirinfo.size; end; inc(pagelen); if (pagelen > 23) and (pause) then begin write('--Pause--'); readln; pagelen := 0; end; findnext(dirinfo); end; if (doserror in [1..17]) or ((dirs = 0) and (files = 0)) then writeln('No files found.'); writeln; writeln('Volume label: ', getvolumeid(filespec[1]), 'Total Files: ':20, number(files), 'Total Dirs: ':20, number(dirs)); writeln('DOS Version: ', lo(dosversion), '.', hi(dosversion)); writeln; writeln(number(totalsize), ' bytes.'); writeln(number(bytesfree(filespec[1])), ' bytes free out of ', number(bytesthere(filespec[1])), ' total bytes.'); end. Next Time ========= We will cover reading and writing from binary files in part 10, as well as use of units, overlays, and include files in programming. Part 10 will be a long program, which will stress the ideas represented here mainly, but will also involve material in part 10. It will be a tedious one, but does not require a lot of programming knowledge to complete. This problem will also be set up for a programming contest. If there are any comments, please write ggrotz@2sprint.net. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 10 -- binary files; units, overlays, and include files. All parts copyright 1995-6 (c) by Glenn Grotzinger. There was no prior problem, so lets get started... Typed binary files ================== We know that files can be of type text. We can also make them type "file of ". We can read and write binary data types to disk. Here's an example. Keep in mind that with typed binary files, you can only read and write the type of file you define it to be. For the example below, we can only deal with integers with this file. The type we may use may be anything that we have covered up to this point. We also will see that reading, accessing and writing of typed binary files will be no different than accessing text files, except we can not make use of readln and writeln (as those are for text files only). program integers2disk; { writing integers 1 thru 10 to a disk data file, then reading 'em back } var datafile: file of integer; i: integer; begin assign(datafile, 'INTEGERS.DAT'); rewrite(datafile); for i := 1 to 10 do write(datafile, i); close(datafile); { done with write } reset(datafile); { now lets start reading } read(datafile, i); while not eof(datafile) do { we can use the same concept } begin writeln(i); read(datafile, i); end; writeln(i); close(datafile); end. You will notice the numbers 1 through 10 come up. Look for the file named INTEGERS.DAT, and then load it up in a text editor. You will notice that the file is essentially garbage to the human eye. That, as you see, is how the computer sees integers. In part 11, I will explain storage methods of many many different variables, and introduce a few new types of things we can define. We can use records, integers, characters, strings, whatever...with a typed file as long as we comply with the specific type we assign a file to be in the var line. Untyped Binary Files ==================== We can also open binary files as an untyped, unscratched (essentially) file. There we simply use the declaration "file". (I think this is ver7 dependent, am I right?) Anyway, in addition to this, we have to learn a few new commands in order to use untyped files. BLOCKREAD(filevar, varlocation, size of varlocation, totalread); filevar is the untyped file variable. varlocation is the location of where we read the variable into. size of varlocation is how big varlocation is. totalread is how much of varlocation that was readable. (optional) BLOCKWRITE(filevar, varlocation, totalread, totalwritten); filevar is the untyped file variable. varlocation is the location of where we read the variable into. totalread is how much of varlocation was readable. (optional) totalwritten is how much of varlocation that was written. (optional) SizeOf(varlocation) Function that gives total size of a variable in bytes. Maximum readable by BlockRead: 64KB. Reset and Rewrite have a record size parameter if we deal with an untyped file. Probably, the best thing to make things clearer is to give an example. This program does the same thing as the last one does, but only with an untyped file. See the differences in processing... program int2untypedfile; var datafile: file; i: integer; numread: integer; begin clrscr; assign(datafile, 'INTEGERS.DAT'); rewrite(datafile, 1); for i := 1 to 10 do blockwrite(datafile, i, sizeof(i)); close(datafile); reset(datafile, 1); blockread(datafile, i, sizeof(i), numread); while numread <> 0 do begin writeln(i); blockread(datafile, i, sizeof(i), numread); end; close(datafile); end. This program performs essentially the same function as the first example program, but we are using an untyped file. Blockread and blockwrite are used in very limited manners here. It's *VERY GOOD* for you to experiment with their use!!!!!!! As far as the EOF goes on a comparison, blockread returns how many records it actually read. We use that as an equivalent. The 2 missing DOS file functions ================================ We now have the tools to perform the 2 missing DOS file functions that you probably recognized were gone from part 8, copying files, and moving files. Copying files essentially, is repeated blockreads and blockwrites until all the input file is read and all the output file is written. We can do it with either typed or untyped files. An untyped file example may be found on page 14 of the Turbo Pascal 7.0 Programmer's Reference. For those who do not have this reference...Snippet of my own...untested... while (numread <> 0) or (bytesw = bytesr) begin blockread(infile, rarray, sizeof(rarray), bytesr); blockwrite(outfile, rarray, bytesr, bytesw); end; Moving files is a copy of an input file to a new location, followed by erasure of the input file. Units ===== A unit is what you see probably on your drive in the TP/units directory. Compiled units are TPU files. They are accessed via USES clauses at the start. CRT, DOS, and WinDos are some of the provided units we have already encountered. Nothing is stopping us from writing our own, though. The actual coding of procedures/functions that we place into units is no different. The format of the unit, though, is something we need to think about. An example is the best thing for that. This is a simple implementation of a unit, with examples to give you some idea of a skeleton to place procedures and functions into. unit myunit; interface { all global const, type, and var variables go here as well as any code we may want to run as initialization for starting the unit. } { procedures and function headers are listed here } procedure writehello(str: string); implementation { actual procedural code goes here, as it would in a regular program } procedure writehello(str: string); { must be same as above } begin writeln(str); end; end. The unit up above is compilable to disk/memory, but unrunable. Essentially, what it is is a library of procedures/functions that we may use in other programs. Let's get an example out on how to use one of our own units. program tmyunit; uses myunit; { must match ID at beginning } var str: string; begin str := 'Hello! Courtesy of myunit!'; writehello(str); end. Though this program/unit combination is ludicrous, it does illustrate exactly how to incorporate your own unit with MANY functions into your programming, if your project gets too big, or for portability's sake on some of your frequently used procedures. Overlays ======== This will describe how to use TP's overlay facility. It must be used with units. Typically, my thoughts are that if you get a large enough project to dictate the use of overlays (we can use 'em on anysize projects, but the memory taken up by the overlay manager far uses more memory on smaller projects to make it an advantage to habitually do this). We will use the overlay facility with the unit/program set above for example purposes. ONLY CODE IN UNITS HAVE AN OPPORTUNITY TO BE OVERLAID! System, CRT, Graph, and Overlay (if I remember right) are non-overlayable. {$O+} is a compiler directive for UNITS only which designate a unit which is OK to overlay. {$O-} is the default, which says it's not OK to overlay a unit. To get to the overlay manager, we must use the overlay unit. After the overlay unit, we need to use the {$O } compiler directive to specify which units that we want to compile as an overlay. WARNING: It is good to check your conversion to overlays in a program with a copy of your source code. If you alter it with overlays in mind and it doesn't work (it's known to happen -- a procedure works ONLY when it's not overlaid...), you won't have to go through the work to alter it back if it doesn't work right... NOTE: You must compile to disk, then run when you work with overlays. Results come back in the OvrResult variable. Here's a list... 0 Success -1 Overlay manager error. -2 Overlay file not found. -3 Not enough memory for overlay buffer. -4 Overlay I/O error. -5 No EMS driver installed. -6 Not enough EMS memory. As for examples, let's look at the unit set up to overlay. As we can see, the only real difference (which is a good policy to make), is that there is the {$O+} compiler directive there now... {$O+} unit myunit; interface { all global const, type, and var variables go here as well as any code we may want to run as initialization for starting the unit. } { procedures and function headers are listed here } procedure writehello(str: string); implementation { actual procedural code goes here, as it would in a regular program } procedure writehello(str: string); { must be same as above } begin writeln(str); end; end. Now lets look into the program itself. It's error-reporting from the overlay manager isn't great. It stops the program if the overlay won't load, but doesn't do a thing, really, with the ems section. program tmyunit; uses myunit, overlay; {$O MYUNIT.TPU} { include myunit in the overlay } var str: string; begin ovrinit('TMYUNIT.OVR'); { final overlay file name/init for program. } if OvrResult <> 0 then begin writeln('There is a problem'); halt(1); end else write('Overlay installed '); ovrinitems; {init overlay for EMS. Usable after ovrinit} if OvrResult <> 0 then writeln('There was a problem putting the overlay in EMS') else writeln('in EMS memory.'); str := 'Hello! Courtesy of myunit!'; writeln; writehello(str); end. EXE Overlays ============ Here's how to set up EXE overlays. The DOS copy command features the B switch. For example, to take the programs source file above and attach the overlay to the end of the EXE (be sure you run any exe packers/encryptors before you do this!), use the following: COPY /B TMYUNIT.EXE+TMYUNIT.OVR Then the change that needs to be made in the source for the program is to change the overinit line to read TMYUNIT.EXE instead of TMYUNIT.OVR. You should be able to handle doing this and understanding what is going on. Include Files ============= Use the {$I } compiler directive at the position the include file is to be placed. An include file is code that is in another file, which may be considered as "part of the program" at the position the {$I ..} compiler directive is at. Copy function ============= You can use the copy function to get a portion of a string into another part of a string. For example... str := copy('TurboPascal', 5, 3); writeln(str); { writes oPa } Programming Practice for Part #10 ================================= We have opened ourselves a business selling computer equipment in 1993. Since we have occupied ourselves with working on computers, and not on bookkeeping (we wanted to save the funds instead of hiring someone), and rather not use the cash registers, we have done everything on paper over the last two years. It's the beginning of 1996, and any accurate records of sales progression, as well as records of our customers has become almost impossible, since our records are represented by a closet-full of paper. So, we finally have decided to get things into computer. To do the typing, we have temporarily hired interns from a nearby business college. Unfortunately, with our limited funds, we could not draw in people who had sufficient typing skill and accuracy, but we took what we could get. We now have things typed in as text files with 80 columns a line. Unfortunately, the interns' attention to detail has been as bad as their typing skill, and nothing makes sense in their work. Our purposes is to save our money in hiring these interns and locate the badly entered records, while writing the good records to a solid binary data file by the name of COMPHVN.DAT. For the bad records, on EACH AND EVERY error we encounter, we should write a text message with the first 20 characters of the problem line and a description of what is wrong with the data set for that particular error so we may go back through and make the interns redo what they did wrong to a text file named ERRORS.LOG. The data format for the output file COMPHVN.DAT is as follows. For interest of efficiency, we shall write this program using COMPHVN.DAT as an untyped file. As the person posing this problem, I realize that some of the data types in this record will not be recognizable at this point, but with the variable description, you will know how to handle them, and in part 11, you will see what they are exactly. In creating a binary file, we must always be concerned with using the least amount of space as effectively as possible. Uses of the variables will be explained later. For interest of typing efficiency on your parts, I am asking that you cut and paste this record description out of this description and save it as a text file named COMPHVN.INC, which may be used as an include file in our compilation. comphvndata = record datacode: string[7]; acct_classification: char; phone_area: integer; {area+prefix+exchange = phone number} phone_prefix: integer; phone_exchange: integer; work_area: integer; work_prefix: integer; work_exchange: integer; other_area: integer; other_prefix: integer; other_exchange: integer; cnct1_lname: string[16]; cnct1_fname: string[11]; cnct1_minit: char; cnct1_pobox: integer; cnct1_sname: string[8]; cnct1_stype: string[4]; cnct1_apt: integer; cnct1_city: string[10]; cnct1_state: string[2]; cnct1_zip: longint; cnct1_birthm: byte; cnct1_birthd: byte; cnct1_birthy: integer; accept_check: boolean; accept_credt: boolean; balnce_credt: real; total_sold: real; cnct1_emp_code: string[4]; total_sales: integer; emp_name: string[10]; emp_stnum: integer; emp_sttype: string[4]; emp_city: string[10]; emp_state: string[2]; emp_zip: longint; emp_area: integer; emp_prefix: integer; emp_exchange: integer; emp_yrs: byte; compu: boolean; compu_type: string[9]; compu_mon: char; compu_cdr: boolean; compu_cdt: char; compu_mem: byte; minor: boolean; end; The format for our INPUT file, which will be named INDATA.TXT, will be as follows (80 characters). Since we had 15 interns doing the typing at once we also had them merge their work. They were careless, and may have not accomplished it properly. There will be three lines for each customer that we have encountered. Line 1 Line 2 -------------------------------------------------------------------- datacode columns 1-7 datacode columns 1-7 acct_classification column 8 accept_check column 8 sequence number column 9 sequence number column 9 phone_area columns 10-12 cnct1_stype columns 10-13 phone_prefix columns 13-15 cnct1_apt columns 14-17 phone_exchange columns 16-19 cnct1_city columns 18-27 work_area columns 20-22 cnct1_state columns 28-29 work_prefix columns 23-25 cnct1_zip columns 30-38 work_exchange columns 26-29 cnct1_birthm columns 39-40 other_area columns 30-32 cnct1_birthd columns 41-42 other_prefix columns 33-35 cnct1_birthy columns 43-46 other_exchange columns 36-39 balnce_credt columns 47-55 cnct1_lname columns 40-55 total_sold columns 56-63 cnct1_fname columns 56-66 cnct1_emp_code columns 64-67 cnct1_minit column 67 total_sales columns 68-70 cnct1_pobox columns 68-72 emp_name columbs 71-80 cnct1_sname columns 73-80 Line 3 -------------------------------------------------------------------- datacode columns 1-7 accept_credt column 8 sequence number column 9 emp_stnum column 10-13 emp_sttype column 14-17 emp_city column 18-27 emp_state column 28-29 emp_zip column 30-38 emp_area column 39-41 emp_prefix column 42-44 emp_exchange column 45-48 emp_yrs column 49-50 compu column 51 compu_type column 52-60 compu_mon column 61 compu-cdr column 62 compu_cdt column 63 compu_mem column 64-65 minor column 66 spaces column 67-80 Now, a description as to what is defined as a correct set that we should write to COMPHVN.DAT. 1) Each 3 lines that are read are considered for errors. Check the sequence numbers. The first line's sequence number should be 1, for example. A successful read of 3 lines should say 1, 2 and 3 in that order. For example, in our error reporting, if you have a read of 1,2,2 , you should not write the group to the binary file, and report a duplicate line #2 and a missing line #3. There will not ever be a circumstance where these sequence numbers will all be the same...The cases covered in this paragraph would be the only cases that would ever forstall processing of error-checks listed in points 2-14. 2) Datacode on lines 1, 2 and 3 should MATCH exactly and be checked for the following: It has the format, for example, with my name of GROTZ*G, and should be verified using the cnct1_names... 3) phone_area, phone_prefix, phone_exchange, work_area, work_prefix, work_ exchange, other_area, other_prefix, other_exchange, pobox, emp_zip, emp_ area, emp_prefix, emp_exchange, emp_yrs, cnct1_zip, cnct1_birthm, cnct1_ birthd, cnct1_birthy, balance_credt, total_sold, total_sales, compu_mem all should be checked to verify that they are numeric in origin. 4) phone_prefix, work_prefix, other_prefix, emp_prefix all should not start with a 1 or a 0. 5) cnct1_birthy should be in this century 1900-1999. 6) acct_classification should be B,C,G,P, or O. 7) accept_check, accept_credt, compu, compu_cdr, and minor should be Y or N. 8) emp_yrs (employed how many years?) should be checked with cnct1_birthy for sanity (a person who was born in 1980 cant have worked 20 years). 9) If compu is N, then compu_type, compu_mon, compu_cdr, compu_cdt, and compu_mem should be either blank or 0 depending upon the type of field. 10) cnct1_emp_code should be GOVT, RET, STUD, or BUS. If this field is RET, then emp_* should either be blank or 0 depending on the type of field. 11) compu_mon should be S, V, E, C, H, or I. 12) compu_cdt should be 1, 2, 4, 6, or 8. 13) emp_sttype and cnct1_stype should be BLVD, LANE, ST, AVE, CT, LOOP, DR, CIRC, or RR. 14) minor should be Y if person listed in cnct1_?name is < 21 years old and N otherwise. Check to be sure that this field is correct in being Y or N. Format of ERRORS.LOG (also solution to the INDATA.TXT posted below) -------------------- Error Report -- INDATA.TXT -------------------------- First 20 characters of line Problem --------------------------- -------------------------- GROA2*GN334 ST WAR Datacode does not agree with name. GROT2*GP181612932918 Work-exchange is not numeric. GROT2*GP181612932918 phone-prefix started with a 0 or 1. GROT2*GT2ST 314 SED accept-check is invalid. GROT3*GP181642932918 Duplicate line #1 GROT3*GN234 ST WAR Missing line #3 GROT4*GI181642932918 Datacode does not agree with name. GROT4*GY2ST 314 SED Datacode does not agree with name. GROT4*GN334 ST WAR Datacode does not agree with name. GROT4*GY2ST 314 SED cnct1-birthy is not in this century. GROT4*GI181642932918 acct-classification is invalid. GROT7*GN334 ST WAR emp-zip is not numeric. GROT7*GN334 ST WAR compu-cdr is invalid. GROT7*GN334 ST WAR The emp-yrs doesn't make sense. GROT7*GN334 ST WAR There were fields present when compu was N. GROT7*GN334 ST WAR compu-mon is invalid. GROT7*GN334 ST WAR compu-cdt is invalid. GROT8*GN334 ST WAR empcodes are present when RET is true. GROT8*GN334 ST WAR compu-mon is invalid. GROT0*GN334 STR WAR compu-cdt is invalid. GROT0*GN334 STR WAR emp-sttype is invalid. Remember to be as general as possible on your error messages. Use the example listed above as a guide. Your program can not predict everything. Also, in the interest of finding out your programming skill, we ask that you code this program using the pascal overlay system with EMS load capability, with all error codes and status statements active and visible to the user, for at least one procedure or function. Also note, that many of the separate integer fields are put together in the input file, so we can not just plain read the input file. Here is a copy of the current input file, INDATA.TXT (keep in mind it's 80 characters per line, and the character positions MATTER) ---------------------------------------------------- GROT1*GP1816429329181674700008163475753GROT1INGER GLENN K232 34th GROT1*GY2ST 314 SEDALIA MO64093 062519742.34 3245.23 STUD32 CMSU GROT1*GN334 ST WARRENSBURMO65337 81654341114 YHOMEBUILTVY18 N GROT2*GP18161293291816747000A8163475753GROT2INGER GLENN K232 34th GROT2*GT2ST 314 SEDALIA MO64093 062519742.34 3245.23 STUD32 CMSU GROA2*GN334 ST WARRENSBURMO65337 81654341114 YHOMEBUILTVY18 N GROT3*GP1816429329181674700008163475753GROT3INGER GLENN K232 34th GROT3*GY1ST 314 SEDALIA MO64093 062519742.34 3245.23 STUD32 CMSU GROT3*GN234 ST WARRENSBURMO65337 81654341114 YHOMEBUILTVY18 N GROT4*GI1816429329181674700008163475753BROT4INGER GLENN K2E2 34th GROT4*GY2ST 314 SEDALIA MO64093 062518742.34 3245.23 STUD32 CMSU GROT4*GN334 ST WARRENSBURMO65337 81654341114 YHOMEBUILTVY18 N GROT5*GP1816429329181674700008163475753GROT5INGER GLENN K232 34th GROT5*GY2ST 314 SEDALIA MO64093 062519742.34 3245.23 STUD32 CMSU GROT5*GN334 ST WARRENSBURMO65337 81654341114 YHOMEBUILTVY18 N GROT6*GP1816429329181674700008163475753GROT6INGER GLENN K232 34th GROT6*GY2ST 314 SEDALIA MO64093 062519742.34 3245.23 STUD32 CMSU GROT6*GN334 ST WARRENSBURMO65337 81654341114 YHOMEBUILTVY18 N GROT7*GP1816429329181674700008163475753GROT7INGER GLENN K232 34th GROT7*GY2ST 314 SEDALIA MO64093 062519742.34 3245.23 STUD32 CMSU GROT7*GN334 ST WARRENSBURMO65W37 816543411134NHOMEBUILT 00 N GROT8*GP1816429329181674700008163475753GROT8INGER GLENN K232 34th GROT8*GY2ST 314 SEDALIA MO64093 062519742.34 3245.23 RET 32 CMSU GROT8*GN334 ST WARRENSBURMO65337 81654341114 YHOMEBUILTZY18 N GROT9*GP1816429329181674700008163475753GROT9INGER GLENN K232 34th GROT9*GY2ST 314 SEDALIA MO64093 062519742.34 3245.23 STUD32 CMSU GROT9*GN334 ST WARRENSBURMO65337 81654341114 YHOMEBUILTVY18 N GROT0*GP1816429329181674700008163475753GROT0INGER GLENN K232 34th GROT0*GY2ST 314 SEDALIA MO64093 062519742.34 3245.23 STUD32 CMSU GROT0*GN334 STR WARRENSBURMO65337 81654341114 YHOMEBUILTVYA8 N Notes ----- 1) You may use a for loop to read each set of 3 lines. I will not throw an error of omission of lines into the data file. There will always be multiples of 3 lines to work with. 2) The included data file in this text file includes errors from all 14 points listed above. The data file I use for the contest will be different, but will as well cover all 14 points listed above... 3) Be sure to get good use of your debugger, as you will NEED it...Also, be sure to plan the program -- this is an easy one, yet it's complex because of the amount of planning it requires...plan well, it's easy. Don't plan well, it's a bugger...:> 4) ONE hint: remember string addressing, and use of the copy procedure. 5) Another hint. You can have what is referred to as "next sentence" IF THEN ELSE statements. It is very good in this program to be able to use them. (if condition then else) is essentially, a do nothing if con- dition is true situation. I suggest it because the pascal operator NOT seems to not work right in all cases. :< Also, keep in mind that this is the part 10 practice, too, so be sure to at least attempt it! Next Time ========= Interfacing with a common format; how data types are stored in memory and on disk. You may wish to obtain use of a hex viewer for this next part. Send comments to ggrotz@2sprint.net. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 11 -- data representation; reading specs sheets copyright (c) 1995-96 by Glenn Grotzinger Is there still any interest in this tutorial? If so, tell me! :> Here is the solution I got from part 10...No one tried it and sent it to me. Here's the UNIT. Keep in mind that I said to JUST CREATE ONE. You don't have to have the same functions that I had in there. Just as long as it works.... {$O+} unit unit10; interface type strtype = array[1..3] of string[80]; {$I COMPHVN.INC} function numeric(str: string): boolean; procedure writerecord(var outfile: file; strs: strtype); implementation function numeric(str: string): boolean; var num: boolean; i: integer; begin i := 1; num := true; while (num) and (i <= length(str)) do begin num := (str[i] in ['0'..'9','.',' ']); inc(i); end; numeric := num; end; procedure writerecord(var outfile: file; strs: strtype); var numerr: integer; writerec: comphvndata; begin with writerec do begin {part 1} datacode := copy(strs[1], 1, 7); acct_classification := strs[1][8]; val(copy(strs[1], 10, 3), phone_area, numerr); val(copy(strs[1], 13, 3), phone_prefix, numerr); val(copy(strs[1], 16, 4), phone_exchange, numerr); val(copy(strs[1], 20, 3), work_area, numerr); val(copy(strs[1], 23, 3), work_prefix, numerr); val(copy(strs[1], 26, 4), work_exchange, numerr); val(copy(strs[1], 30, 3), other_area, numerr); val(copy(strs[1], 33, 3), other_prefix, numerr); val(copy(strs[1], 36, 4), other_exchange, numerr); cnct1_lname := copy(strs[1], 40, 16); cnct1_fname := copy(strs[1], 56, 11); cnct1_minit := strs[1][67]; val(copy(strs[1], 68, 5), cnct1_pobox, numerr); cnct1_sname := copy(strs[1], 73, 8); {part2} accept_check := (strs[2][8] = 'Y'); cnct1_stype := copy(strs[2], 10, 4); val(copy(strs[2], 14, 4), cnct1_apt, numerr); cnct1_city := copy(strs[2], 18, 10); cnct1_state := copy(strs[2], 28, 2); val(copy(strs[2], 30, 9), cnct1_zip, numerr); val(copy(strs[2], 39, 2), cnct1_birthm, numerr); val(copy(strs[2], 41, 2), cnct1_birthd, numerr); val(copy(strs[2], 43, 4), cnct1_birthy, numerr); val(copy(strs[2], 47, 9), balnce_credt, numerr); val(copy(strs[2], 56, 8), total_sold, numerr); cnct1_emp_code := copy(strs[2], 64, 4); val(copy(strs[2], 68, 3), total_sales, numerr); emp_name := copy(strs[2], 71, 10); {part3} accept_credt := (strs[3][8] = 'Y'); val(copy(strs[3], 10, 4), emp_stnum, numerr); emp_sttype := copy(strs[3], 14, 4); emp_city := copy(strs[3], 18, 10); emp_state := copy(strs[3], 28, 2); val(copy(strs[3], 39, 3), emp_area, numerr); val(copy(strs[3], 42, 3), emp_prefix, numerr); val(copy(strs[3], 45, 4), emp_exchange, numerr); val(copy(strs[3], 49, 2), emp_yrs, numerr); compu := (strs[3][51] = 'Y'); compu_type := copy(strs[3], 52, 9); compu_mon := strs[3][61]; compu_cdr := (strs[3][62] = 'Y'); compu_cdt := strs[3][63]; val(copy(strs[3], 64, 2), compu_mem, numerr); minor := (strs[3][66] = 'Y'); end; blockwrite(outfile, writerec, sizeof(writerec)); end; end. And now here is the main program I got for part 10. What I can see that is not readily explainable here (I used every method I could think of that falls into the rules I outlined for the thing.). The two compiler directives you see below turn off stack checking and range checking respectively. With regards to data intensive applications such as sorting, large #'s of comparisons, and so forth, adding these speeds it up. {$S-} {$R-} program part10; uses dos, unit10, overlay; {$O UNIT10.TPU} var strs: strtype; outfile: file; infile, errfile: text; i: integer; errwritten: boolean; procedure writeerror(errline, errtype: string); var temp1: string; begin temp1 := copy(errline, 1, 20); writeln(errfile, temp1,'':10, errtype); errwritten := true; end; function checkstatus(strs: strtype): boolean; var check: boolean; seqs: array[1..3] of char; cnts: array[1..3] of byte; i: byte; errtype: string; begin check := true; for i := 1 to 3 do cnts[i] := 0; for i := 1 to 3 do begin seqs[i] := strs[i][9]; case seqs[i] of '1': inc(cnts[1]); '2': inc(cnts[2]); '3': inc(cnts[3]); end; end; for i := 1 to 3 do begin if cnts[i] = 0 then begin errtype := 'Missing line #' + chr(i+48); writeerror(strs[i], errtype); check := false; end; if cnts[i] = 2 then begin errtype := 'Duplicate line #' + chr(i+48); writeerror(strs[i], errtype); check := false; end; end; checkstatus := check; end; procedure checkdatacodes(strs: strtype); var datacodes: array[1..3] of string[7]; check1: string[5]; check2: char; error: string[20]; begin for i := 1 to 3 do datacodes[i] := copy(strs[i], 1, 7); check1 := copy(strs[1], 40, 5); check2 := strs[1][56]; for i := 1 to 3 do begin if datacodes[i][6] <> '*' then writeerror(strs[i], 'Invalid datacode.'); if (check1 <> copy(datacodes[i], 1, 5)) or (check2 <> datacodes[i][7]) then writeerror(strs[i], 'Datacode does not agree with name.'); end; end; procedure checknumeric(strs: strtype); var temp1, temp2: string; int, numerr, numdiff: integer; year, month, day, dayofweek: word; empyrs, birthy, bmo, bday: word; age: byte; isminor: boolean; begin if numeric(copy(strs[1], 10, 3)) = false then writeerror(strs[1], 'Phone-area is not numeric.'); if numeric(copy(strs[1], 13, 3)) = false then writeerror(strs[1], 'Phone-prefix is not numeric.'); if numeric(copy(strs[1], 16, 4)) = false then writeerror(strs[1], 'Phone-exchange is not numeric.'); if numeric(copy(strs[1], 20, 3)) = false then writeerror(strs[1], 'Work-area is not numeric.'); if numeric(copy(strs[1], 23, 3)) = false then writeerror(strs[1], 'Work-prefix is not numeric.'); if numeric(copy(strs[1], 26, 4)) = false then writeerror(strs[1], 'Work-exchange is not numeric.'); if numeric(copy(strs[1], 30, 3)) = false then writeerror(strs[1], 'Other-area is not numeric.'); if numeric(copy(strs[1], 33, 3)) = false then writeerror(strs[1], 'Other-prefix is not numeric.'); if numeric(copy(strs[1], 36, 4)) = false then writeerror(strs[1], 'Other-exchange is not numeric.'); if numeric(copy(strs[1], 68, 5)) = false then writeerror(strs[1], 'cnct1-pobox is not numeric.'); if numeric(copy(strs[3], 30, 9)) = false then writeerror(strs[3], 'emp-zip is not numeric.'); if numeric(copy(strs[3], 39, 3)) = false then writeerror(strs[3], 'emp-area is not numeric.'); if numeric(copy(strs[3], 42, 3)) = false then writeerror(strs[3], 'emp-prefix is not numeric.'); if numeric(copy(strs[3], 45, 4)) = false then writeerror(strs[3], 'emp-exchange is not numeric.'); temp2 := copy(strs[3], 49, 2); if numeric(temp2) = false then writeerror(strs[3], 'emp-yrs is not numeric.') else val(temp2, empyrs, numerr); if numeric(copy(strs[2], 30, 9)) = false then writeerror(strs[2], 'cnct1-zip is not numeric.'); temp2 := copy(strs[2], 39, 2); if numeric(temp2) = false then writeerror(strs[2], 'cnct1-birthm is not numeric.') else val(temp2, bmo, numerr); temp2 := copy(strs[2], 41, 2); if numeric(temp2) = false then writeerror(strs[2], 'cnct1-birthd is not numeric.') else val(temp2, bday, numerr); temp1 := copy(strs[2], 43, 4); if numeric(temp1) = false then writeerror(strs[2], 'cnct1-birthy is not numeric.') else begin val(temp1, int, numerr); if (int < 1900) or (int > 1999) then writeerror(strs[2], 'cnct1-birthy is not in this century.'); end; getdate(year, month, day, dayofweek); val(temp1, birthy, numerr); numdiff := year - birthy; if numdiff < empyrs then writeerror(strs[3], 'The emp-yrs doesn''t make sense.'); age := year - birthy; if month < bmo then dec(age); if month = bmo then if day < bday then dec(age); isminor := (age < 21); if ((isminor = true) and (strs[3][66] = 'N')) or ((isminor = false) and (strs[3][66] = 'Y')) then writeerror(strs[3], 'Minor is set incorrectly.'); if numeric(copy(strs[2], 47, 9)) = false then writeerror(strs[2], 'balance-credt is not numeric.'); if numeric(copy(strs[2], 56, 8)) = false then writeerror(strs[2], 'total-sold is not numeric.'); if numeric(copy(strs[2], 68, 3)) = false then writeerror(strs[2], 'total-sales is not numeric.'); if numeric(copy(strs[3], 64, 2)) = false then writeerror(strs[3], 'compu-mem is not numeric.'); end; procedure checkprefix(strs: strtype); begin if strs[1][13] in ['0'..'1'] then writeerror(strs[1], 'phone-prefix started with a 0 or 1.'); if strs[1][23] in ['0'..'1'] then writeerror(strs[1], 'work-prefix started with a 0 or 1.'); if strs[1][33] in ['0'..'1'] then writeerror(strs[1], 'other-prefix started with a 0 or 1.'); if strs[3][42] in ['0'..'1'] then writeerror(strs[3], 'emp-prefix started with a 0 or 1.'); end; procedure checkacct(strs: strtype); begin if strs[1][8] in ['B','C','G','P','O'] then else writeerror(strs[1], 'acct-classification is invalid.'); end; procedure checkyn(strs: strtype); begin if strs[2][8] in ['Y', 'N'] then else writeerror(strs[2], 'accept-check is invalid.'); if strs[3][8] in ['Y', 'N'] then else writeerror(strs[3], 'accept-credt is invalid.'); if strs[3][51] in ['Y', 'N'] then else writeerror(strs[3], 'compu is invalid.'); if strs[3][62] in ['Y', 'N'] then else writeerror(strs[3], 'compu-cdr is invalid.'); if strs[3][66] in ['Y', 'N'] then else writeerror(strs[3], 'minor is invalid.'); end; procedure checkcompun(strs: strtype); begin if strs[3][51] = 'N' then if (copy(strs[3], 52, 14) <> ' 0 ') then writeerror(strs[3], 'There were fields present when compu was N.'); end; procedure checkempcode(strs: strtype); begin if copy(strs[2], 64, 4) = 'RET ' then if (copy(strs[2], 71, 10) <> ' ') and (copy(strs[3], 10, 20) <> '0 ') and (copy(strs[3], 30, 20) <> '0 0 0 0 ') then writeerror(strs[3], 'empcodes are present when RET is true.'); end; procedure checkcompumon(strs: strtype); begin if strs[3][61] in ['S','V','E','C','H','I'] then else writeerror(strs[3], 'compu-mon is invalid.'); end; procedure checkcompucdt(strs:strtype); begin if strs[3][63] in ['1','2','4','6','8'] then else writeerror(strs[3], 'compu-cdt is invalid.'); end; procedure checksttype(strs: strtype); var a: string; begin a:=copy(strs[3], 14, 4); if (a <> 'BLVD') and (a <> 'LANE') and (a <> 'ST ') and (a <> 'AVE ') and (a <> 'CT ') and (a <> 'LOOP') and (a <> 'DR ') and (a <> 'CIRC') and (a <> 'RR ') then writeerror(strs[3], 'emp-sttype is invalid.'); a:=copy(strs[2], 10, 4); if (a <> 'BLVD') and (a <> 'LANE') and (a <> 'ST ') and (a <> 'AVE ') and (a <> 'CT ') and (a <> 'LOOP') and (a <> 'DR ') and (a <> 'CIRC') and (a <> 'RR ') then writeerror(strs[2], 'cnct1-stype is invalid.'); end; procedure writeheader; begin writeln(errfile, 'Error Report -- INDATA.TXT':50); writeln(errfile, '--------------------------':50); writeln(errfile); writeln(errfile, 'First 20 characters','':10, 'Problem'); writeln(errfile, '-------------------','':10, '-------'); end; begin ovrinit('PART10.OVR'); if ovrresult <> 0 then begin case ovrresult of -1: writeln('Overlay manager error.'); -2: writeln('Overlay file not found.'); -3: writeln('Not enough memory for overlay buffer.'); -4: writeln('Overlay I/O error.'); end; halt(1); end; ovrinitems; case ovrresult of 0: writeln('Overlay loaded to EMS memory!'); -5: writeln('No EMS driver found! Loading to conventional memory!'); -6: writeln('Not enough EMS memory to load! Loading to conventional', ' memory!'); end; assign(infile, 'INDATA.TXT'); reset(infile); assign(errfile, 'ERRORS.LOG'); rewrite(errfile); assign(outfile, 'COMPHVN.DAT'); rewrite(outfile, 1); writeheader; while not eof(infile) do begin errwritten := false; for i := 1 to 3 do readln(infile, strs[i]); if checkstatus(strs) then begin checkdatacodes(strs); checknumeric(strs); checkprefix(strs); checkacct(strs); checkyn(strs); checkempcode(strs); checkcompun(strs); checkcompumon(strs); checkcompucdt(strs); checksttype(strs); end; if errwritten = false then writerecord(outfile, strs); end; close(infile); close(errfile); close(outfile); end. Basically, the plan for this tutorial is to go through all standard data types and show how TP stores them in memory, so we will be able to interpret a binary data file we may create. Then, using the information we presented on how things are stored, we will get a spec sheet on a common format, and interpret the data, so we may be able to obtain data from that file through a Pascal program. Byte ==== The basic idea of a byte has been covered in part 7 of the tutorial. Please refer back to there for a refresher on the concept of what a byte is. Basically, a byte can be used to substitute or represent a char type, or a number type....The number types, stored as a byte, have a limit of 0..255 as an unsigned number (byte), and -128..127 with a signed number (shortint). The terms in the parentheses are what we would define the byte to be to get the specified range. I will explain later in this tutorial the difference between a "signed" and "unsigned" number, actually signed and unsigned data space for numbers. The number for an unsigned number is formed, much like we were doing the binary computations in part 7. Numbers as Integers =================== There are several types of numbers we can define... Byte, and ShortInt: as described above in the byte description... Integer types are either as signed (range: -32768..32767) or unsigned (0..65535) words. A word is a unit of 2 bytes in TP. Basically, in a unsigned integer, the number is calculated from right to left in binary much like a byte. Since it is easier, for us to show binary storage repsentation as hexadecimal units of bytes, we will use hexadecimal values for an example. An integer type is written to disk, to test something. The sequence of two bytes is: 3F 4D . What is the number in base 10 form? If we remember from our hex notation, and the way things work: 3 * 16^3 + 15 * 16^2 + 4 * 16^1 + 13 * 16^0 = 16205 An integer is ALWAYS two bytes, whether or not the number may technically fill two bytes in this manner described before. For example, 13 in base 10 is stored in memory and on disk by TP in integer form as 00 0D . We could equally store this number as a byte if we knew that this variable location would never be requested to hold a number that is greater than 255. For example, if we knew we were going to hold days of the month in memory, we would never have a number greater than 31, so a byte type for this number would be appropriate. A longint type is always a signed number, but we do need to know, that it is what is called a double word, or two words put together. Therefore, we know that a longint type is 4 bytes, and has a maximum limit of 2bil. A longint is ALWAYS 4 bytes, whether or not the number may fill 4 bytes. 13 in base 10 would be stored in a longint as 00 00 00 0D . Char ==== A character is stored in memory as a byte, with the byte value representing the corresponding character as it refers to the ASCII chart. byte representation 67 as a char is C. Signed vs. Unsigned Integer Numbers =================================== We have talked before in this part about signed and unsigned numbers. In part 7, essentially, we have covered unsigned numbers, when we were describing binary logic. Let's describe what a signed number is. A signed number is represented by either using a base system of 0..1, or 1..0 in binary. In a signed number, the leftmost bit is either 0 for a positive number, and 1 for a negative number. For positive numbers, the remaining bits are considered using a 0..1 scheme as we did in part 7 with the binary computations. For a negative number, we count starting from 1 and go down to 0. Let's observe what that difference is, by demonstrating how 3 and -3 would be shown in a shortint (signed) type in binary. Let's start with 3 in binary as a signed number. That is a positive number so the leftmost bit will be 0. Then we will use a 0..1 counting system to finish out the number using standard binary notation. So, 3 will be represented in a shortint value as: 0000 0010 just like it was an unsigned number. Now, lets observe what -3 would look like. That is a negative number, so the leftmost bit would be a 1. Since we use a 1..0 system, we would start with 1's in everything. We know 3 is 10 in binary, so we use a reverse system and come up with: 1111 1101 To illustrate further, in binary counting (to a computer) -1..-5 would be (represented in hex) as $FF,$FE,$FD,$FC,$FB; as opposed to $01,$02,$03, $04,$05 for 1..5 . In a signed system, negative number counting starts at -1, which explains why the equal range of a shortint is -128..127, and there are equally 128 items (positive and negative). As practice, look at the integers.dat now, in a hex viewer, and see exactly how the numbers are stored. They are two bytes in length, and should count from one to ten, as we did in the program. It should look like this in your hex viewer.... 00 01 00 02 00 03 00 04 00 05 00 06 00 07 00 08 00 09 00 0A I recommend very heavily that you get a hex viewer to be able to help out. Boolean ======= 0 = False; 1 = True Common Pascal Strings ===================== Now we will start to discuss the format of grouped data. The first format we will discuss will be the common pascal string. The format of a pascal string formatted group of characters is the following. When we define a string without a subscript, we are defining a maximum of 255 characters. When we define for example, a string[20], we are defining a max of 20 characters. In reality, we are defining the number of char- acters + 1. A string has a first byte (0th byte) that represents the actual character length of the data stored in the string, as an unsigned byte. (Actually, the length function reads this byte when you call it, but it's also possible to read and refer to the byte, and even SET it.) Refer back to part 8 where I said you could individually set each part of the string. It's possible to actually build a string by setting the length byte, then setting the rest of the string as characters. Say, to set the dynamic length of a string to 4, we can do this, as in this example, which will only write "Hello" instead of "Hello world!". We are setting the 0'th part of the string as a character, which we need to do: program test; var p: string; begin p := 'Hello world!'; p[0] := #5; writeln(p); end. Blocked Character Strings ========================= It's also possible to work with an array of characters as a string, by referring to the whole array. The maximum length must be set by the length of the array, essentially. str: array[1..20] of char; if we read this structure in as a whole, it will have 20 characters in it, in which we can write back out to the screen by saying WRITE(STR);, or actually convert to a string by counting through to find the actual length and then setting the length byte. Null Terminated Strings ======================= This is a length of characters, which are terminated by a null character, or #0. The strings unit is documented to work with these strings, but it's easier to get away with simply converting it into something we can work with through Pascal itself, if we have to do much with it. Arrays ====== The general structure of an array was described in part 6. It is basically usable as a grouping of items. Records ======= A record is stored in memory basically as a grouping of data types. The record type below: datarecord = record int: integer; character: char; end; would be stored in memory like this: INT CHARACTER Now, we have described all of the pertinent items we need to know to be able to work through a spec sheet on a common format. Basically, data for spec sheets are sometimes presented as the record format, in either Pascal or C format. We don't need to really do any work for that, but most of the time, it will be presented in a byte by byte offset format. Let us look at this structure file for an example. It is of the standard sound file called a MOD file. (you can find them anywhere, almost) We will cover just the header of the file for now... ------------------------------------------------------------------------- Protracker 1.1B Song/Module Format: Offset Bytes Description 0 20 Songname. Remember to put trailing null bytes at the end... Information for sample 1-31: Offset Bytes Description 20 22 Samplename for sample 1. Pad with null bytes. 42 2 Samplelength for sample 1. Stored as number of words. Multiply by two to get real sample length in bytes. 44 1 Lower four bits are the finetune value, stored as a signed four bit number. The upper four bits are not used, and should be set to zero. Value: Finetune: 0 0 1 +1 2 +2 3 +3 4 +4 5 +5 6 +6 7 +7 8 -8 9 -7 A -6 B -5 C -4 D -3 E -2 F -1 45 1 Volume for sample 1. Range is $00-$40, or 0-64 decimal. 46 2 Repeat point for sample 1. Stored as number of words offset from start of sample. Multiply by two to get offset in bytes. 48 2 Repeat Length for sample 1. Stored as number of words in loop. Multiply by two to get replen in bytes. Information for the next 30 samples starts here. It's just like the info for sample 1. Offset Bytes Description 50 30 Sample 2... 80 30 Sample 3... . . . 890 30 Sample 30... 920 30 Sample 31... Offset Bytes Description 950 1 Songlength. Range is 1-128. 951 1 Well... this little byte here is set to 127, so that old trackers will search through all patterns when loading. Noisetracker uses this byte for restart, but we don't. 952 128 Song positions 0-127. Each hold a number from 0-63 that tells the tracker what pattern to play at that position. 1080 4 The four letters "M.K." - This is something Mahoney & Kaktus inserted when they increased the number of samples from 15 to 31. If it's not there, the module/song uses 15 samples or the text has been removed to make the module harder to rip. Startrekker puts "FLT4" or "FLT8" there instead. Source: Lars "ZAP" Hamre/Amiga Freelancers -------------------------------------------------------------------------- Building the basic record format ================================ We will need to essentially, for any standard file, built record format(s) for the file. Let us start with this one. It says above that the first 20 bytes would be a song title. The description best seems to define it as a null-terminated string, but we know the maximum limit. So, lets just call it a blocked character string of length 20. So we will use this description for part of our component record: songname: array[1..20] of char; If we read on through the file, it states data for samples 1-31. They are varied data, so that infers that we need to build another record format. But for the position so far in our current record format, we will use this, since we know that there are a group of 31 samples...we will call our alternate record for the sample data, samplerec. sampledata: array[1..31] of samplerec; Alternate Sample Record ======================= 22 bytes are defined for a samplename. Same logic for songname. So this unit of our sample record will be samplename: array[1..22] of char; The next part is a sample length defined by 2 bytes. So, we could either call this a word, or an integer: samplelength: integer; One byte for a finetune value. Logical definition: finetune: byte; Volume is defined as one byte. So... volume: byte; Repeat point and Repeat length are both defined as words above so... repeatpoint: integer; repeatlength: integer; It is indicated above that we are done with the samples. Therefore, our final record format for the samples would be: samplerec = record samplename: array[1..22] of char; samplelength: integer; finetune: byte; volume: byte; repeatpoint: integer; repeatlength: integer; end; Finishing the Record Format =========================== Continuing on... songlength is a byte. So songlength: byte; A byte is defined next that seems like a filler byte. So... filler: byte; The next set of 128 bytes are defined as "song positions 0-127". Each byte is also defined to hold a number, so lets keep to that: positions: array[0..127] of byte; The next 4 bytes to finish out our headers is the moduleid. it's 4 bytes, text, so... id: array[1..4] of char; Having gone through the header format for this file, we have come up with a record we can use to read all the header data, that we would need to read for a mod file. Therefore, we can use a final type listing in our program to read a mod file header of: samplerec = record samplename: array[1..22] of char; samplelength: integer; finetune: byte; volume: byte; repeatpoint: integer; repeatlength: integer; end; modrecord = record songname: array[1..20] of char; sampledata: array[1..31] of samplerec; songlength: byte; filler: byte; positions: array[0..127] of byte; id: array[1..4] of char; end; We can use this format to gain any information we know about a file. To test this format, we need to write a program that pulls the information out of a standard file, that we know, and check to see if they're identical. For MODs, we would need to get a player, and load up the player, in order to get data we are familiar with from the spec sheet, such as the id being "M.K.". All the data we need to know to extract data out, and get formatted data from standards files, have been presented. Practice Programming Problem #11 ================================ In keeping with the topic of this tutorial, I am asking you to write a program entirely in Pascal that will take a ZIP file name from the command- line and then print a list of all the files within that zip file. With each filename, list the compressed size, uncompressed size, date, time, and compression method, one per file. At the end, list the total number of files, total compressed and uncompressed size, effective compression ratio, and write the comment. Then output this list to a file taken on the command-line. For example, a command-line will always be: ZIPLIST FILENAME.ZIP OUTPUT.TXT Be sure to design this program with any error checks you may need to perform. Don't forget to devise a check to be sure you are dealing with a ZIP file on the first parameter. Here, we have gone to look at some references, and have found the following out of a specs list: --------A-ZIP------------------------------- The ZIP archives are created by the PkZIP/PkUnZIP combo produced by the PkWare company. The PkZIP programs have with LHArc and ARJ the best compression. The directory information is stored at the end of the archive, each local file in the archive begins with the following header; This header can be used to identify a ZIP file as such : OFFSET Count TYPE Description 0000h 4 char ID='PK',03,04 0004h 1 word Version needed to extract archive 0006h 1 word General purpose bit field (bit mapped) 0 - file is encrypted 1 - 8K/4K sliding dictionary used 2 - 3/2 Shannon-Fano trees were used 3-4 - unused 5-15 - used internally by ZIP Note: Bits 1 and 2 are undefined if the compression method is other than type 6 (Imploding). 0008h 1 word Compression method (see table 0010) 000Ah 1 dword Original DOS file date/time (see table 0009) 000Eh 1 dword 32-bit CRC of file (inverse??) 0012h 1 dword Compressed file size 0016h 1 dword Uncompressed file size 001Ah 1 word Length of filename ="LEN" 001Ch 1 word Length of extra field ="XLN" 001Eh "LEN" char path/filename 001Eh "XLN" char extra field +"LEN" After all the files, there comes the central directory structure. (Table 0009) Format of the MS-DOS time stamp (32-bit) The MS-DOS time stamp is limited to an even count of seconds, since the count for seconds is only 5 bits wide. 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 |<---- year-1980 --->|<- month ->|<--- day ---->| 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 |<--- hour --->|<---- minute --->|<- second/2 ->| (Table 0010) PkZip compression types 0 - Stored / No compression 1 - Shrunk / LZW, 8K buffer, 9-13 bits with partial clearing 2 - Reduced-1 / Probalistic compression, lower 7 bits 3 - Reduced-2 / Probalistic compression, lower 6 bits 4 - Reduced-3 / Probalistic compression, lower 5 bits 5 - Reduced-4 / Probalistic compression, lower 4 bits 6 - Imploded / 2/3 Shanno-Fano trees, 4K/8K sliding dictionary [7..9 also exist] => note added by Glenn Grotzinger from Phil Katz's description --- Central directory structure The CDS is at the end of the archive and contains additional information about the files stored within the archive. OFFSET Count TYPE Description 0000h 4 char ID='PK',01,02 0004h 1 byte Version made by 0005h 1 byte Host OS (see table 0011) 0006h 1 byte Minimum version needed to extract 0007h 1 byte Target OS see above "Host OS" 0008h 1 word General purpose bit flag see above "General purpose bit flag" 000Ah 1 word Compression method see above "Compression method" 000Ch 1 dword DOS date / time of file (see table 0009) 0010h 1 dword 32-bit CRC of file 0014h 1 dword Compressed size of file 0018h 1 dword Uncompressed size of file 001Ch 1 word Length of filename ="LEN" 001Eh 1 word Length of extra field ="XLN" 0020h 1 word Length of file comment ="CMT" 0022h 1 word Disk number ?? 0024h 1 word Internal file attributes (bit mapped) 0 - file is apparently an ASCII/binary file 1-15 - unused 0026h 1 dword External file attributes (OS dependent) 002Ah 1 dword Relative offset of local header from the start of the first disk this file appears on 002Eh "LEN" char Filename / path; should not contain a drive or device letter, all slashes should be forward slashes '/'. 002Eh+ "XLN" char Extra field +"LEN" 002Eh "CMT" char File comment +"LEN" +"XLN" (Table 0011) PkZip Host OS table 0 - MS-DOS and OS/2 (FAT) 1 - Amiga 2 - VMS 3 - *nix 4 - VM/CMS 5 - Atari ST 6 - OS/2 1.2 extended file sys 7 - Macintosh 8-255 - unused --- End of central directory structure The End of Central Directory Structure header has following format : OFFSET Count TYPE Description 0000h 4 char ID='PK',05,06 0004h 1 word Number of this disk 0006h 1 word Number of disk with start of central directory 0008h 1 word Total number of file/path entries on this disk 000Ah 1 word Total number of entries in central dir 000Ch 1 dword Size of central directory 0010h 1 dword Offset of start of central directory relative to starting disk number 0014h 1 word Archive comment length ="CML" 0016h "CML" char Zip file comment EXTENSION:ZIP OCCURENCES:PC,Amiga,ST PROGRAMS:PkZIP,WinZIP REFERENCE:Technote.APP Source: FILEFMTS (c) 1994,1995 Max Maischein Sample output for this program ============================== Files contained in FILENAME.ZIP NAME COMP-SIZE DATE TIME UNCOMP-SIZE COMP-METHOD --------------------------------------------------------------------- FOOBAR10.TXT 732 10-01-1993 02:30 1021 7 FOBAR11.ZIP 11021 12-01-1995 22:03 23923 6 ... --------------------------------------------------------------------- 1520032 4023232 15 files; Effective compression ratio: 65.3% < write the comment here, or write "No ZIP comment." if there is no ZIP comment > Notes: 1) Note how I have the sample output. It needs to be EXACTLY as I have listed it. 2) The orders and the methods you need to use to read files should be evident from the specs file. They will be random reads... 3) With the random reads, it is about impossible to tell what you got read unless you check first...Notice a good way to check out of the first field of each record... 4) The "MS-DOS Time Format" can be done really easily. Just think DOS unit. 5) Do not read anything unless you HAVE to.... 6) Since this is a program that happens to use another program's data, more than likely, the data are correct, so there is no need to check any of the data beyond determining whether it's an actual ZIP file. With this data listed, you should be able to do anything related to listing, stripping comments, showing comments, and so on and so forth. Next Time ========= We will cover the use of stacks and queues. Send any comments, encouragements, problems, etc, etc. to ggrotz@2sprint.net. Haven't seen anything of that type, ya know.... Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 12: Stacks and Queues copyright(c) 1995-96 by Glenn Grotzinger Solution to part 11's problem.... program part11; uses dos; { Lists files and data in a ZIP file. } type ziplocfileheader = record zipver: integer; bitfield: word; compmethod: integer; packtime: longint; crc32: longint; compsize: longint; uncompsize: longint; filelen: integer; xtralen: integer; end; zipcds = record ver: byte; hostos: byte; verxtr: byte; targos: byte; bitfield: word; compmethod: integer; packtime: longint; crc32: longint; compsize: longint; uncompsize: longint; filelen: integer; xtralen: integer; comtlen: integer; disknum: integer; intattr: integer; extattr: longint; roffset: longint; end; endcdsheader = record numdisk: integer; numdiska: integer; numfiles: integer; entries: integer; size: longint; ofs: longint; comtlength: integer; end; var zipfile: file; outfile: text; locfile: ziplocfileheader; dirstr: zipcds; endcds: endcdsheader; filechar: char; filelength: array[1..20] of char; id: array[1..4] of char; i: integer; totsize, compsize, uncompsize: longint; param1, param2: string; { Example structure of a ZIP file. localheader1 loccompresseddata1 localheader2 loccompresseddata2 centraldirectorystructure1 centraldirectorystructure2 endofcentraldirectorystructure } procedure readzip; { ZIP is a random data binary file. We read the ID to check it, then differentiate it from there. We also devise a test of whether the "ZIP file" is a valid ZIP file here. } begin blockread(zipfile, id, sizeof(id)); totsize := totsize + 4; { totsize is a seek index } if (id[1] <> 'P') and (id[2] <> 'K') then begin writeln('Corrupted ZIP file, or not a ZIP file.'); halt(1); end; case id[3] of #05: begin blockread(zipfile, endcds, sizeof(endcds)); totsize := totsize + sizeof(endcds); end; #03: begin blockread(zipfile, locfile, sizeof(locfile)); totsize := totsize + sizeof(locfile); end; #01: begin blockread(zipfile, dirstr, sizeof(dirstr)); totsize := totsize + sizeof(dirstr); end; end; end; function zero(int: integer):string; {places zero on the beginning of a number if it's < 10 } var strng: string; numerr: integer; begin str(int, strng); if int < 10 then strng := '0' + strng; zero := strng; end; function strip(str: string):string; { removes null characters from random-read filename } var tstr: string; i: integer; begin tstr := ''; for i := 1 to length(str) do if str[i] <> #0 then tstr := tstr + str[i]; strip := tstr; end; function exist(filename: string):boolean; { file existence test } var thefile: text; begin assign(thefile, filename); {$I-}reset(thefile);{$I+} if IOResult <> 0 then exist := false else begin exist := true; close(thefile); end; end; procedure header(filename: string); { writes header of data } var s: string; begin writeln(outfile, 'Files contained in ':41, filename); writeln(outfile); writeln(outfile, 'NAME', 'COMP-SIZE':21, 'DATE':9,'TIME':12, 'UNCOMP-SIZE':15, 'COMP-METHOD':13); fillchar(s, 75, '-'); { fillchar fills a set of contiguous bytes of any type defined by the position s is in, for x number of spaces (75), with a char- acter represented by - } s[0] := #74; { set length byte } writeln(outfile, s); end; procedure footer(compsize, uncompsize: longint); { write footer of data } var s: string; begin fillchar(s, 75, '-'); s[0] := #74; writeln(outfile, s); writeln(outfile, compsize:22, uncompsize:34); writeln(outfile); writeln(outfile, endcds.numfiles, ' files; Effective compression', ' ratio: ', (compsize / uncompsize)*100 :0:1, '%'); end; procedure writehelp; { help for incorrect usage } begin writeln('ZIPLIST '); halt(1); end; procedure writedata(locfile: ziplocfileheader); var dt: datetime; begin unpacktime(locfile.packtime, dt); { this handles the MS-DOS time format } writeln(outfile, strip(filelength):12, locfile.compsize:10, zero(dt.month):7,'-',zero(dt.day),'-',dt.year, zero(dt.hour):6,':', zero(dt.min), locfile.uncompsize:10, locfile.compmethod:12); end; begin if paramcount <> 2 then { incorrect parameters? } writehelp; totsize := 0;compsize := 0;uncompsize := 0; param1 := paramstr(1); param2 := paramstr(2); assign(zipfile, param1); if exist(param1) = false then begin writeln(param1, ' does not exist!'); halt(1); end; reset(zipfile, 1); assign(outfile, param2); rewrite(outfile); header(param1); readzip; while id[3] = #03 do { process local file headers and compressed data } begin blockread(zipfile, filelength, locfile.filelen); compsize := compsize + locfile.compsize; uncompsize := uncompsize + locfile.uncompsize; writedata(locfile); totsize := totsize + locfile.filelen; totsize := totsize + locfile.xtralen; fillchar(filelength, sizeof(filelength),#0); seek(zipfile, totsize); totsize := totsize + locfile.compsize; seek(zipfile, totsize); readzip; end; while id[3] = #01 do { process central directory structure } begin totsize := totsize + dirstr.filelen; seek(zipfile, totsize); totsize := totsize + dirstr.xtralen; seek(zipfile, totsize); readzip; end; footer(compsize, uncompsize); writeln(outfile); { handle ZIP comment } if endcds.comtlength = 0 then writeln(outfile, 'No comment in ZIP file.') else for i := 1 to endcds.comtlength do begin blockread(zipfile, filechar, sizeof(filechar)); write(outfile, filechar); end; close(zipfile); close(outfile); end. Hello. Basically, we are dealing in this part with specialized types of data structures called stacks and queues. Stacks ====== A stack is best analagous to a stack of papers on a desk, only able to either place a sheet on the top of the stack, or take a sheet off the top of the stack. It is generally defined as a record format, which can take on the following: stack = record elements: array[1..maxstacksize] of ; capacity: integer; {range of 0..maxstacksize} end; It is best described as a last in/ first out data structure, much like the sheet of paper analogy. The elements array holds whatever information we need, while capacity tells us how many elements are still in the array. The best description I can use for this and queues is that it's a limited defined access structure. I will now describe the basic actions of working with stacks. The best way to think about coding this is to use the stack of paper as a view- point, and using the sample stack record above. To initialize or effectively kill a stack currently in usage is to set capacity to 0. We can do this, because all work with a stack ultimately comes down to usage of capacity as an index. If we have 0 sheets of paper on an imaginary stack, essentially, the stack is empty. Now, let's add a sheet of paper to our imaginary stack. We recognize that there is one more sheet of paper on the stack now. Then we have to place that sheet on the stack...also, we have to keep in mind if we have hit our arbitrarily set limit of sheets on the stack, and notify of such thing. What now, if we have to remove the sheet of paper from our imaginary stack of paper? We would need to check if we had a sheet of paper to remove. Then after that, we would need to pull the sheet off of the stack, then recognize that we removed the sheet. How do we recognize if we have a full stack or an empty stack? Look at the value of capacity. If it's 0, then it's empty. If it's the value for the maximum capacity of the stack we decide, then we know it's full. Basically, in the last four paragraphs, I have, in discussing the actions of working with stacks, have given you all the pseudocode you need to be able to come up with the code to work with a stack. It is basically good to develop all of these components as procedures. If you come across a problem where a last in, first out solution is needed, a stack may be the proper way to go. Queues ====== A queue is set up much like a line at the local shop to pay the merchant at the cash register. The first element in is the first element out. Much like a stack, a queue may be defined as a record format. queue = record elements: array[1..maxqueuesize] of ; front, back: integer; count: integer; end; Notice that the differences we see here, are that to take from the front of the so called line we need to know where the front is in the order. The same idea occurs with the back. We need to know where the back is in the array. Count just helps us in knowing how many values we currently have in the queue. To start up a queue, basically, we need to start the front at 1, the rear at the maximum size, and count at zero. To check whether it's full or empty is basically set through count. Check count to be zero for a queue to be empty and maxsize for a queue to be full. One sticky mess comes up when we try to add an element to the back of the line. How do we keep track of things? We increment the back unless it's maxqueuesize, then we set it to 1, essentially, to continue reusing the array storage as a loop.... To add to the back of the line, we do like we did with the stack. We add the element to the back, then we increment the back count as described above. To take from the front of the line, we pull the element from the front of the queue, and then increment the front count as described above. Basically, the effect we are getting is much like a message scrolling across a surface, like you may have seen on your television, and at Times Square in NY (on several movies). Conclusion ========== Both of these data storage schemes work beautifully, when you have a lot of data to deal with that can be passed through quickly, but needs to be released at other times...beyond that, at this point, I can't think of anything that can be done by only using a stack or a queue, so my suggestion for practice is to attempt coding each of these specialized data types to do simple things. Practice Programming Problem(s) #12 =================================== Here are a couple of things for this one. 1) Reverse a string of text inputted from the screen (not necessarily a string) of a maximum of 500 characters, and output the text back to the screen. Sample ------ Enter a string: AbCdE Your string reversed is: EdCbA 2) Use a queue to store a maximum set of numbers that is 50 numbers long. These numbers will come from a series of randomly generated numbers by your program. The random numbers you will generate are from 1..1000, and the ones that are from 1..50 will be written out to the screen when the queue is filled up with those numbers. Write the numbers in the queue out in 5 rows of 10, and at the bottom, write out the total number of random numbers you had to go through to get those 50 numbers in the proper range. Sample ------ 23 23 11 22 33 1 9 12 23 11 23 3 11 22 33 11 9 12 23 11 23 23 11 22 33 11 9 12 23 11 23 29 11 2 33 11 9 12 23 11 23 23 11 22 33 11 9 50 23 11 987 numbers were generated from 1-1000 to get the 50 numbers from 1-50. Next Time ========= Recursion. (n) See Recursion. :> Any comments? Write ggrotz@2sprint.net. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 13: Recursion; system unit commands not covered yet. copyright(c) 1995-96 by Glenn Grotzinger There were 2 simple problems presented...The first one... program part12_1; const maxstacksize = 500; type stack = record elements: array[1..maxstacksize] of char; capacity: byte; end; var thestack: stack; i: byte; ch: char; procedure startstack(var thestack: stack); begin with thestack do capacity := 0; end; procedure place(var thestack: stack; element: char); begin with thestack do begin if capacity = maxstacksize then writeln('**ERROR** Trying to place element on full stack!') else begin inc(capacity); elements[capacity] := element; end; end; end; procedure remove(var thestack: stack; var element: char); begin with thestack do begin if capacity = 0 then writeln('**ERROR** Trying to remove element from empty stack!') else begin element := elements[capacity]; dec(capacity); end; end; end; begin randomize; startstack(thestack); write('Enter a string: '); while ch <> #10 do begin read(ch); place(thestack, ch); end; writeln; writeln; write('The string reversed is: '); while thestack.capacity <> 0 do begin remove(thestack, ch); write(ch); end; end. And now the second one... program part12_2; uses crt; const maxqueuesize = 50; type queue = record elements: array[1..maxqueuesize] of integer; front, back: integer; count: integer; end; procedure startqueue(var thequeue: queue); begin with thequeue do begin front := 1; back := maxqueuesize; count := 0; end; end; procedure incqueue(var thenum: integer); begin if thenum = maxqueuesize then thenum := 1 else inc(thenum); end; procedure place(var thequeue: queue; element: integer); begin with thequeue do if count = maxqueuesize then writeln('**ERROR** Trying to place element in full queue.') else begin elements[back] := element; incqueue(back); inc(count); end; end; procedure remove(var thequeue: queue; var element: integer); begin with thequeue do if count = 0 then writeln('**ERROR** Trying to remove element from empty queue.') else begin element := elements[front]; incqueue(front); dec(count); end; end; var thequeue: queue; int, count: integer; begin randomize; startqueue(thequeue); clrscr; while thequeue.count <> maxqueuesize do begin int := random(1000) + 1; if int <= 50 then place(thequeue, int); inc(count); end; while thequeue.count <> 0 do begin remove(thequeue, int); write(int, ' '); if thequeue.count mod 10 = 0 then writeln; end; writeln(count, ' numbers generated from 1-1000 to get these 50 ', 'numbers from 1-50'); end. Forward ======= Very useful to defeat the fact with the compiler that a function has to be defined above another function in order to use the first function in that second function. Above the function that it needs to be in, say something like: function a(var input: integer): string; forward; Then on down below in the code, you need to go ahead and define the procedure or function entirely, with code. For the header, you would use something like this: function a; Doing this would accomplish being able to use a function defined after a code call of the function for some purpose. Successive recursion using two seperate functions (a calls b; b calls a; and so on and so forth), would be one example of having to use a forward. The $M compiler directive ========================= I described the $M compiler directive back in part 8. Please review it there. Basically, the first number of the compiler directive is the program stack size. It is very important. The default, if the $M is not defined is 16K. The maximum it can be defined to is 64K. You may have to use this compiler directive to increase the stack size so a recursion may complete. If the stack is filled, a runtime 202 error occurs. Recursion ========= Recursion, to put it simply, is the execution of a function or procedure directly within that same function or procedure. It is hard, sometimes, logically to see use of recursion, but when you see a thoroughly repetitive action, recursion could be used. Recursion should be used with a procedure which basically has a small number or NO parameters, since the recursion places a new occurrence of the procedure on the stack, along with those variables. It is quite possible for a procedure to recurse itself upwards of thousands of times. Therefore, you could easily run out of memory in the stack in running your program. Basically, in logic, recursion is the repetition of a procedure as a regular or irregular loop by calling the procedure inside of the procedure, with some regulated terminating code. NOTE: Recursion must be done with relative caution in Pascal. It is really, REALLY easy to shoot oneself in the foot, literally, by use of recursion. It has it's advantages, but since Pascal is unlimited by how, and why you use recursion, versus other languages, which may limit it or not allow it all together. As an example, we will look at taking a factorial of a number. Basically, to use an example, 4! (factorial) is 4 X 3 X 2 X 1. Algebraically, we could see a factorial (n!) as n X (n-1) X (n-2) X (n-3)...(n - (n+1)). Let's try looking at a code example that does it...after that, I will explain the process in detail that goes behind how it works. It's a simple, elegant one line set of code in a function that does all the multiplications for any number we put in there. I will try my best to explain what exactly is going on here in this example of recursion. That, I find, is the hardest thing to see in the concept of how recursion works, is because it is hard to conceptualize how the variables and functions work, in a manner that is understandable. In all of the books I have read, I have not seen an adequate description of the actual logic and action of recursion -- enough to allow people to understand the idea of what is going on. Most teachers I've heard of and talked to, just say what I have already said to this point, and shy away from actually requiring written code using recursion, or explaining code using recursion to enable people to truly understand what is going on. I seek to change those observed facts, by trying to fully explain this example below, so people may be able to understand them sufficiently. I hope this explanation could be the best people have ever seen, and I *definitely* want e-mail and feedback on how well I do in explaining the concepts of what is going on (via showing all variable changes at all points, and order of execution of the code), because it is one of the hardest concepts in programming that I have come across in understanding. (I will also ask for input like this in the part I write later on readdressing pointers) program example_of_recursion; function factorial(a: longint): longint; var c: longint; begin {1} c := 1; {2} if a > 1 then { {3} c := a * factorial(a-1); {4} factorial := c; end; begin writeln(factorial(4)); end. to explain the path of logic in this program in calling the function factorial with regard to the variables, the biggest problem, I think, with understanding recursion -- if you don't understand the main body of the program, something is definitely wrong with you! :> Line #'s are placed in {}'s above this paragraph, and below this paragraph. { call to factorial } {1} a = 4 c = 1 {2} 4 > 1 = true {3} c = 4 * factorial(3); { call to factorial } {1} a = 3 c = 1 {2} 3 > 1 = true {3} c = 3 * factorial(2); { call to factorial } {1} a = 2 c = 1 {2} 2 > 1 = true {3} c = 2 * factorial(1); { call to factorial } {1} a = 1 c = 1 {2} 1 > 1 = false (so the chain of calls ends...) {3} skipped because 2 is false. {4} c = 1; factorial function is 1. { end call to factorial } {4} c = (2 * 1) = 2; factorial function is 2. { end call to factorial } {4} c = (3 * 2) = 6; factorial function is 6. { end call to factorial } {4} c = (4 * 6) = 24; factorial function is 24. { **FINAL** termination of factorial -- return of value 24 } If you check the code, the final value is correct. 4! = 24. Basically, with the layout I used, you can see especially also, why memory (stack space allocated for procedure and functions specifically) runs out quick, and why I say to keep the parameters and local variables for that matter to a minimum.... Recursion can be used in procedures, as well as functions, for any repetitive action. They are just like the function call above, which recursed when or until an action became true. To be able to extend for example, the part 8 dir clone to list and search for files (list all files in all dirs), we would need to add another boolean variable to get permission to run through all dirs encountered. We can re-call the directory list procedure with a regulating if variable of it being a directory. For more practice (I won't post the solution for these ones), you may wish to do this. As another practice, you may wish to try and recode an integer power function (the "simplistic" power function) that I included in my solution to part 7's programming problem to use recursion. Practice Programming Problem #13 ================================ Code a program in Pascal and entirely Pascal that will make a catalog of the additive size of all files in all dirs on a drive specified on the command-line to a text file named FILESLST.TXT. Sample output ------------- c:\>sizelist c: Drive: C C: 131,123 C:\DOS 5,231,131 ... ... C:\UTIL 3,212,985 C:\UTIL\ACAD 131,123 ============= 527,212,122 Notes: 1) The additive end of the listing (under the ='s) is the total size of the files on the drive. You may use the function offered by the DOS unit to check yourself in your addition. 2) The spacing is not exact above. Make it look SIMILIAR to what I have above, but make it reasonably attractive... 3) Use a forward. 4) Be sure to check to be sure the drive is specified EXACTLY like above. 5) Please use recursion for going through the drive (actually, recursion is probably the best way to do this). But, be sure you put the directories in the order listed above. 6) Sizes of subdirectories are not counted in the size of a main directory. 7) Be sure to error-check the command-line. Next Time ========= We will discuss the functions of the CRT unit. Be sure to write comments, encouragements, problems, errors, to ggrotz@2sprint.net. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 14: The CRT unit. copyright (c) 1995-96 by Glenn Grotzinger Hello. Here is a solution from last time... {$M 65520,0,655360} {$B+} program part13; uses dos; var drive, s: string; drivesize: longint; thefile: text; function number(n: longint): string; forward; procedure listdirsizes(filedir: string); var size: longint; f: string; dirinfo: searchrec; begin size := 0; f := filedir + '\*.*'; findfirst(f, $27, dirinfo); { allfiles - VolumeID - directory } while doserror = 0 do begin size := size + dirinfo.size; findnext(dirinfo); end; writeln(thefile, filedir, number(size):50-length(filedir)); drivesize := drivesize + size; findfirst(f, $10, dirinfo); while doserror = 0 do begin { for some reason, the findfirst above doesn't work right. } { Unable to determine exactly why...:< } if (dirinfo.attr = $10) and (dirinfo.name <> '.') and (dirinfo.name <> '..') then begin s := filedir + '\' + dirinfo.name; listdirsizes(s); end; findnext(dirinfo); end; end; function number; var i, j: integer; s, r: string; m: integer; begin str(n, s); r := ''; i := length(s); if i > 3 then begin while i > 3 do begin for j := i downto i-2 do r := s[j] + r; r := ',' + r; i := i - 3; end; for j := i downto 1 do r := s[j] + r; number := r; end else number := s; end; procedure writehelp; begin writeln('Include a drive like this: c:'); halt(1); end; begin assign(thefile, 'FILESLST.TXT'); rewrite(thefile); if paramcount <> 1 then writehelp; drive := paramstr(1); if (length(drive) <> 2) or (drive[2] <> ':') then writehelp; drive[1] := upcase(drive[1]); writeln(thefile, 'Drive: ', drive[1]); listdirsizes(drive); writeln(thefile, '===============':50); writeln(thefile, number(drivesize):50); close(thefile); end. In this part, we will try to discuss the complete usage of the CRT unit. The CRT unit will allow with text different things, such as PC speaker control, control of the screen for screen clears, positioning of the cursor, changing the color of text and background, and so forth. Basically, when the uses crt, or wincrt, is used, direct writes to the screen are initiated, and can not be redirected to text file. Boolean variables. ================== There is a list of boolean control variables accessible through CRT. CheckBreak. It is true by default. If it's false, CTRL+BREAK will be unfunctional. CheckEOF. It is true by default. If it's false, CTRL+Z when pressed will not represent an EOF in the program. CheckSnow. Enables and disables "snow checking" on CGA adapters. DirectVideo. Enables and disables direct video writes. Keypressed. Will return true if a key has been pressed. Usable in a loop for doing something until the user stops it. A function like this is similarly used for example in screen savers to stop the display. Here is a small example to see how it acts. program the_key_pressed; uses crt; begin repeat write(#254); until keypressed; end. This program should write ascii character #254 to the screen repeatedly until the user presses a key. Screen Manipulation Commands ============================ Clreol; Clears to the end of the line for the currently active window from the current cursor position. The concept of "currently active window" will be explained later. Clrscr; Clears the screen. Already used and demonstrated. Delline; Deletes the current line containing the cursor. Insline; Inserts line at current position. PC Speaker Sounds and Machine Delay =================================== These are the basics of controlling the PC Speaker to make sounds. The first thing is that there is an ASCII character #7 which will cause a standard DOS PC Speaker beep. Then there are some pascal procedures which will offer much more control. You will have to experiment with it using different numbers in the delay and sound value. Delay(milliseconds: word); will delay the program for approximately the number time in Milliseconds. Sound(hertz: word); will start up the PC speaker at a specified frequency. NoSound; will stop the PC speaker. Here is an example of use of the PC speaker through TP. program test_of_pc_speaker; uses crt; begin sound(1500); delay(500); nosound; end. Basically, we're emitting a sound of 1500 Hz, for a time of .5 seconds. Note: The delay procedure can be used anytime to "freeze" up the system for a specified period of time, say if we want to delay between writes. Screen Positioning ================== These is a command to control positioning of writing on the screen. gotoxy(x, y); is a procedure that will position the cursor at the point defined by x, y. The standard defintion of the screen positioning is a cartesian system like this for the standard video mode (on most systems when you boot them up). (1, 1)(2,1)----------------------------------------------... (x, 1) (1, 2) | (1, 4) | | | | | | ... (1, y) x and y is defined by the current video mode, which is settable. We will discuss how to set the current video mode. The standard video mode at bootup is 80x25. wherex is the current x position of the cursor, while wherey is the current position of the y variable. gotoxy(wherex, wherey+1) is valid as an example, and will move the cursor DIRECTLY down one unit, irreguardless of where it is (as long as x and y are valid, most of these commands are inactive if they get bad data) The view window can also actually be changed. The window function can be used for this purpose. The window is measured by the coordinate of the upper left hand corner of the window (x1, y1), and the lower right hand corner of the window(x2, y2). The syntax if we call window is window(x1,y1,x2,y2); This is one procedure that you really need to experiment with to see its effects. As a note, most, if not all of the screen writing commands are affected by this window... Changing Text Color, Appearance, and Mode ========================================= These are functions and procedures which control the appearance of text. HighVideo; will select high intensity characters. LowVideo; will select low intensity characters. NormVideo; will select the original intensity at startup. Textcolor(color); and Textbackground(color); are used to control the foreground and background color respectively. Foreground colors work from 0-15 and the background colors are from 0-8. Blinking foreground text adds 128 to the value. A list of text color constants which are equated with these numbers can be found on page 192 of the TP programmer's reference. LastMode is a word variable which will hold the current video mode that the system was in when the program started execution. Changing text mode actually involves changing what x and y as was originally stated in the description of gotoxy. textmode(modeconstant); actually does this. As with everything in this part, you should experiment with all of the commands and procedures defined, because you can't exactly be showed the effects of these commands in this tutorial. A list of mode constants can be found on page 26 of the TP programmer's reference. Here is a sample program which demonstrates several of the functions and procedures listed, and gives us an idea of what can be done with manipulation of text windows, colors, and so forth. Be sure to run this one, and experiment with changing some of the values and actions to see what happens. program demo_of_fun_text_manipulation; uses crt; var oldx, oldy: byte; begin { save original position of cursor so we can return there later } oldx := wherex; oldy := wherey; window(2,2,20,20); textbackground(Blue); clrscr; { blue background now b/c blue when clrscr was called } highvideo; window(10,10,15,15); textbackground(Green); clrscr; textcolor(LightCyan + blink); write('Hello, how are we doing today?'); window(1,1,80,25); gotoxy(oldx, oldy); end. Reading Characters from the Keyboard ==================================== You may have been wondering, how to directly read things from the keyboard. The CRT offers a function called readkey, which is a parameterless function that returns a character. Unlike read or readln, it does not require a CR to initiate. You may see this by replacing a character read from the keyboard somewhere in your program with something like this: answer := readkey; You will notice that you did not have to press ENTER to move on. Also, you will notice that the character didn't echo. Very useful for password applications. The next advantage of readkey is that it can be used to read about every key or key combination on the keyboard. For this tutorial, we will discuss simply the keys that can be accessed via one keypress using a basic standard to be discussed in the next paragraph. Extended keys, like the function keys, or the arrow keypad may be read using readkey. These keys generate a #0 character as their first character followed by an extended scan code. These vary depending upon which key is depressed. To read extended keys, we would have to use a structure such as this: char := readkey; if char = #0 then char := readkey; Most of the one press keys on the keyboard may be detected using this method. Examples of those keys which can't be detected using the method described by the snippet of code above are the shift keys, capslock, ScrollLock, NumLock, F11, F12, the CTRL keys, and the ALT keys. There are other methods available for those keys and key comb- inations. I recommend as practice that you set up a reader program that will tell you what the extended character codes are for each of the extended keys (any key not on the basic keyboard). Practice Programming Problem #14 ================================ Create a program in Pascal and entirely Pascal that will do the following: 1) Using normal text mode, set up a screen that has a green background all the way around the screen as a frame of 4 units in size all the way around. Inside of that, place a red frame of 1 unit all the way around. 2) Inside all of the frames described in part 1, set up a title that states "Keypress Detector" on the first line available below the red frame, in an high-intensity light cyan. Be sure this title is NEVER overwritten by scrolling text. 3) The "Keypress Detector" title should be on a black background. Also, the text which shows what keys we have pressed are should be a dull lightblue on a black background. The exception to the last statment is that any standard key identity in the statement should be in a bright green, and any extended key identity in the statment should be in a bright red. Key identity will be evident as I give a sample. (properly inside the "frame") Keypress Detector (press ESC 5 times to terminate) You pressed the F1 key. You pressed the SPACE key. You pressed the LEFT key. You pressed the Backspace key. Note: Some of the keys on the keyboard are singular control characters. Look at your ASCII chart. For example, the TAB key is actually ASCII character #9, and character #9 should be recognized as such... F1, SPACE, LEFT, and BACKSPACE are the key identities as I described earlier... 4) Make sure that in order to terminate the program, the ESC key gets pressed 5 times, not consecutively (be sure to indicate the ESC key has been pressed each time). Upon termination, the entire screen should visibly scroll up until none of what we wrote is visible anymore (clue: use delay). Next Time ========= We will discuss the usage of the QuickSort and ShellSort algorithms. Please by all means send comments to ggrotz@2sprint.net. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 15: Concepts of Sorting Arrays copyright (c) 1995-96 by Glenn Grotzinger Here is a solution from last time...keep in mind to be as efficient as possible in coding...I figure that many will try simply writing those frames out. program part14; uses crt; var esccount: byte; chr: char; procedure setitup; begin { green border of 4 units } textbackground(green); clrscr; window(5,5, 76, 21); { red border of one unit } textbackground(red); clrscr; window(6,6,75,20); { set up black background } textbackground(black); clrscr; { write keypress statement } highvideo; textcolor(lightcyan); writeln('Keypress Detector (Press ESC 5 times to quit)'); { preserve Keypress detector title. } window(6,7,75,20); end; function status(char1: char;var esccount: byte): string; var char2: char; begin if char1 = #0 then begin char2 := readkey; case char2 of #59: status := 'F1'; #60: status := 'F2'; #61: status := 'F3'; #62: status := 'F4'; #63: status := 'F5'; #64: status := 'F6'; #65: status := 'F7'; #66: status := 'F8'; #67: status := 'F9'; #68: status := 'F10'; #82: status := 'Insert'; #71: status := 'Home'; #73: status := 'PageUp'; #81: status := 'PageDown'; #83: status := 'Delete'; #79: status := 'End'; #77: status := 'Right'; #72: status := 'Up'; #80: status := 'Down'; #75: status := 'Left'; end; end else case char1 of #8: status := 'Backspace'; #9: status := 'TAB'; #13: status := 'ENTER'; #27: begin status := 'ESC'; inc(esccount, 1); { the 1 is not required here, but the inc and dec functions can be used with values greater than one like this in this case } end; #32: status := 'SPACE'; else status := char1; end; end; procedure endit; var i: byte; begin window(1,1,80,25); gotoxy(1,1); for i := 1 to 25 do begin delline; delay(100); end; end; begin esccount := 0; setitup; while esccount <> 5 do begin chr := readkey; normvideo; textcolor(lightblue); write('You pressed the '); highvideo; textcolor(green); write(status(chr, esccount)); normvideo; textcolor(lightblue); writeln(' key.'); end; endit; end. Now, we will discuss the use of sorting with regards to arrays into a particular order. Sometimes we may need numbers, such as dates in chronological order, or a list of names in alphabetical order. Swapping Units ============== The integral part of a sorting routine is a unit swap. As a rule, we MUST have a temporary variable, because a simple assign set will not work. For example, to swap the contents in variables a and b, we need a temporary variable we will call temp. Then code such as what is below will do... temp := b; b := a; a := temp; The swap should ideally be performed with the smallest units possible, based on the sorting key. The idea of a sorting key will be explained later. You may end up using pointers to get it to move the smallest amount of data, which will be explained in a future part. The less amount of data the computer can move, the better. The BubbleSort (or the brute force sort) ======================================== Basically, in this sorting method, each and every item in the array is compared each and every other item in the array. This is a largely inefficient method for sorting items, but easy to code, and useful for small amounts of data. program example_of_bubblesort; var thearray: array[1..20] of integer; temp: integer; i, j: integer; begin randomize; { generate numbers for array and write them as unsorted } write('The unsorted array: '); for i := 1 to 20 do begin thearray[i] := random(50) + 1; write(thearray[i], ' '); end; writeln; { the bubblesort. } for i := 1 to 20 do for j := i+1 to 20 do if thearray[i] > thearray[j] then { compare and swap } begin temp := thearray[i]; thearray[i] := thearray[j]; thearray[j] := temp; end; write('The sorted array: '); for i := 1 to 20 do write(thearray[i], ' '); writeln; end. As it is a purely iterative solution, you should have no exact trouble seeing what is going on. But to further another point as to exactly how it works, and why it is so inefficient, we will sort a sample set of numbers manually according to this algorithm to see what is going on. 1 3 2 5 4 We will start out with the following short description of what is going on within the two for loops for 5 values of data: 1) i = 1; j = 2; Position1 = 1; Position2 = 3; 1 > 3 = false; 2) i = 1; j = 3; Position1 = 1; Position3 = 2; 1 > 2 = false; 3) i = 1; j = 4; Position1 = 1; Position4 = 5; 1 > 5 = false; 4) i = 1; j = 5; Position1 = 1; Position5 = 4; 1 > 4 = false; 5) i = 2; j = 3; Position2 = 3; Position3 = 2; 3 > 2 = true; we swap the two values...so our resultant array is... 1 2 3 5 4 6) i = 2; j = 4; Position2 = 2; Position4 = 5; 2 > 5 = false; 7) i = 2; j = 5; Position2 = 2; Position5 = 4; 2 > 4 = false; 8) i = 3; j = 4; Position3 = 3; Position4 = 5; 3 > 5 = false; 9) i = 3; j = 5; Position3 = 3; Position5 = 4; 3 > 4 = false; 10) i = 4; j = 5; Position4 = 5; Position5 = 4; 5 > 4 = true; we swap the two values...so the resultant array is... 1 2 3 4 5 11) Effective termination of loop; As we can see, we took 10 steps in the algorithm to swap 2 elements of the array in order to sort it. Basically, this is a very inefficient algorithm in comparison to other types that are available, as we are considering portions of the array that are already sorted. As a note, to sort the items in descending order instead of ascending order, change the comparison between the two positions i and j of the array from > to <. QuickSort ========= This is a much faster recursive solution for sorting than the bubblesort. It makes use of a pivot marker, which moves according to what exactly is contained in the array. It also will make use of a "divide and conquer" approach. Here is a short example... program example_of_quicksort; var thearray: array[1..20] of integer; i, j, PIVOT, t: integer; procedure quicksort(L, R: integer); begin if L < R then begin i := L + 1; j := R; PIVOT := thearray[L]; repeat while thearray[i] <= PIVOT do inc(i); while thearray[j] > PIVOT do dec(j); if i < j then begin t := thearray[i]; thearray[i] := thearray[j]; thearray[j] := t; end; until i > j; thearray[L] := thearray[j]; thearray[j] := PIVOT; quicksort(L, j-1); quicksort(i, R); end; end; begin randomize; write('The unsorted array is: '); for i := 1 to 20 do begin thearray[i] := random(50) + 1; write(thearray[i], ' '); end; quicksort(1, 20); write('The sorted array is: '); for i := 1 to 20 do write(thearray[i], ' '); end. This is a recursive solution, so it will be a little different. Let's start with the same number sequence we had above and see how quicksort works...keep in mind that quicksort is about as inefficient as bubble- sort with smaller sets...Once you get into larger sets, quicksort beats bubblesort hands down -- it more intelligently seeks the proper array units to swap. 1 3 2 5 4 1) 1 < 5 = true so continue.... i = 2; j = 5; PIVOT = 4; 3 <= 4 = true so i = 3; 2 <= 4 = true so i = 4; 5 <= 4 = false so quit; 4 > 5 so quit; 4 < 5 = true, so swap values. The resulting swap is... 1 3 2 4 5 4 > 5 = false so continue on repeat loop... i = 4; j = 5; PIVOT = 4; 4 <= 4 = true so i = 5; 5 <= 4 = false so quit; 5 > 4 = true so j = 4; 4 > 4 = false so quit; 5 > 4 = true so quit repeat loop. j = 4; i = 5; 4 is left side. 4th element is 4. 2) Quicksort called for left side of 1, and right side of 3. Then quicksort for left side of 5, and right side of 5. Essentially, we keep cutting the array in half based by where the pivot lands. We will process the quicksort for the left side as 2a) and the quicksort for the right side as 2b). 2a) Our number set for this instance of quicksort is: 1 3 2 1 < 3 = true so continue. i = 2; j = 3; PIVOT = 2; 3 <= 2 = false so quit; i = 2; j = 3; PIVOT = 2; 2 > 2 = false so quit; 2 < 3 = true so swap values...the resulting swap is... 1 2 3 2 > 3 = false so quit repeat loop. Quicksort called twice for left side of 1, 2 and 2,3. The results of both of these sorts end up being false, so they will terminate readily. 2b) 5 < 5 = false so terminate quicksort. As we can see, the algorithm sets itself up so it ignores portions of the array that are already sorted. For larger arrays, it will provide a great performance boost as we are ignoring the parts of the array that happen to be sorted by using this particular algorithm. The version of quicksort pictured sorts in ascending order. To make it sort in descending order, change the two while loops to read the following: while thearray[i] <= PIVOT do inc(i); while thearray[j] > PIVOT do dec(j); ShellSort ========= This is a non-recursive sort that performs close in performance to quicksort. We can follow what is going on, so I will just simply write an example of the use of the shellsort, and describe how to change it to sort in descending order instead of ascending order... program example_of_shellsort; var thearray: array[1..20] of integer; i: integer; procedure shellsort(n: integer); const m = 3; { total number of sort passes } var i: array[1..m] of integer; j, k, p, s, t, incr: integer; begin i[m] := 1; for j := m - 1 downto 1 do i[j] := 2 * i[j]; for j := 1 to m do begin incr := i[j]; for k := 1 to incr do begin s := incr + k; while s <= n do begin p := s; t := thearray[p]; thearray[k-incr] := t; while t < thearray[p-incr] do begin thearray[p] := thearray[p-incr]; dec(p, incr); end; thearray[p] := t; inc(s, incr); end; end; end; end; begin randomize; write('The unsorted array is: '); for i := 1 to 20 do begin thearray[i] := random(50) + 1; write(thearray[i], ' '); end; writeln; shellsort(20); { 20 is high end of array } write('The sorted array is: '); for i := 1 to 20 do write(thearray[i], ' '); writeln; end. To get it to sort in ascending order instead of descending order, change the line: while t < a[p-inc] do to while t > a[p-inc] do Practice ======== You should practice using these sorting methods. They stay basically as indicated for any use, except for changing the identities of the item type in the array we sort. For strings, we can alphabetize them by using the strings in the sorting routine for ascending order. Look at the ASCII chart...It sees characters as numbers... a < b < c ...et al... With strings, be sure to sort them case insensitively, especially, if we alphabetize a list or something like that.... Next Time ========= We will cover methods of searching an array for data. send comments to ggrotz@2sprint.net Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 16: Searching an Array copyright(c) 1995-96 by Glenn Grotzinger There was no defined problem in part 15, so we will start. Sequential Array Search ======================= This method for searching arrays for items is much like the bubblesort is for sorting arrays. It works well for small arrays, but for larger arrays, it is very inefficient, if we are merely interested in the existence of an item in the array. Basically, it's a very simple proposition to set up a sequential array search. program example_of_sequential_search; var a: array[1..15] of integer; i, number: integer; found: boolean; begin randomize; found := false; write('The array is: '); for i := 1 to 15 do begin a[i] := random(10) + 1; write(a[i], ' '); end; writeln; writeln('Enter a number, and we shall see if it is in the array'); readln(number); i := 1; while i <= 15 do begin if a[i] = number then begin writeln(number, ' was found at position ', i); { i := 15; can be done to break the loop on the first encounter of the one if we are interested in just whether the number exists in the array } found := true; end; inc(i); end; if not found then writeln(number, ' was not found in the array.'); end. Binary Search ============= The other type of algorithm we will discuss is the binary search. It is to searching an array what the quicksort is to sorting an array. Basically, what it will do is keep halfing the array... This method of array search has a prerequisite of having the data sorted. Basically, we probably would want to sort data anyway to make it in a readily presentable format for the user, so this doesn't necessarily matter (searches and sorts are products of data processing programs normally, anyway). program example_of_binary_search; const first = 1; last = 15; var a: array[first..last] of integer; i, number: integer; found: boolean; location: integer; procedure bsearch(var number, location: integer; lowend, highend: integer; var found: boolean); var midpoint: integer; begin if lowend > highend then found := false else begin midpoint := (lowend + highend) div 2; if number = a[midpoint] then begin found := true; location := midpoint; end else if number < a[midpoint] then bsearch(number, location, lowend, midpoint-1, found) else if number > a[midpoint] then bsearch(number, location, midpoint+1, highend, found); end; end; begin randomize; found := false; write('The array is: '); for i := 1 to 15 do begin { to insure we have sorted data } a[i] := 3*i; write(a[i], ' '); end; writeln; writeln('Enter a number, and we shall see if it is in the array'); readln(number); bsearch(number, location, first, last, found); if found then writeln(number, ' was found at position ', location) else writeln(number, ' was not found. '); end. As you may be able to pick out of this, it is possible to write an iterative version of the bsearch procedure. With sorting the array, though, this one is a little better than simply going through the array one by one, but it still has it's drawback of having to actually sort the information. Another method available for use, which we will not discuss here is called hashing, which is a very complex matter. What should you use? The serial search I described originally could be very good, for a limited number of searches on a small array size. If it's not good to do the serial search, and you needed to sort the data anyway, the binary search is best. Another method you may see as possible to use will be covered later, called the binary search tree. The cost in that approach will be basically building the tree initially, then traversing it. The tree is built based on specific rules, which we can exploit to cause a search. Practice Programming Problem #16 ================================ Randomly generate 1000 numbers from 1-10000 into an array. Then generate another 500 numbers from 1-10000. If there is a number from the second set of numbers that happens to be in the first set of numbers, write out to the file named LOCATION.TXT something such as: 5322 was found in position 532. Only indicate the first instance of the number you encounter. Next Time ========= We will cover some basic concepts of the use of pointers. Please write any comments you have to ggrotz@2sprint.net. As I look at my formatted version, so far there has been 116 pages written of this tutorial, from part 1, not counting this one. As I look through my line count figures, this one is the smallest. (interesting facts, huh?) I will ask for feedback on how I do with regards to any of the pointer related texts coming up. Please do that. Thank you. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 17: Primary Concepts of Pointers copyright(c) 1995-96 by Glenn Grotzinger Hello. Here is a solution from last time... program part16; var a: array[1..1000] of integer; number: integer; c, i, j, PIVOT, t: integer; found: boolean; location: integer; outfile: text; procedure quicksort(L, R: integer); { nothing to say we couldn't sort the data, especially with the fact that we will be performing 500 search hits on this 1000 unit array...the overhead of sorting the data will be of good benefit in the long run... } begin if L < R then begin i := L + 1; j := R; PIVOT := A[L]; repeat while a[i] <= PIVOT do inc(i); while a[j] > PIVOT do dec(j); if i < j then begin t := A[i]; A[i] := a[j]; A[j] := t; end; until i > j; a[l] := A[j]; a[j] := PIVOT; quicksort(L, j-1); quicksort(i, R); end; end; procedure bsearch(number, lowend, highend: integer; var location: integer; var found: boolean); var midpoint: integer; begin if lowend > highend then found := false else begin midpoint := (lowend + highend) div 2; if number = a[midpoint] then begin found := true; location := midpoint; end else if number < a[midpoint] then bsearch(number, lowend, midpoint-1, location, found) else if number > a[midpoint] then bsearch(number, midpoint+1, highend, location, found); end; end; begin randomize; assign(outfile, 'LOCATION.TXT'); rewrite(outfile); for c := 1 to 1000 do a[c] := random(10000) + 1; quicksort(1, 1000); for c := 1 to 500 do begin number := random(10000) + 1; bsearch(number, 1, 1000, location, found); if found then writeln(outfile, c, ') ', number, ' was found at position ', location, '.'); end; close(outfile); end. Intro ===== This will be the first of several parts on the use of pointers...This part will contain the basic idea behind pointers and their use. I would like feedback from anyone about how well they felt they learned from this tutorial, and the next two. They all will be about pointers, and I want to be sure I am doing OK in explaining them coherently. :> The Concept of a Pointer ======================== A pointer is simply what it implies by the name. Something that points. A pointer is a 4 byte double word that stores the segment and offset in memory where the variable is located. We have referred to before a method of getting more than 64K in a data structure, in the array section. This is a way. A pointer is placed on what is referred to as the heap. The heap is the remainder of conventional memory not used by the program itself, the data stack, or any TSRs that may be on the system at any one time. The data stack is where variables are normally allocated directly by name and can be a maximum of 64K in size. Why would we want to use pointers? Well, we have already eluded to one reason, to get around the 64K data limit. Another reason is to dynamically allocate memory space. Basically, any variables declared by name(s) in a program is allocated at the initial start-up of a program, and is allocated until the end of the program. For a lengthy program with a lot of variables, we may not want all variables to be allocated for something at the start. Therefore, we can, with pointers, allocate and deallocate memory anytime we wish in a program. If we want to save memory in the data stack, 1 4-byte pointer compared to, say, an array of 1000 integers would be preferable. That is one function of a pointer. In using pointers, we must remember that they are not direct variables, but addresses of variables. Let us look at the first example program below to see exactly what I mean by the last statement. We also will see how to properly create variable space address pointers in memory, address that variable space, and then deallocate it. program pointers_one; type strptr = ^string; var a, b, c: strptr; begin new(a); a^ := 'Turbo Pascal'; new(b); b^ := 'is fun!!!!!!'; writeln('a is now "', a^, '".'); writeln('b is now "', b^, '".'); new(c); c^ := a^; a^ := b^; b^ := c^; dispose(c); writeln; writeln('a is now "', a^, '".'); writeln('b is now "', b^, '".'); dispose(a);dispose(b); end. We see the use of several use conventions in the program above. We have three pointers listed to be pointers of strings. (as a note, any data type that will eventually be a pointer should NORMALLY be defined under the type declaration to facilitate the use of pointers in procedures. We will see this later.) In the declaration to allocate the pointers, we used the ^ sign first, then the datatype. We say that the pointer will eventually point to a string variable. To address each of the pointer variables, we used the ^ sign after the particular variable name. The ^ sign references the variable that the pointer is pointing to, and NOT the pointer itself. You may see this by doing a debug trace for a, b, and c on that program. You will clearly see that the pointer's contents is NOT what the variable's contents is... The next thing to note is the assignment statements. For a pointer variable, we are not assigning direct values, but MOVING THE POINTER address. When we say a^ := c^, we are moving the pointer c to point to the value that a points to. **WE ARE NOT DIRECTLY CHANGING THE VALUE OF THAT LOCATION IN MEMORY!!!**. The last thing to note is the strategically placed uses of the functions new() and dispose(). These are new functions to us that we will learn and make use of in the system unit. The new() function will allocate the space for the pointer listed in the variable on the heap. The dispose() function will deallocate the space for the pointer on the heap. As you can see, we created and removed the variables as we needed them. The next thing you were probably wondering with pointers is how to get a pointer to not point to anything. Make it address a reserved word called nil. If I want pointer p to not point to anything, then I would write: p := nil; Whatever you do, do not set a pointer to nil before you dispose of the pointer. The system will lose track of the variable on the heap, and therefore is LOST to the system. Keep this issue in mind as we do our work and examples with pointers. Look up getmem() and freemem(). Another issue that wasn't covered above is how we determine how much memory is in the heap. Use the functions memavail and maxavail for the purpose of finding out how much memory you can allocate and if the pointer data structure is big enough, BE SURE TO CHECK AND SEE IF YOU HAVE THE MEMORY TO ALLOCATE before you allocate it for a big structure. Procedure or Function Pointers ============================== A procedure or function may also be addressed as a pointer. For that, as you will see in the example, we can use a declaration for a variable exactly like the procedure or function declaration. It is very useful to minimize code usage, and make things multi-functional. This sheds some light into the inner workings of the write command, as was wondered in c.l.p.b. a little while ago... program pointer_two; type compfunc = function(a, b: integer):integer; var compute: compfunc; first, second: integer; choice: char; {$F+} function compadd(a, b: integer): integer; begin compadd := a + b; end; function compsub(a, b: integer): integer; begin compsub := a - b; end; function compmult(a, b: integer): integer; begin compmult := a * b; end; function compdiv(a, b: integer): integer; begin compdiv := a div b; end; {$F-} begin @compute := nil; write('Enter a first number: '); readln(first); write('Enter a second number: '); readln(second); write('Enter +, -, *, or / as a operation: '); readln(choice); case choice of '+': compute := compadd; '-': compute := compsub; '*': compute := compmult; '/': compute := compdiv; else writeln('Enter a correct option.'); end; compute(first, second); writeln(first, ' ', choice, ' ',second,' = ',compute(first, second)); end. As you can see, we are assigning a particular function to the function named compute. After that, compute performs the specified function of the function that we just assigned to it. The rule is that those FAR declarations MUST be present. As well, you can see how to refer to the procedure pointer (as @procedure). As long as the declarations are similar for all the functions, we can readdress functions/procedures using a variable like we did above. We see that we can probably cut down on a lot of code by using this method if we had several different ways of doing things, with large amounts of code for each thing. For example, if we wanted to sort something based on user input dependent upon many different methods or types (say sort increasing by name, decreasing by name, increasing on size, by date, et.al), we can set up our swapping procedure to be the way the compute procedure is above. Now, we will lead into a special usage of a pointered procedure called exitproc. ExitProc ======== This is a special defined procedure pointer in Pascal that will determine the procedures that will be performed upon an exit, NO MATTER what happens, EVEN IF THERE IS A RUN-TIME ERROR. Here is your method on being able to catch and log those run-time errors either to the screen, or to an errors log. The primary usage of an exit procedure, though, is to perform any maintenance that needs to be done upon termination that may not get done upon an abnormal termination, such as closing files. The way things work are basically the same as above... Here is a short example...I will force a run-time error in this program, so we can see that the end gets run anyway. As you may see, exitproc is defined as a pointered procedure when we work with it. The rest is basically documented. program pointer_three; var exitsave: procedure; afile: text; {$F+} { must be far } procedure myexit; {must be parameterless} begin if exitcode <> 0 then writeln('There was a problem.'); writeln('We are exiting...'); end; {$F-} begin @exitsave := exitproc; { saving the original exit procedure } exitproc := @myexit; { set to new exit procedure } { I am placing a couple of error situations in here so you can experiment with what is going on...try it without errors too! } {1 - here is a standard file not found RTE.} assign(afile, '()()()().!!!'); reset(afile); {2 - here is a division by zero.} { writeln(3 / 0); } exitproc := @exitsave; { set the original exit procedure back } end. Here is enough material that I believe that is enough to digest for right now on use of pointers. Practice allocating and using pointers very heavily, as we will get into more advanced issues of using pointers in the next two parts...Remember that ANY structure can be placed as a pointer, except for files, and remember to define types for all pointers. Practice Programming Problem #17 ================================ Randomly generate 15000 numbers from 1-25000 into an array. Then generate another 10000 numbers from 1-25000. If there is a number from the second set of numbers that happens to be in the first set of numbers, write out to the file named LOCAT2.TXT something such as: 13131 was found at position 12000. Only indicate the first instance of the number you encounter. You may wish to write a "redrawing bar" for a process indicator. Note: Be sure with the allocation for those first 15000 numbers that you keep in line with the topic of this tutorial. Next Time ========= We will cover the concept of linked lists, or chained lists. Practice very heavily the idea of pointers, because you will need use of them again in the next section. E-mail ggrotz@2sprint.net with your comments. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 18: Chained or Linked lists, the linked list sort copyright(c) 1995-96 by Glenn Grotzinger Here is a solution from last time... {$M 64000,0,655360} program part17; uses crt; type arptr = ^atype; atype = array[1..15000] of integer; var a: arptr; number: integer; c, i, j, PIVOT, t: integer; found: boolean; location: integer; outfile: text; procedure quicksort(L, R: integer); { nothing to say we couldn't sort the data... } begin if wherex = 79 then begin gotoxy(1, wherey); clreol; end else write(#254); if L < R then begin i := L + 1; j := R; PIVOT := A^[L]; repeat while a^[i] <= PIVOT do inc(i); while a^[j] > PIVOT do dec(j); if i < j then begin t := A^[i]; A^[i] := a^[j]; A^[j] := t; end; until i > j; a^[l] := A^[j]; a^[j] := PIVOT; quicksort(L, j-1); quicksort(i, R); end; end; procedure bsearch(number, lowend, highend: integer; var found: boolean); var midpoint: integer; begin if lowend > highend then found := false else begin midpoint := (lowend + highend) div 2; if number = a^[midpoint] then begin found := true; location := midpoint; end else if number < a^[midpoint] then bsearch(number, lowend, midpoint-1, found) else if number > a^[midpoint] then bsearch(number, midpoint+1, highend, found); end; end; begin if maxavail - sizeof(a) > 0 then new(a) else begin writeln('Out of memory!'); halt(1); end; randomize; assign(outfile, 'LOCAT2.TXT'); rewrite(outfile); for c := 1 to 15000 do a^[c] := random(25000) + 1; quicksort(1, 15000); for c := 1 to 15000 do begin number := random(25000) + 1; bsearch(number, 1, 15000, found); if found then writeln(outfile, c, ') ', number, ' was found at position ', location, '.'); end; dispose(a); close(outfile); end. Now we will discuss the idea of the linked list or chained list. Basically, there are 4 types of linked lists that we can discuss, the singularly linked linear list (SLLL), singularly linked circular list (SLCL), doubly linked linear list (DLLL), and the doubly linked circular list (DLCL). I will use the abbreviations I placed in the parentheses for any future references to these data types. These are basically the preferred ways to allocate large amounts of storage space on the heap. All linked lists are basically describable in the best analogy as a chain. They are record structures which have pointers that interconnect them. The method that these structures are connected distinguish the type of linked list it is. We will look at an example of the use of SLLL's, observe the advantages of linked lists through what we do with the example, and study the things to look out for on all 4 types. SLLL Concepts ============= This is the simplest type, in sense. It involves a record structure which is connected in a chain in a linear fashion with one link forward to the next link. A sample record structure for an SLLL follows below. type nodeptr = ^node; nodetype = record ourinfo: integer; nextnode: nodeptr; end; An example of an SLLL conceptually is something like this: NODE-->NODE-->NODE-->NODE-->NODE-->NODE-->NODE-->nil As we remember from earlier, nil is what we set a pointer to, if we do not want it to point to anything...In the use of an SLLL, it is also what we will use to indicate whether we are at the end of the list or not. We will see from the slll_demo program that there are several specialized issues we need to take into consideration with working with any linked or chained list. 1) We need to make a special case to insert or delete a node from the start of the list. 2) We need to be sure to maintain nil pointers in any insert or delete operation. 3) NEVER NEVER NEVER WORK DIRECTLY WITH THE HEAD TRACKING POINTER WE ORIGINALLY ALLOCATE UNLESS WE DESIGN OUR CODE COMPLETELY AROUND RECURSION. As a result, you will cause what is called a heap leak. This is where the pointer loses track of where the data it points to is stored. Logically, looking at the model above, if we disconnect one of the pointers, represented by the arrows, we lose track of the rest of the list, or chain. Work with a temporary pointer for each linked list function. What I say by recursion, you will see later in this document. 4) With regards to the example I wrote, I tried to demonstrate any and all functions that we might need with an SLLL. 5) Pointers that point at nil CAN NOT be deallocated. You will see this fact manifest itself by the memory statement at the end being 8 bytes smaller than it was at the start. SLLLs Demonstrated ================== Here is the SLLL_DEMO program. I will place stop notes in there, as well as comments. Advantages of linked lists: We will see here, that the data is not static, we can place data independently at different positions WITHOUT shuffling data, remove data in the same fashion, and definitely are capable of handling *A LOT* more data than 64KB, since we only have a 4 byte stub in that area. Take a good look at this program and seek to understand EXACTLY how it works. As you will remember from last time, a direct assignment to a pointer is making it point to something while a reference to the pointer (via ^) changes the contents of the data it points to. I recommend you draw out what is going on via pencil and paper, using boxes to represent the records and arrows representing pointers. It will help you VASTLY to do this in understanding what is going on. Remember a pointer can only point to one thing at a time. When you look at this program, seek to answer the following questions taking any "housekeeping functions" out of consideration: 1) Why is the insert code different than the build code? 2) Why is the delete code different than the cleanup code? 3) On the "divisible by 8" search, why is the NEXT node being searched for this and not the current node? 4) Why did I say to always use a temporary variable? Or Why does the statement p := list; always occur? 5) Observe methods of moving through the list. program slll_demo; uses crt; { Program written by Glenn Grotzinger for a demonstration of all of the functions/uses of a linked list that the author could think of. the variable used throughout called p, and sometimes t, are temporary variables. Note: This probably isn't completely optimized. } type nodeptr = ^nodetype; nodetype = record thenum: integer; nextnode: nodeptr; end; var list: nodeptr; procedure buildlist(var list: nodeptr); { This procedure builds up the list for us. } var p: nodeptr; i: integer; begin new(list); { This is creating the head of the list } list^.thenum := 1; p := list; { Set and move temporary pointer } for i := 2 to 18 do begin new(p^.nextnode); p^.nextnode^.thenum := i; p := p^.nextnode; { p := p^.nextnode advances the temporary pointer to the next node. this is a memory storage address or pointer and not a direct variable, referencing a node of the linked list. Anything, in reality does not become a pointer until the new function is used. } end; p^.nextnode := nil; { set last pointer to nothing } end; procedure writelist(list: nodeptr); { This procedure serves the function of writing out the list for us to the screen when called } var p: nodeptr; begin p := list; while p <> nil do { while we're not at the end of the list } begin write(p^.thenum:3); p := p^.nextnode; end; end; procedure insert(var list: nodeptr); { This procedure will serve to insert a node into the list either in the middle or the end. The logic can be done for the head of the list. } var p: nodeptr; begin new(p); p^.thenum := 20; p^.nextnode := list; if p^.nextnode = nil then { maintenance of the end of list marker } p^.nextnode^.nextnode := nil; list := p; end; procedure delete(var list: nodeptr); { This is a procedure that will serve to delete a node from the list, and consequently deallocate the memory. It is possible to remove the node without deallocating the memory, though it is a bad practice to do so } var p, t: nodeptr; begin p := list; t := p^.nextnode^.nextnode; dispose(p^.nextnode); p^.nextnode := t; end; procedure insertbythree(var list: nodeptr); { This procedure moves through the linked list and determines where the new nodes needs to be inserted, then calls the insert function written before } var p: nodeptr; i: integer; begin p := list; i := 1; while p <> nil do begin p := p^.nextnode; inc(i); if i mod 3 = 0 then insert(p^.nextnode); end; end; procedure findanddispose(var list: nodeptr); { This procedure finds and disposes the first number in the list divisible by 8. } var p, t: nodeptr; begin p := list; while (p^.nextnode <> nil) and (p^.nextnode^.thenum mod 8 <> 0) do p := p^.nextnode; delete(p); end; procedure cleanup(var list: nodeptr); { This procedure removes the list from memory. } var p, t: nodeptr; begin p := list; while p <> nil do begin t := p^.nextnode; dispose(p); p := t; end; end; begin clrscr; writeln;writeln; writeln('Free memory available: ', memavail, ' bytes.'); buildlist(list); writeln('Free memory available: ', memavail, ' bytes.'); write('The list is: '); writelist(list); writeln;writeln; writeln('Now we will insert a 20 in every third position'); insertbythree(list); writeln('Free memory available: ', memavail, ' bytes.'); write('The list is: '); writelist(list); writeln;writeln; write('Now we will search for and take the first # divisible by 8 '); writeln('out of the list.'); findanddispose(list); writeln('Free memory available: ', memavail, ' bytes.'); write('The list is: '); writelist(list); writeln;writeln; writeln('Now we will be good little programmers and clean up our list. :)'); cleanup(list); writeln('Free memory available: ', memavail, ' bytes.'); end. Hopefully, you can go through here, and follow the logic (actually, you will need to do that successfully to understand what is going on). Linked lists are very modular in nature. Therefore, a good understanding of what is going on here is essential. As a proof to be able to think through the logic of using pointers in linked structures, write out and logically explain on your sheet of paper how to perform the following (I will not provide a solution for this one -- code it up yourself and try and figure it out -- this is an important skill you will need to start developing as a programmer at this point, since you're at a pretty advanced level now (:))), and then code it up as a program: 1) Create 1000 nodes in an SLLL that consists of integers numbered from 1 to 1000. 2) Print this list to a text file. 3) Reverse the direction of the linked list. By doing this, I mean, instead of the linked list looking like this conceptually: NODE-->NODE-->NODE-->nil make it look like this: nil<--NODE<--NODE<--NODE DO NOT CREATE ANOTHER LINKED LIST IN MEMORY. USE THE CURRENT ONE YOU HAVE BUILT. 4) Print the new list to the same text file. Instead of it being from 1 to 1000 as the first printing was, it should be from 1000 to 1. CLUE: Think about how many temporary variables you will need (1? Maybe 2?, Possibly 3?). SLCL Concepts ============= This is essentially the same as an SLLL, except instead of being nil at the end of the list, the end of the list points at the beginning of the list. This type uses the same record format as the SLLL. Conceptually, an SLCL looks like this: NODE--->NODE--->NODE--->NODE--->NODE ^ | | V NODE NODE ^ | | V NODE<---NODE<---NODE<---NODE<---NODE As before with the SLLL, one of these nodes would be denoted as the head of the list. The only consideration that would differ that I could note, is that you would use a comparison of your temporary pointer with your head pointer in order to move through the list. So instead of while p <> nil, it would be while p <> list in the above example to make it that way. DLLL Concepts ============= This type of linked list uses a different kind of record format. It looks like this: type nodeptr = ^node; node = record ourinfo: integer; lastnode, nextnode: nodeptr; end; Conceptually, a DLLL looks like this: nil <-- <-- <-- <-- NODE NODE NODE NODE --> --> --> --> nil If you study up your logic from previously, this one shouldn't be too awfully bad to figure out. DLCL Concepts ============= This type of list uses the same record format as the DLLL. The conceptual diagram looks much like the SLCL diagram, but with double links much like the DLLL diagram, instead of single links. Final Thoughts on Linked Lists ============================== I did not provide examples of SLCLs, DLLLs, and DLCLs , merely for space, and also by the fact that I have never had reason to use the other three types. I am presenting their basics here, merely for people's study, and learning. Using the knowledge learned from doing those logic problems presented in the SLLLs, and references (though I find those to be VERY sparse on the types other than SLLL), you should be able to come up with the code to do the other three types pretty readily. Always remember that the best thing to do to work out the logic of what to do with the pointers is to draw it out using the boxes and the arrows. An Idea on Sorting Data Using Linked Lists ========================================== Here, I will now present the reasoning behind my "recursion" statement, plus an idea of sorting data upon build. I don't have any stats on this being more or less efficient than using one of the array sorts, but if you can't use an array to sort in memory, you would have to resort to this. Here is a little code/pseudocode (with a bent toward sorting names alphabetically) For purposes of the recursion, we will call the function INSERT: IF WE ARE TO PUT NODE HERE GET DATA TO PUT INTO NODE (read data from file, or elsewhere) WHILE DATA IS NOT DONE DO BEGIN IF NIL LIST THEN PUT NODE HERE ELSE PUT NODE HERE = NEWNAME <= NODE^.NAME IF WE ARE TO PUT NODE HERE THEN BEGIN new(p); SET DATA TO NODE p^.nextnode := LIST; if p^.nextnode = nil then p^.nextnode^.nextnode = nil; list := p; END ELSE INSERT(LIST^.NEXTNODE); IF WE ARE TO PUT NODE HERE THEN BEGIN GET INFO. DO NOT PUT NODE HERE (boolean variable set to false). END. END. This general code does work for a high capacity. I have used this code to sort a maximum of an 86KB list of 150 char items per line alphabetically using memory alone, no disk swapping. For practice: Do things as I have suggested throughout this document. No real practice problem. Next Time ========= We will talk about binary trees. Be sure to send comments to ggrotz@2sprint.net. I will say again that I apologize for the long period of time it took to get this out. I also apologize for the length this document has become. Be sure to please comment on how this part is. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 19: Binary Trees copyright (c) 1995-96 by Glenn Grotzinger OK. I stated no real problem that needed solving earlier, so we will start getting into some new material. This is an extension to part 18, in a small way, since it involves pointered structures similar to the linked lists, but the procedures for handling them are much different, so I am covering them in a seperate section. What is a Binary Tree? ====================== A binary tree is a set of nodes with 2 possible paths for each node, left or right. Hence, a binary tree node would look something like this: type nodeptr = ^node; node = record ourinfo: integer; left, right: nodeptr; end; Conceptually (my best low ASCII rendering, trust me!), a binary tree would look something like this. We can see from below that the possible paths of those two directions are any numerous. As with the linked lists, nil ends the path (knowing, it is probably garbled, but....). NODE _________/ \_________ / \ NODE NODE ____/ \____ ____/ \____ / \ / \ NODE nil nil NODE ____/ \____ ____/ \____ / \ / \ nil nil NODE nil ____/ \____ / \ NODE nil ____/ \____ / \ nil nil Rules of a Binary Tree ====================== Now that we see what the basic idea of a binary tree is, we will now study the rules for it. The basic rules of a binary tree is the following for any node in a binary tree commonly used for the purpose that they are used for a lot of times -- as a binary search tree or BST. Any program code in this section will make use of BSTs...: 1) values smaller than the value currently stored in a questioned node goes to the left. 2) values larger than the value currently stored in a questioned node goes the right. 3) values which are equal to other values in the tree are not placed into the tree. Let us see this by manually examining how we might build a small binary tree. Example of a Binary Tree Manual Build ===================================== Let us examine how we would build a binary tree for a set of values and how a program would traverse the tree with a very short example. 1) Let us start examining a tree build with a value set data of 27, 54, 32, 18, and 5. a) 27 is naturally the first or root node of the tree. There is no issue there. b) 54 > 27 so 54 would go to the right. Our resultant tree looks now like this: 27 \ 54 c) 32 > 27. So we would move to the right. Now we see 54 as the main node 32 < 54, so we would go to the left. So the resultant tree after this addition looks like this: 27 \ 54 / 32 d) 18 < 27. So we would move to the left. So our tree looks like this: 27 / \ 18 54 / 32 e) 5 < 27. So we would move to the left. Now we see 18 as the current node. 5 < 18. Then we would move to the left again. So the final binary tree for those 5 values looks like this: 27 / \ 18 54 / / 5 32 This is a reasonable binary tree structure. Any paths not used are set to nil in the process. We should see now how binary trees are constructed. 2) Now lets study traversal of the tree. It would depend on if we visit the actual node first or last. (I will give a detailed explanation of that last statement later.) Normally, as a standard, it's good to use the left side first. Therefore, lets look at the tree built in #1. The order the numbers would come up in is 5-18-27-32-54 on a basis that we handle or visit the head node of the tree last in moving through the binary tree. If you remember the discussion of the array binary search, we had to sort the data to accomplish it. Here it is taken care of already for us. Basic Idea of Programming for Binary Trees ========================================== You may have figured out already from our preliminary discussions, that recursion is a _major_ staple of binary tree programming (read ALWAYS use it!). Note that a binary tree can be divided up into smaller and smaller trees, hence our ability to use recursion. Any functions involved almost always ends up using some order of the following pseudocode, assuming a name of PROCEDURE for the procedure: PERFORM PROCEDURE ON LEFT SIDE OF TREE PERFORM ACTION ON ROOT NODE (the top node of the tree) PERFORM PROCEDURE ON RIGHT SIDE OF TREE We will see this idea be paramount in all the code we use. If you have noticed in your previous work about recursion, there is always a control statement present. Here, it will always be searching for the nil nodes. *ALWAYS* remember to replace the nil nodes if they come up....binary search trees are the perferred method generally, for setting up searches. program binary_tree_example; type nodeptr = ^node; node = record somedata: integer; left, right: nodeptr; end; var tree: nodeptr; afile: text; i: integer; procedure deletetree(var tree: nodeptr); { This procedure is a function designed to remove a binary tree from memory } begin { Given the background information, and other procedures as examples, you should be able to come up with this one } end; procedure inserttree(anumber: integer; var tree: nodeptr); { This procedure is a function designed to create a binary tree in memory by inserting a node in the proper position in the tree } begin if tree = nil then begin new(tree); tree^.somedata := anumber; tree^.left := nil; tree^.right := nil; end else begin if anumber > tree^.somedata then inserttree(anumber, tree^.right); if anumber < tree^.somedata then inserttree(anumber, tree^.left); end; end; procedure searchthrutree(anumber: integer; tree: nodeptr); { This procedure is designed to search through a binary tree for a particular value given in the variable anumber. } begin { Given the background information and other procedures as examples, you should be able to come up with this one. } end; procedure writeouttree(tree: nodeptr); { This procedure outputs a binary tree. } begin if tree <> nil then begin writeouttree(tree^.left); write(afile, tree^.somedata, ','); writeouttree(tree^.right); end; end; begin randomize; tree := nil; { the last statement is required for the nil markers set up via the BST procedures listed above } assign(afile, 'BTEST.TXT'); rewrite(afile); writeln(memavail, ' bytes available before tree creation.'); for i := 1 to 10 do inserttree(random(5)+1, tree); {This creates the tree. } writeln(memavail, ' bytes available after tree created.'); for i := 1 to 10 do searchthrutree(random(5)+1, tree); writeln(afile); writeln(afile); writeln(afile, 'I will now output the binary tree for the example.'); writeouttree(tree); writeln; close(afile); writeln('Now deleting tree out of memory'); deletetree(tree); writeln(memavail, ' bytes available after tree deletion.'); end. This is a whole program for working with a binary tree. I purposely left out 2 of the procedures, because the background information I gave earlier, plus pointer logic you should have learned from part 18 should allow you to logically come up with it. Do like I recommended in part 18 and draw it out. Example code is often not available for many problems, but the information _is_ there to be used. To program it is a needed skill to be able to figure out things on your own given some information. Code should not be handed out readily, IMO, unless there is no other way. Practice Programming Problem #19 ================================ One noted thing I have noticed with the binary search tree method especially, is that searches can be done very quickly with any kind of data -- working with arrays have a small way of being kludgy. Here we will see an example. You need to create a binary data file named SOMEWRDS.DAT which should contain 50 non-duplicated words, randomly defined (do not alphabetize them) out of 70 total words in the file. Allow users to type words into the keyboard, until they type the word "quit". Case-sensitivity should not matter in any usage of words. Write out to the user whether the word they typed was found in your database or not. Do not be concerned with reporting of run-time errors. Do be concerned, though, with the removal of the final binary-tree you use. Also for the delete code you write for binary trees, explain why your code does what it is supposed to do. Note ---- 1) Exercise prudent practice as programmers. In the event it was not covered, accuracy is a programmer's #1 concern, followed by efficiency in speed, memory usage, and disk usage. 2) This program should be friendly to the user, letting the user know precisely what is going on in all cases, even if a common error occurs, and cleaning up anything that may be of a problematic nature from those errors. You should have enough experience from previous parts to be able to determine what those errors are you would commonly encounter here. 3) The goal here for this program is to create a high-quality application. Final Statements for this Part ============================== I know many people may wish to someday release their creations to the public, or make programs for themselves or other's general usage. This is why I am being so vague now about describing programs, and will be from now on. Creativity and neatness is one of the major qualities that need to be developed by those who program. Also for all who program: The way to solve the problem is not always handed to you. You have to identify problems that are encountered, sit down and solve them. I estimate I'm probably 4-6 parts away from completing this whole tutorial, and 2 parts away from completing the parts of the tutorial regarding conventional Pascal programming. By now, most, if not all the tools have been presented for you to do this, and not need me to give you clues, and point you in the direction of what to do to solve the problem. Most if not all problems you will encounter in normal programming practice anywhere are normally presented in the form that this one is in -- a statement of what the program is expected to do. In short, I feel those who have been new programmers that have been following my tutorial from part 1 are about ready for the *REAL* world of programming. Next Time --------- I will cover several short random topics, which some of which may be suggested by those who e-mail me. I know I will cover hooking interrupts, calling an interrupt procedure, and linking an OBJ into Pascal code, incorporating assembler code into pascal code, and including inline statements in pascal program code. Is there any more people would like to see? E-mail me. Also, I haven't gotten any comments from anyone about how I am doing. Is anyone interested? Email ggrotz@2sprint.net. Note: I am sorry if I offended anyone by the "Final Statements for This Part" section. I am apologizing in advance if I come off too harsh. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 20: Miscallaneous Topics copyright (c) by Glenn Grotzinger Hello. Here is a solution for the programming problem that was given in part 19 with a little background explanation. PART19A.PAS ----------- This was the program written to generate the binary data file for use with the actual word checker. A text file was written to facilitate reading in the data, one word per line, to ease implementation. program part19a; var infile: text; outfile: file; datastring: string[14]; begin assign(infile, 'SOMEWRDS.TXT'); assign(outfile, 'SOMEWRDS.DAT'); reset(infile); rewrite(outfile, 1); readln(infile, datastring); while not eof(infile) do begin blockwrite(outfile, datastring, sizeof(datastring)); writeln(datastring); readln(infile, datastring); end; blockwrite(outfile, datastring, sizeof(datastring)); writeln(datastring); close(infile); close(outfile); end. PART19B.PAS ----------- This is the actual word checker. It is made so the words typed into the keyboard do not have to be case sensitive with reference to the datafile. All prevalent errors have error-checking code. program part19b; type strtype = string[14]; nodeptr = ^node; node = record str: strtype; left, right: nodeptr; end; var tree: nodeptr; datastring: strtype; infile: file; procedure opendatafile(var infile: file; afile: string); begin assign(infile, afile); {$I-}reset(infile, 1);{$I+} if IOResult <> 0 then begin writeln('Terminating. ', afile, ' does not exist!'); halt(1); end; end; function upstr(instr: strtype):strtype; var i: byte; tempstr: strtype; begin tempstr := ''; for i := 1 to length(instr) do tempstr := tempstr + upcase(instr[i]); upstr := tempstr; end; procedure memoryerror; begin writeln('Out of memory.'); halt(1); end; procedure deletetree(var tree: nodeptr); begin if tree <> nil then begin deletetree(tree^.right); deletetree(tree^.left); dispose(tree); end; end; procedure inserttree(datastring: strtype; var tree: nodeptr); begin if tree = nil then begin if memavail - sizeof(tree) > 0 then new(tree) else memoryerror; tree^.str := datastring; tree^.left := nil; tree^.right := nil; end else begin if upstr(datastring) > tree^.str then inserttree(upstr(datastring), tree^.right); if upstr(datastring) < tree^.str then inserttree(upstr(datastring), tree^.left); end; end; procedure buildsearch(var infile: file; var tree: nodeptr); var datastring: strtype; begin blockread(infile, datastring, sizeof(datastring)); while not eof(infile) do begin inserttree(datastring, tree); blockread(infile, datastring, sizeof(datastring)); end; inserttree(datastring, tree); close(infile); end; procedure searchtree(datastring: strtype; tree: nodeptr); begin if tree = nil then writeln(datastring, ' was not found in the data file.') else begin if datastring = tree^.str then writeln(datastring, ' was found in the data file.') else begin if datastring > tree^.str then searchtree(datastring, tree^.right); if datastring < tree^.str then searchtree(datastring, tree^.left); end; end; end; procedure promptwrite; begin writeln('Type QUIT to terminate.'); writeln('Type a word, and we''ll see if it''s in the database.'); writeln; end; begin writeln('Database checker.'); writeln; opendatafile(infile, 'SOMEWRDS.DAT'); buildsearch(infile, tree); promptwrite; readln(datastring); while (upstr(datastring) <> 'QUIT') do begin searchtree(upstr(datastring), tree); promptwrite; readln(datastring); end; deletetree(tree); end. The written explanation of the delete code that was written in the example, to completely remove the BST from memory... We know that in a pointer-linked structure if we remove a node that has links assigned to it, that we will cause a heap leak and render the rest of the data structure unremovable from memory. In a BST, that case becomes when both right and left branches of the node are nil. We have to use recursion, evidently, since the problem of deleting the entire binary tree comes down to many smaller problems of deleting nodes with both sides assigned to nil, basically. So basically, if we observe those lines of code, recursion occurs until both the left and right sides of nodes become nil, and then a dispose occurs. In working back up in returning from the procedure calls, the tree is essentially disposed of from the bottom up to the root node, in reverse from the way the insert code illustration created it. Contents for Part 20 ==================== Since this part contains many random varied topics, a table of contents is in order.... A) Linking OBJ code into Pascal programs. B) Including ASM or inline statements in Pascal programs. C) Hooking an interrupt in pascal programs. D) Calling an interrupt procedure. E) Conditional Compiler Compilation. Linking OBJ code into Pascal programs ===================================== This is a short program, which describes the exact process, USING A C program's function (the topic of using C OBJ's came up). If you do this with C OBJ, the code must not make any calls to any C libraries, or make any calls to a DLL. A description of what's going on... TEST1.H int far pascal add2numbers(int num1, int num2) { return (num1 + num2); } This is a C header file with a small defined function in it. You MUST define it using either void far pascal for a procedure or far pascal for a function. I describe a function in test1.h. For those who wonder, a header file in C functions much like a unit does in Pascal. For an OBJ you intend to use, each and every function must be defined in the resultant pascal program. (I know it is not liked, and you SHOULD NEVER normally post attachments in c.l.p.b.) With differences between so many different C compilers and syntaxes that exist in the world, I will post the OBJ file I got from the compiler I use, for purposes of enabling others to test this. I recommend you to cut and paste it before you uu or xx encode it if you need to obtain it. begin 644 test1.obj M@`D`!U1%4U0Q+D..B!\````;5$,X-B!";W)L86YD(%1U boolean: indicates that the operating system is MS-DOS or PC-DOS CPU87 => boolean: true if there is a math coprocessor (FPU) present. The only construct you will typically see is an if then or if then else. The compiler directives I listed above are most commonly used with this type of situation. There are additional floating point data types available called comp, single, double, and extended; which are usable via the FPU generally. The definitions of these variables may be defined using the conditional compiler directives.... The conditional compiler directives we need to know are: {$IFDEF } If item defined {$IFNDEF } if item not defined {$ELSE} ELSE directive {$IFOPT } If compiler opt.. {$ENDIF} END IF STATEMENT {$DEFINE } define a statement {$UNDEF } undefine a statement. Functionally, these work and compile the source code if certain things are true. For example, {$IFOPT N+} {$ENDIF} in some code will make it so the code represented by will only be used if N+ is defined. Here is a short example program using conditional compiler directives.... program condcomptest; begin {$IFDEF CPU87} writeln('There is a math coprocessor in the system.'); {$ELSE} writeln('There is not a math coprocessor in the system.'); {$ENDIF} end. You will notice that only ONE writeln statement will execute itself. The dependence will be whether a FPU exists in your system. Practice Programming Problem #20 ================================ Write a program which will count the number of keystrokes a user presses in a period of 15 seconds. Use proper programming interest in this program, and be sure to be friendly to the user with this program. Background information. Keyboard interrupt is $09. Interrupt occurs 2 times for every key pressed. Notes: Unfortunately, in all attempts I've made, to make it TERMINATE exactly on 15 seconds, it would cause a nasty problem. The program will have to read in at least one key after termination. Do not worry about this. Next Time ========= We use the BGI. Write comments to ggrotz@2sprint.net. Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 21: Use of the BGI. copyright (c) 1995-6 by Glenn Grotzinger Here is a solution to the problem presented last time. program part20; uses dos; const oneminute = 273; { # of timer ticks that happens in 15 seconds } var saveint09, saveint1c: procedure; timecounter, keycounter: integer; achar: char; timeup: boolean; {$F+} procedure counttime; interrupt; begin inc(timecounter); if timecounter = oneminute then timeup := true; inline($9C); saveint1c; end; procedure countkeys; interrupt; begin inc(keycounter); inline($9C); saveint09; end; {$F-} begin timeup := false; timecounter := 0; keycounter := 0; getintvec($09, @saveint09); getintvec($1C, @saveint1C); setintvec($09, @countkeys); setintvec($1C, @counttime); while timeup = false do read(achar); setintvec($09, @saveint09); setintvec($1C, @saveint1C); writeln('There were ', keycounter div 2, ' keys pressed.'); end. BGI Intro ========= This part is basically about using the Borland Graphics Interface. It is not generally recommended to use it -- use assembler instead to make it a lot quicker, and smaller.... but due to demand, and the ability to use the BGI for basic graphics, we will talk about use of the BGI. For heavy use of graphics, assembler is indeed better.... BGI loader ========== Here, I will describe how to make the BGI files functionally more useable than in their current state....Compile with looking for the external BGI files is fine, but will run into an annoyance quick, especially with a distributed utility...it is easier to have the EXE available with the BGI files binded to it than to make a user keep track of the BGI files.... There is a utility available in TP called BINOBJ. The BGI files are video control files, while CHR files are font definition files. They both are usable in graphics as binary files...and that is what BINOBJ does...it converts binary files to OBJect files, which may be handled like described before. The proper usage of this utility is... BINOBJ Here is a sample command-line. To convert VESA16.BGI to an OBJ, do this: BINOBJ VESA16.BGI VESA16.OBJ Vesa16driver Locate the BGI files that should be in your TP/BGI directory. You will need to copy them off to a separate directory. After you convert all BGI files like this, I recommend you write a unit to link the BGI files in -- optionally, if you need fonts, you may write a unit to link those in likewise. Be sure to define your proper external procedures like it needs to be done. Make use of meaningful names, as you will have to remember them to make use of the graphics set. For example, I call my unit bgivideo; for each procedure, I put the first 8 chars of the name of the file, and then driver for the BGI files. These function names will be easy to remember when we need to use them. Going into Graphics Mode ======================== Now, hopefully you have your BGI video unit ready now, and sitting in whatever directory you use to place your pascal code. Now we will discuss about how to go into graphics mode using the BGI. Generally, going to graphics mode initially would require setting up the BGI unit you created, and then performing an autodetect on the graphics, followed by an init into graphics mode. Let us look at some sample code... program video_example_1;{1} uses graph, bgivideo; var graphicsdriver, graphicsmode: integer; procedure errormessage(driver: string); begin writeln('There was an error: ', grapherrormsg(graphresult), driver); halt(1); end; begin {2}if (registerbgidriver(@attdriver) < 0) then errormessage('ATT'); if (registerbgidriver(@cgadriver) < 0) then errormessage('CGA'); if (registerbgidriver(@egavgadriver) < 0) then errormessage('EGA/VGA'); if (registerbgidriver(@hercdriver) < 0) then errormessage('Herc'); if (registerbgidriver(@pc3270driver) < 0) then errormessage('PC 3270'); {3}detectgraph(graphicsdriver, graphicsmode); graphicsdriver := Detect; {4}initgraph(graphicsdriver, graphicsmode, ''); {5}if GraphResult <> grOk then begin writeln('Video error.'); halt(1); end; {6}repeat putpixel(random(getmaxx), random(getmaxy), random(getmaxcolor)); until keypressed; readln; {7}closegraph; end. This is basically a random pixel place system using the video mode in the recommended auto-detect. Let's go through a few of the features of the code, which were marked by {}'s. {1} As we see, the graph, and bgivideo units are used here. The graph unit is the basic function interface for the BGI system. We are familiar with bgivideo from earlier. {2} RegisterBGIDriver() must be called for any and all realistic possibilities. I recommend that only the ones listed really need to be checked. If the function is less than zero, then there is a problem. The errormessage function holds a function called grapherrormsg() which will output a direct error as to why things aren't working right. {3} detectgraph detects the graphics card. it takes integers represented by graphicsdriver, and graphicsmode....graphicsdriver will hold the recommended video type, and graphicsmode will hold the recommended video mode (it can be changed). This is why we registered all the drivers...it will use whichever one it needs, and ultimately, the program will work with all video modes. {4} initgraph() takes us into graphics mode. It is called basically as indicated in the program. the third param '', is for when we load the BGI files externally. To do that, do not include BGIVIDEO and provide a path to the BGI files....to get a full auto-detect capability, just make all the BGI files available....it is easier in the long run to have the BGI files combined in the exec. {5} For almost any graphics function, a variable called graphresult is changed. There are graphics mode constants, which will be covered later, which are in there representing different things. grok is the one which indicates that things are OK. {6} This is the real meat of the procedure. It keeps placing pixels of random position and color on the screen until a key is pressed. getmaxx is the maximum screen position on the x axis. getmaxy is the maximum screen position on the y axis. Graphics screens have the same kind of dimensional setup as the crt graphics screens do. The putpixel procedure takes a x, y coordinate, then a color...getmaxcolor is a constant that holds the maximum # of colors present in the current video mode. {7} Closegraph is pretty much self-explanatory. It shuts down graphics mode and takes us back to text. BGI Font Usage ============== The CHR files you saw, are font files, which have an ability to be used in graphics programs.... These files can be converted to OBJs, and I recommend that you do so for purposes of using the fonts..... Here is a small example of loading and using fonts -- I'm not repeating the video load code, so I will omit that.... program video_example; uses graph, bgivideo; var graphicsdriver, graphicsmode: integer; i: integer; {1} {$L GOTH.OBJ} procedure Gothfont; external; procedure errormessage(driver: string); begin writeln('There was an error: ', grapherrormsg(graphresult), driver); halt(1); end; begin { This is the video load code } {2} if registerbgifont(@gothfont) < 0 then errormessage('Script font'); i := 100; {3} settextstyle(DefaultFont, HorizDir, 1); {4} setcolor(blue); {5} outtextxy(20, i, 'This is the DEFAULT font.'); {6} inc(i, textheight('T')+2); readln; {7} cleardevice; settextstyle(Gothicfont, horizdir, 2); setcolor(green); outtextxy(20, i, 'This is the GOTHIC font.'); readln; closegraph; end. This basically goes into graphic mode, and writes those two statements to the screen. We will go through the areas marked by {}'s.. {1} This is exactly like I described before. This is how we load the CHR file to not make it separate. We do it exactly like the BGI files, and set them up as external OBJ files. {2} registerbgifont works exactly like registerbgidriver does...it registers the font into the program -- load fonts judiciously -- only if you need them. {3} settextstyle() changes the font, direction, and size....things can be written out either horizontally, or vertically. Fonts letters are 8X8 pixels in size...so it is also possible to zoom the fonts...the third is the factor in which it may be done...a 2 in the third parameter makes the letters 16X16 pixels, and so on and so forth. {4} setcolor(blue) sets the foreground graphics draw color to blue. {5} outtextxy() puts out a text statement at coordinate x, y for graphics mode. {6} textheight is a function which guages the height in pixels of a text placed in the statement. Font file names =============== DefaultFont; TriplexFont; SmallFont; SanSerifFont; GothicFont; represent fonts. HorizDir; VertDir; A VERY QUICK overview of commands available from BGI ==================================================== Due to the volume of commands available, all of them can not be sufficiently covered in the space of this document. Most if not all of them are straight-forward to use. Look at page 185 of the Turbo Pascal language guide for a list of all of the BGI commands. Conclusion ========== IMO, Borland made the BGI very hard to use. I have stumbled across many things that looked like bugs in their system (I couldn't use their included script font). Beyond that, it works OK for light-duty graphics. Anything beyond that truly needs assembler. There is enough knowledge here to set up and make use of BGI (not with- standing the basic commands in that list -- for example, rectangle.... draws a rectangle....). As another side note, you may have noticed that executables you create using methods described here are large. Get a program such as PKLITE, or LZEXE, and compress it. They compress down about 45-55%, in my experience. Graphics programs using BGI seem to characteristically compile to be large. Practice Programming Problem #21 ================================ Make a program which will successively place rectangles on the screen in random colors. Since it is hard to illustrate what I'm wanting, given this medium, I will place the final executable, named part21.EXE in the file at Garbo. Cut and paste the document after you save this, please. Here are the basic stats behind the program: 1) Squares are used. use the rectangle() function. it takes for arguments, the coordinates of the upper left hand corner, and the lower right hand corner. 2) Each square drawn successively is one pixel larger than the previous.... 3) Continually draw the squares until the user presses a key. 4) The colors are randomly determined. Look at the program to get an idea of what I am looking for. Also please send comments back to ggrotz@2sprint.net if it happens to not work on your system for no readily apparent reason. I need to get an idea of how well these sample video routines work. Next Time ========= Things will be indeterminate. I will be covering object-oriented program- ming. As I have not set down and figured out how many parts object-oriented programming will take to cover, I do not know. E-mail ggrotz@2sprint.net and suggest anything that has to do with TP, which I may have not covered. If it sounds good, I will cover it! Turbo Pascal for DOS Tutorial by Glenn Grotzinger Part 22: Finale copyright(c) 1995-96 by Glenn Grotzinger Here is a solution of the graphics problem from last time... program part21; uses graph, bgivideo, crt; { must be in this order } var graphicsdriver, graphicsmode: integer; x1, x2, y1, y2: integer; procedure errormessage(driver: string); begin writeln('There was an error: ', grapherrormsg(graphresult), driver); halt(1); end; begin randomize; if (registerbgidriver(@attdriver) < 0) then errormessage('ATT'); if (registerbgidriver(@cgadriver) < 0) then errormessage('CGA'); if (registerbgidriver(@egavgadriver) < 0) then errormessage('EGA/VGA'); if (registerbgidriver(@hercdriver) < 0) then errormessage('Herc'); if (registerbgidriver(@pc3270driver) < 0) then errormessage('PC 3270'); detectgraph(graphicsdriver, graphicsmode); graphicsdriver := Detect; initgraph(graphicsdriver, graphicsmode, ''); if GraphResult <> grOk then begin writeln('Video error.'); halt(1); end; repeat x1 := getmaxx div 2 - 15; x2 := getmaxx div 2 + 15; y1 := getmaxy div 2 - 15; y2 := getmaxy div 2 + 15; { center of screen is always (getmaxx div 2, getmaxy div 2) -- look at geometric properties of a rectangle } repeat setcolor(random(getmaxcolor)); rectangle(x1, y1, x2, y2); inc(x2, 1);inc(y2, 1);dec(x1, 1); dec(y1, 1); delay(50); until (keypressed) or (x1 <= 0); until keypressed; readln; closegraph; end. As you can see in this example, it's always good to have a good background in analytical geometry to be able to do graphics well. It is good for you to find a mathematical reference and learn a few concepts of it, if you do not already know something about it. Finale ====== Ultimately, it was decided that object-oriented programming will not be covered at this time for the tutorial. I apologize. Note ==== Be sure to read license.txt!