| |||||||||||||||||
Machine Problem 2: Financial Calculations
9/21: Due date changed to 10/3 9/20: FDPow even/odd b corrected to n 9/18: FDPow description IntroductionAfter reeling from several thousand dollars of debt to the Illini Union Bank, you get a strange feeling that something was not right. The Illini Union Bank was not paying interest on its accounts! It's time to fix this major oversight. The IUB is still expecting the unwitting engineering students to work out the minor flaws in its original roll-out, such as the inability to collect from students who have taken out several $4096 withdrawals. Of course, we don't want to fix that bug. Problem DescriptionNow no longer working with the front end to the bank ledger, we will instead work with back-end functionality. The back-end interface takes individual commands that work on fixed-point decimal values. The operations it supports are as follows:
Fixed Point Decimal NumbersSince we want to read, work with, and store numbers that look like 102.34, we need to define a representation and to write functions to perform useful math on them. First we'll address the representation we will be using, and then we'll address what functions we need to write and how to write them. RepresentationOne way to store these numbers is by storing a larger number that is 100 times larger than the stored number. For example to store our number 102.34, we'd store the binary for 10234, and divide off the 34 to display it. As it turns out, doing it this way would make it hard to use ascbin, binasc, and other assorted functions since we would need to store more than 16 bits to keep the same range our normal 16 bit numbers have. To address these size issues, we will instead use a object which is 32 bits large. In one half we will store the integer component. In the other half, we will store the decimal component multiplied by 100, with a maximum value of 99. The number 102.34 would be stored with the two word-sized numbers 102 and 34. The primary advantage to doing it this way is that our normal tools will work, ascbin and binasc in particular. FunctionsFor the financial calculations in this MP we need several basic utility functions. These include the following: FDReadTo do any work with a FDNumber, we must first read it in. While ascbin can turn a string into a number, this only works for integers. FDRead must handle numbers that look like 102, 102.3, and 102.34. To do this, you will probably wish to call ascbin on the components and then store them into the appropriate halves of the FDNumber. FDRead must report success and error cases like ascbin does. Unlike ascbin the output is in the FDNumber pointed to by di, and ax should remain unchanged. Special cases to be aware of:
FDWriteAfter reading in a number, it's good to be able to display the number to a curious user. Just as FDRead can be implemented with two calls to ascbin with additional logic, FDWrite can be implemented as two calls to binasc also with additional logic. FDWrite must match binasc in terms of outputs, with bx pointing to the first non-blank character in the 10-byte buffer, and cl holding the number of non-blank characters in the generated string. The only special case to FDWrite is for numbers like 102.03. Since binasc will generate the string " 3$", you must manually add the prepended zero. You may find it useful to use an additional buffer to write parts of the numbers and copy them into the final buffer pointed to by bx. FDAddReading and writing FDNumbers is nice, but now let's do some math. Adding two numbers is very straightforward, and is good practice for the more complicated operations we will be doing next. If you consider the two FDNumbers A and B, with corresponding pieces Ah, Al, Bh, and Bl such that A=Ah+Al/100. Their sum S is calculated as follows: Sh = Ah+Bh Except one thing: Al+Bl may exceed 99, but Sl is not allowed to. In such cases, for every 100 in Sl, subtract it out and add 1 to Sh. You may find it more intuitively obvious to do this with div or you may not. Note that FDAdd reads from the FDNumbers at offsets si and di and writes to the FDNumber at offset di, much like an add [di], [si] might function if it were possible. FDMulMultiplication is an extension of adding. Consider again two such numbers A and B, separated as above. Their product P is calculated much like you would calculated the product (x+y)(x-y) Ph = Ah*Bh + Ah*Bl/100 + Al*Bh/100 where x % 100 is the remainder when x is divided by 100, or its modulus. Note that Al*Bl % 100 may be greater than or equal to 50. In such cases Pl should be incremented, and such changes should be propagated through to Ph as necessary. The additions in the formula for P may be done by calling FDAdd or you may find it easier to implement them manually. FDPowFDPow is our exponentiation operator. In an algorithms class you may have become acquainted with the recursive algorithm we will be using. Don't worry if you haven't, as we describe it fully here. Recursive algorithms work by defining a base case and an iterative step. The Fibonacci function is the classic example: the base cases are F(0)=0 and F(1)=1. For all other F(n), F(n) = F(n-1) + F(n-2). Thus when finding F(12), it recursively can ask for F(10) and F(11). Once F(10) and F(11) return their answers, the body for the Fibonacci function can add them together and return the final answer for F(12). Applying this back to the problem at hand: we have a number b which we wish to raise to the nth power. The base case is b raised to the first power, or merely b itself. Thus when FDPow is called with exponent 1, it can return the base as the answer without further calculations. The recursive step is one of two things, depending on whether the exponent is an even or odd number: bn = b * bn/2 * bn/2, for all odd n, and truncated values n/2, or bn = bn/2 * bn/2, for all even n. Once these cases are all handled, i.e. n=1, and all even or odd numbers above 1, the nature of recursion (or the Recursion Fairy, according to Professor Jeff Erickson, who should know) will give you your final answer almost magically. The only trick is the actual implementation of recursion in assembly. Where high level languages will automagically keep your inputs and outputs separate, assembly style procedures make use of single pieces of hardware (registers) for both input and outputs.
UPDATE (9/18) The library code now handles an additional base case n=0.
You are encouraged, but not required, to handle it as well. The proper
return value is 1.00.
FDPow takes two addresses as input. Like the previous functions they are si and di. Unlike the previous functions, we neither store back to the number at offset di, nor is the operation commutative: an is not na. The convention we have instead chosen is to calculate [di][si], storing the result in dx:ax. Store the integer portion in dx and the hundredths in ax. Note: thinking recursively takes practice. The same code must function correctly whether it is called with 1, 4, or n as the exponent. And more importantly, since it calls itself, anything it writes to will be similarly written to by its recursive call. In general you can work around this by pushing values to the stack and restoring them from the stack as the recursive procedure exits. Don't give up! Input HandlingNow that we've got a set representation and several utility functions for our fixed-point decimal numbers, it's time to write the rest of the program that makes it possible to use them. To perform a compound interest calculation or currency conversion, the user will type something like: i 100 0.03 12, which represents a starting value of $100, at a 3% interest rate, compounded 12 times. How do we read this command in and then process it? There are four parts to this answer: DoCommand, ReadLine, GetLetter, and GetNumber. Additionally, once a command line is understood, either CalculateInterest or ConvertCurrency is called to handle the computation. DoCommandDoCommand is the heart of the operation. It performs all the steps necessary to do one calculation. It will orchestrate the reading of a line of input data, the matching pieces to either an interest or currency calculation, the dispatching of this data to the corresponding computational function, and error checking in several steps. In particular DoCommand must call ReadLine with pointers set to the appropriate buffer (tbuf, for typing buffer). If no characters are read, it must call ReadLine again until some are. Then DoCommand must call various patterns of GetLetter and GetNumber to parse out a command. The first item in the line must be either i, c, or q. If it reads q, then DoCommand must return a 0 so the main loop will know to quit. If instead it reads i, it must then read three fixed-point decimal numbers, the principle, the interest rate, and the number of periods, respectively. DoCommand can then call CalculateInterest and print the result. Correspondingly, if the first letter is c, DoCommand must get two more letters, and a number. These are, in order, the originating currency, the resulting currency, and the amount in the original currency. For instance, c d c 100.30 represents a conversion from 100.30 US dollars to Canadian dollars. For any other first letters, including of course any non-letters that cause GetLetter to return an error, DoCommand must report an error. The help message should be provided at this point in time. If any of the GetNumber or GetLetter calls returns an error, or if the call to the computational function returns an error, DoCommand should display the error message (but no help message), and exit with a nonzero value in ax to indicate not to quit. Otherwise if no errors occur, DoCommand should print out the result (preceded by the result message, of course). ReadLineBefore we can retrieve any values from the string which the user inputs, we must read the string into a buffer. There are several important details to this function, so read carefully!
Note the inputs and outputs; you're given a buffer address and size, and must return a (partially) filled buffer, and a count of filled characters. Prompt the user by displaying "> " but do not place it in the buffer. Don't forget to write to the screen. GetLetterThe purpose of GetLetter is simple. Ignoring any intervening spaces, it should return in al the ASCII value of the first letter at or after offset bx. If the first value which is not a space is not a letter, it should return ax=0 instead to indicate an error. GetLetter should also update bx to point to the character after the letter it returned. GetNumberGetNumber functions similarly to GetLetter. It receives as input the offset of (part of) a string, and returns in ax a value indicating whether it was successful. Instead of returning a successful letter in al, however, GetLetter places the read number in the memory located at offset di; di is an input to GetNumber. CalculateInterestCalculate the interest by calling appropriate FDx functions and manipulating pointers correctly. To be fully functional, CalculateInterest may not write to any of its inputs. If necessary, however, it may use other buffers such as the pre-declared numbuf1. Remember that the equation that we are using is: [result] = [principle] * (1+[interest])[periods] Note that large numbers, especially as the exponent, can cause overflows. You do not have to handle these; merely understand why the occur with large numbers ConvertCurrencyConverting currency is probably a longer procedure than calculating interest, but mostly because it must validate its input. The two currencies chosen are represented with letters in dh and dl. You must make sure that the provided arguments are really letters because you must then use them as indices in the lookup-tables tab_from and tab_to. Furthermore, if the value you retrieve from the table is 0, it means the user has requested an invalid currency. Otherwise it is the address of a FDNumber as found online last week. The equation for currency conversion is: [result] = from * to * [principle], where from and to are the values looked up in tab_from and tab_to respectively. Note: on any invalid input ConvertCurrency must signal error. Like CalculateInterest, ConvertCurrency should not write to any of its inputs, and instead should write to [result]. SubroutinesIn this section there is a listing of the inputs, outputs, and summarized actions of each function you will need to write. Refer to the above information or to the Gradesheet for more information on what is required. DoCommandInputs: noneOutputs: ax = 1 to continue or 0 to quit Purpose: gets and processes a command, reporting the result Calls: dspmsg, ReadLine, GetLetter, GetNumber, CalculateInterest, ConvertCurrency, FDWrite ReadLineInputs:ax = size of the buffer bx = offset of the buffer Outputs: ax = number of characters in the buffer (excluding '$') buffer at bx holds the typed characters Purpose: take keyboard entry from the user, handling backspace and bounds Calls: dspout, kbdin, optionally dspmsg Note: Display the "> " but do not place it in the buffer. GetLetterInputs:bx = offset of the buffer from which to read Outputs: al = ASCII value of first letter (ignoring spaces), or ax = 0 if it's not a letter bx = offset of the character following the letter returned, undefined on error Purpose: retrieve a letter from the input string; error if one isn't available GetNumberInputs:bx = offset of the buffer from which to read di = offset of the FDNumber in which to store the read number Outputs: ax = 0 on error or non-zero on success bx = offset of the character following the number returned, undefined on error [di] = the number read from the string Purpose: retrieve a FDNumber from the input string; error if one isn't available Calls: FDRead CalculateInterestInputs:[principle] = the principle for the calculation [interest] = the interest rate per period [periods] = the number of periods to compound All three of the above are FDNumbers Outputs: ax = 0 on error or non-zero on success [result] = the result of the calculation principle * (1 + interest)periods Purpose: calculate the compound interest, storing to [result] Calls: FDAdd, FDMul, FDPow Note: This subroutine should always succeed; ax should thus always be non-zero on exit ConvertCurrencyInputs:[principle] = the principle for the calculation, a FDNumber dh = the letter for the currency from which to convert dl = the letter for the currency to which to convert Outputs: ax = 0 on error or non-zero on success [result] = the result of the calculation principle * from * to Purpose: calculate a currency conversion, storing to [result] Calls: FDMul Note: This subroutine can fail, so make sure to properly check for such error conditions FDReadInputs:bx = offset to a string representing a decimal number di = offset of the buffer to which it should be stored Outputs: bx = offset of the first non-converted character dl = conversion error code, as in ascbin; use overflow if the portion following the decimal is above 99 [di] holds the converted FDNumber Purpose: input a FDNumber from a user-typed string Calls: ascbin Note: The formats of supported numbers are varied, and a number like 100.3 is tricky; see above FDWriteInputs:bx = offset to a 10 byte buffer si = offset of the FDNumber to be converted into ASCII Outputs: bx = offset of the first non-blank character, with the number right-justified and with spaces to the left; two digits after the decimal point cl = number of non-blank characters Purpose: output a FDNumber into a user-readable string Calls: binasc Note: Numbers like 100.03 must have the zero inserted manually you may wish to use a second buffer for at least one of the calls to binasc FDAddInputs:si = offset to a source FDNumber di = offset to a source and destination FDNumber Outputs: [di] = holds the result of the addition Purpose: Adds the FDNumbers at offsets di, si and stores at offset di Note: When the decimal portion exceeds 99, the overflow must be transferred into the integer portion of the FDNumber FDMulInputs:si = offset to a source FDNumber di = offset to a source and destination FDNumber Outputs: [di] = holds the result of the multiplication Purpose: Multiplies the FDNumbers at offsets di, si and stores at offset di Calls: FDAdd, optionally Note: Round to the nearest hundredth by turning .005 and higher into .01 You may find it useful to use a scratch FDNumber or two FDPowInputs:si = offset to a source FDNumber as exponent di = offset to a source FDNumber as base Outputs: dx = integer portion of the result ax = the hundredths portion of the result Purpose: raises [di] to the [si] power Calls: FDMul Note: Recursion takes practice Ignore the non-integer portion of [si] if there is one Hints
Procedure
Final Steps
MP2.ASM (program framework)
; MP2 - Your Name - Today's Date
;
;
; Josh Potts, Fall 2001
; Guest Authors: Michael Urman, Justin Quek
; University of Illinois, Urbana-Champaign
; Dept. of Electrical and Computer Engineering
;
; Version 1.0
BITS 16
;====== SECTION 1: Define constants =======================================
BS EQU 8
CR EQU 0Dh
LF EQU 0Ah
BEL EQU 07h
TBUF_SIZE EQU 79
;====== SECTION 2: Declare external procedures ============================
; These are functions from lib291
EXTERN kbdin, dspout, dspmsg, ascbin, binasc, mp2xit
; You will be writing your own versions of these functions
EXTERN _libDoCommand, _libReadLine, _libGetLetter, _libGetNumber
EXTERN _libCalculateInterest, _libConvertCurrency
EXTERN _libFDRead, _libFDWrite, _libFDAdd, _libFDMul, _libFDPow
; The _lib functions need these to work properly
GLOBAL _DoCommand, _ReadLine, _GetLetter, _GetNumber
GLOBAL _CalculateInterest, _ConvertCurrency
GLOBAL _FDRead, _FDWrite, _FDAdd, _FDMul, _FDPow
GLOBAL tbuf, pbuf, binascbuf
GLOBAL principle, interest, periods, result
GLOBAL numbuf1, numbuf2
GLOBAL msg_help, msg_error, msg_result, msg_crlf
GLOBAL tab_from, tab_to
;====== SECTION 3: Define stack segment ===================================
SEGMENT stkseg STACK ; *** STACK SEGMENT ***
resb 64*8
stacktop:
resb 0
;====== SECTION 4: Define code segment ====================================
SEGMENT code ; *** CODE SEGMENT ***
;====== SECTION 5: Declare variables for main procedure ===================
tbuf resb TBUF_SIZE
pbuf resb 10
binascbuf resb 7
principle resd 1
interest resd 1
periods resd 1
result resd 1
numbuf1 resd 1
numbuf2 resd 1
msg_help db CR, LF, 'Perform an Interest calculation by entering'
db CR, LF, ' i |
| Fall 2001 |