The example presented in this chapter is based on a simple dialog box to compute the monthly payments on a mortgage based on the following formula:
where:
P = Mortgage Principle
R = Monthly Interest Rate (expressed in decimal)
N = Number of months
PMT = Computed monthly payments
The dialog box itself was designed with the "WYSIWYG" feature of the Symantec Resource Editor Rs32.exe provided with the MASM32 package in the rc sub-folder. The only manual modification to the generated script was to add the ES_NUMBER style to two of the edit controls, that style not being an option with Rs32. The resource script is reproduced at the end of this document.
Three separate edit controls are provided for the user to input the mortgage principal, the annual interest rate (expressed as a %) and the number of years. Two radio buttons are also provided to choose between two procedures for computing monthly payments.
One procedure is based on the generally allowed practice in the USA of compounding interest on a monthly basis. For example, a 12% stated annual rate becomes a 1% monthly rate which, when compounded, is equivalent to a true 12.6825% annual rate.
The other procedure is based on a restriction imposed on Canadian chartered banks for mortgage loans where the stated annual rate cannot be compounded more than twice per year. For example, a 12% stated annual rate becomes a 6% semi-annual rate which, when compounded, is equivalent to a true 12.36% annual rate. The monthly rate must thus be computed according to the following formula where RSA is the semi-annual rate:
R = (1+RSA)1/6 - 1
When this computed monthly rate is compounded on a monthly basis, it would be equivalent to the semi-annual rate.
In order to simplify the program as much as possible, some limits have been imposed on the user input but may not even be noticed.
-- The input for the mortgage principal is restricted to numbers only (with the ES_NUMBER style for its edit control) and limited to a maximum of 9 characters (with the EM_SETLIMITTEXT message during the WM_INITDIALOG phase). This still allows for a mortgage input of up to 1 billion dollars which should generally be sufficient, but disallows the inclusion of pennies (which would be very rarely specified anyway) as part of the input. The main advantage of those restrictions is that it guarantees a positive binary 32-bit integer can be retrieved directly from the edit control without any need to parse the input for invalid characters and perform the conversion from ASCII with additional code.
-- The input for the number of years is also restricted to numbers only and limited to a maximum of 2 characters. This still allows for a mortgage life of up to 99 years, but disallows partial years (which is also rarely specified) as part of the input. Advantages are the same as above.
-- The input for the annual rate is restricted to 9 characters (rates are rarely specified with more than 3 decimals). This, however, guarantees that the value of the numerical digits excluding the decimal delimiter would not exceed the maximum possible value of a positive 32-bit integer.
While parsing the annual rate, a few more restrictions generate an error message:
-- An annual rate exceeding 100% (which would be considered illegal loan-sharking). This guarantees that the log2 of any (1+R) term will always be less than 1.
-- A "-" sign. The rate must be positive.
A lack of input or an input equal to 0 in any of the three edit controls also generates an error message and no computation is performed.
With pre-validated data, the computation can thus proceed without any risk of error. Some code has nevertheless been added before displaying the result to ascertain that no major problem has been encountered due to unforeseeable circumstances.
The code has been kept as simple as possible, without any attempt to optimize it for speed or size (speed optimization would be a waste of time and effort for such an application). It is also specific for the application. Even the code to convert the annual rate from a string to a floating point should not be generalized without modifying the parts which rely on the designed purpose and restrictions.
The provided code is fully tested. It can be assembled without modification with MASM32 if copied into a file with the .asm extension. If the resource script is copied into a file named rsrc.rc and placed in the same directory as the .asm file, the .exe file can be generated in a single step with the Project->Build All menu option of the QEditor in MASM32. Modifications to the code and/or assembly procedures will be required with other assemblers and/or IDEs.
The FPU instructions used with this example application are (in alphabetical order):
F2XM1 2 to the X power minus 1 FADD Add two floating point values FBSTP Store BCD data to memory FCHS Change the sign of ST(0) FDIV Divide two floating point values FIDIV Divide ST(0) by an Integer located in memory FILD Load integer from memory FIMUL Multiply ST(0) by an Integer located in memory FINIT Initialize the FPU FLD Load real number FLD1 Load the value of 1 FMUL Multiply two floating point values FRNDINT Round ST(0) to an integer FSCALE Scale ST(0) by ST(1) FSTP Store real number and pop ST(0) FSTSW Store status word FSUB Subtract two floating point values FWAIT Wait while FPU is busy FXCH Exchange the top data register with another data register FYL2XP1 Y*Log2(X+1)
; ####################################################################### ; ; Mortgage payment calculator ; Written with MASM32 by Raymond Filiatreault ; August 2003 ; ; ####################################################################### .386 ; minimum processor needed for 32 bit .model flat, stdcall ; FLAT memory model & STDCALL calling option casemap :none ; set code to case sensitive ; ####################################################################### include \masm32\include\windows.inc include \masm32\include\user32.inc include \masm32\include\kernel32.inc include \masm32\include\comdlg32.inc include \masm32\include\comctl32.inc includelib \masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib includelib \masm32\lib\comdlg32.lib includelib \masm32\lib\comctl32.lib ; ######################################################################### return MACRO arg mov eax, arg ret ENDM WndProc PROTO :DWORD,:DWORD,:DWORD,:DWORD ; ######################################################################### PRINCIPLE EQU 711 RATEPCT EQU 712 YEARS EQU 713 PAYMENT EQU 714 COMPUTE EQU 720 QUITS EQU 721 USABUT EQU 730 CANADABUT EQU 731 AMERICAN EQU 0 .data hDlg dd 0 hInstance dd 0 mortgage dd 0 months dd 0 factor6 dd 6 factor10 dd 10 factor12 dd 12 bcdtemp dt 0 radiobutton db 0 ;0=USA, 1=Canada badinput db "Input error",0 princerr db "Unacceptable input for principle",0 raterr db "Unacceptable input for rate",0 yearerr db "Unacceptable input for years",0 invalid db "Invalid FPU operation detected",0 buffer1 db 16 dup(0) ; ######################################################################### .code start: invoke GetModuleHandle,NULL mov hInstance,eax invoke InitCommonControls invoke DialogBoxParam,hInstance,700,NULL,ADDR WndProc,NULL invoke ExitProcess,eax ; ######################################################################### WndProc proc hWin :DWORD, uMsg :DWORD, wParam :DWORD, lParam :DWORD .if uMsg == WM_INITDIALOG push hWin pop hDlg ;save handle in a global variable ;this avoids having to pass it as a ;parameter whenever needed outside this proc invoke SendDlgItemMessage,hDlg,PRINCIPLE,EM_SETLIMITTEXT,9,0 invoke SendDlgItemMessage,hDlg,RATEPCT,EM_SETLIMITTEXT,9,0 invoke SendDlgItemMessage,hDlg,YEARS,EM_SETLIMITTEXT,2,0 invoke CheckRadioButton,hDlg,USABUT,CANADABUT,USABUT return TRUE .elseif uMsg == WM_COMMAND mov eax,wParam and eax,0ffffh .if eax == QUITS ;Exit button clicked invoke EndDialog,hDlg,0 .elseif eax == COMPUTE ;Compute button clicked call maincalc ;process input and display result return TRUE .elseif eax == USABUT ;USA radiobutton clicked mov radiobutton,0 ;store info return TRUE .elseif eax == CANADABUT ;Canada radiobutton clicked mov radiobutton,1 ;store info return TRUE .endif .elseif uMsg == WM_CLOSE invoke EndDialog,hDlg,0 .endif return FALSE ;use Windows defaults to handle other messages WndProc endp ; ######################################################################## maincalc: ;********************************************************* ;***** Retrieve THE MORTGAGE PRINCIPAL input ***** ;********************************************************* invoke GetDlgItemInt,hDlg,PRINCIPLE,0,0 ;retrieve as an integer mov mortgage,eax ;store it or eax,eax ;check if input > 0 ja @F ;jump if OK lea eax,princerr ;error message for input of principal inputerror: invoke MessageBox,0,eax,ADDR badinput,MB_OK ;display error message ret ;****************************************************** ;***** Retrieve the NUMBER OF YEARS input ***** ;****************************************************** @@: invoke GetDlgItemInt,hDlg,YEARS,0,0 ;retrieve as an integer mul factor12 ;convert it to months mov months,eax ;store it or eax,eax ;check if input > 0 ja @F ;jump if OK lea eax,yearerr ;error message for input of years jmp inputerror ;************************************************** ;***** Retrieve the ANNUAL RATE input ***** ;************************************************** @@: invoke GetDlgItemText,hDlg,RATEPCT,ADDR buffer1,16 ;retrieve as string .if buffer1 == 0 ;buffer1 being a global BYTE variable ;if there was no input in the EDIT control, ;the first byte of buffer1 would be 0 badrate: lea eax,raterr ;error message for annual rate input jmp inputerror .endif call atofl ;convert ASCII string (%) to REAL10 decimal ;->st(0)=decimal annual rate (if no error) ; Note: The "atofl" sub-routine is called only once ; and its code could have been inserted directly ; without the need for the "call" instruction. This ; was done for the purpose of clarity in this section. or eax,eax ;check if error was detected jz badrate ;abort and display message if error ;************************************************************* ;***** Convert the annual rate to a MONTHLY rate ***** ;************************************************************* .if radiobutton == AMERICAN fidiv factor12 ;divide the annual rate by 12 ;-> st(0)=monthly rate .else fld1 ;-> st(0)=1, st(1)=annual rate fchs ;-> st(0)=-1, st(1)=annual rate fxch ;-> st(0)=annual rate, st(1)=-1 scaling factor fscale ;divide the annual rate by 2^1 ;to get the semi-annual rate ;(a positive scaling factor will multiply ;a negative scaling factor will divide) fstp st(1) ;overwrite the scaling factor with st(0) ;and pop the top register ;-> st(0)=semi-annual rate fld1 ;-> st(0)=1, st(1)=semi-annual rate fidiv factor6 ;-> st(0)=1/6, st(1)=semi-annual rate fxch ;-> st(0)=semi-annual rate, st(1)=1/6 fyl2xp1 ;-> st(0)=[log2(semi-annual rate+1)]*1/6 ;Note: Because of limitations imposed on input for size and sign, ;the (semi-annual rate+1) term will be between 1 and 1.5 ;and its log2 will be positive and less than 1. That log2 ;is further divided by 6 for a value definitely between ;0 and +1. That value can thus be used directly with ;the next instruction without any need for scaling before ;or after. f2xm1 ;-> st(0)=monthly rate (obtained directly ;because the instruction provides the minus 1) ;When this monthly rate is compounded 6 times ; it would be equal to the semi-annual rate .endif ;************************************************ ;***** Compute the monthly payments ***** ;************************************************ fild months ;-> st(0)=months, st(1)=monthly rate (R) fld st(1) ;-> st(0)=R, st(1)=months, st(2)=R fyl2xp1 ;-> st(0)=log2(1+R)*months, st(1)=R fld st ;-> st(0)=log, st(1)=log, st(2)=R frndint ;-> st(0)=int(log), st(1)=log, st(2)=R fsub st(1),st ;-> st(0)=int(log), st(1)=log-int(log), st(2)=R fxch st(1) ;-> st(0)=log-int(log), st(1)=int(log), st(2)=R f2xm1 ;-> st(0)=2[log-int(log)]-1, st(1)=int(log), st(2)=R fld1 fadd ;-> st(0)=2[log-int(log)], st(1)=int(log), st(2)=R fscale ;-> st(0)=(1+R)N, st(1)=int(log), st(2)=R fstp st(1) ;-> st(0)=(1+R)N, st(1)=R fld st ;-> st(0)=(1+R)N, st(1)=(1+R)N, st(2)=R fld1 ;-> st(0)=1, st(1)=(1+R)N, st(2)=(1+R)N, st(3)=R fsub ;-> st(0)=(1+R)N-1, st(1)=(1+R)N, st(2)=R fdiv ;-> st(0)=(1+R)N/[(1+R)N-1], st(1)=R fmul ;-> st(0)=R*(1+R)N/[(1+R)N-1] fimul mortgage ;-> st(0)=P*R*(1+R)N/[(1+R)N-1]=Monthly payments fimul factor10 ;multiply by 100 to have 2 decimal places as integer fimul factor10 ;-> st(0)=Monthly payments*100 fbstp bcdtemp ;store in memory in BCD format ;rounded to the closest penny ;************************************************************************* ; The following section of code is not necessary for this application ; because every precaution was taken to examine the input to insure that ; the data used in the FPU computations is valid and would not result in ; any major error. It is merely included to indicate how to check the ; validity of the end result whenever there may be a risk of invalid data. ; It also insures that the FBSTP instruction is completed before starting ; to access the stored packed BCD data. ;************************************************************************* fstsw ax ;copy to AX the FPU's Status Word ;containing the exception flags fwait ;insure the execution is completed shr eax,1 ;transfer bit0 to the CPU's carry flag ;That bit would be set if an invalid operation ;was detected with any of the FPU instructions ;The final result would then be invalid jnc @F ;continue if no invalid operation flag lea eax,invalid jmp inputerror ;display error message and return to WndProc ;**************************************************************** ;***** Unpack the BCD result and display it ***** ; ; The coding used is not suitable as a general purpose unpacking ; algorithm for at least two reasons: the sign is known to be ; positive and thus disregarded, and the result was designed to ; always contain 2 decimal places. ;*************************************************************** @@: push esi ;preserve ESI and EDI push edi lea esi,bcdtemp+8 ;use ESI to point initially ;to the 2nd most significant byte lea edi,buffer1 ;use EDI to point to the buffer ;where the ASCII string will be stored mov ecx,8 ;use ECX as counter for the number of bytes ;possibly containing integer digits ;Note: (The BCD format has 10 bytes. The most significant ;byte contains the sign which is disregarded in this ;application, and the least significant byte is known ;to contain two decimal digits.) ;********************************************************************** ; Search for the most significant byte containing the 1st integer digit ;********************************************************************** @@: movzx eax,byte ptr[esi] ;get next byte in AL zero extended in EAX dec esi ;adjust pointer to next byte or eax,eax ;check if it contains the 1st integer jnz @F ;jump if found dec ecx ;decrement counter jnz @B ;continue search until all integer bytes checked ;*********************************************** ; If no integer digit is present, ; place a "0" digit before the decimal delimiter ;*********************************************** mov al,"0" stosb ;insert the "0" digit jmp decimals ;go insert the decimal delimiter and digits ;******************************************************* ; If that 1st byte contains only 1 integer digit, ; it has to be processed separately to avoid a leading 0 ;******************************************************* @@: test al,0f0h ;check the high nibble of AL jnz @F ;jump if there are 2 integer digits add al,"0" ;convert the digit to ASCII stosb ;insert that digit jmp nextdigit ;process next byte ;******************************************************************* ; Other bytes contain 2 integer digits each which must be unpacked ; The high nibble digit must be followed in memory by the low nibble ;******************************************************************* @@: ror ax,4 ;transfer the high nibble to low nibble of AL ;and low nibble of AL to high nibble of AH ror ah,4 ;transfer it to the low nibble of AH add ax,3030h ;convert both to ASCII stosw ;insert both, AL followed by AH in memory nextdigit: movzx eax,byte ptr[esi] ;get next byte dec esi ;adjust pointer dec ecx ;decrement integer byte counter jnz @B ;continue with integer digits until completed decimals: mov byte ptr[edi],"." ;insert the decimal delimiter inc edi ;adjust string pointer ror ax,4 ;unpack the decimal byte as above ror ah,4 add ax,3030h stosw mov byte ptr[edi],0 ;insert the terminating 0 pop edi ;restore the EDI and ESI registers pop esi ;*********************************************************************** ; Display the result in the dialog box and return control to the WndProc ;*********************************************************************** invoke SetDlgItemText,hDlg,PAYMENT,ADDR buffer1 ret ;*************************************************************** ; atofl ; ; This sub-routine is partly general purpose and partly specific ; for the task. It parses the string for unacceptable characters ; but also returns an error if the integral portion of the input ; exceeds 100. It also returns an error for a negative sign. ; ; The conversion approach of treating all the numerical digits as ; being integer digits is sound only because the size of the ; string was limited to 9 characters in the EDIT control. This ; guarantees that, in a worse case scenario, the maximum integer ; value of the input would still fit in a 32-bit register. The ; final integer value obtained is then corrected according to the ; count of decimal digits in the input string. ; ; Returns with EAX = 0 if error detected. ; ; If EAX != 0, the converted annual rate is returned in st(0) ; already divided by 100. ;**************************************************************** atofl: push ebx ;preserve EBX and ESI push esi lea esi,buffer1 ;use ESI as pointer to text buffer xor eax,eax xor ebx,ebx ;will be used as an accumulator xor ecx,ecx ;will be used as a counter ;************************************************ ; Skip leading spaces without generating an error ;************************************************ @@: lodsb ;get next character cmp al," " ;check if a space character jz @B ;repeat until a non-space character is found ;********************************************* ; Check 1st non-space character for a +/- sign ;********************************************* cmp al,"-" ;is it a "-" sign jnz @F atoflerr: xor eax,eax ;set EAX to error code pop esi ;restore the EBX and ESI registers pop ebx ret ;return with error code @@: cmp al,"+" ;is it a "+" sign jnz nextchar lodsb ;disregard a "+" sign and get next character ;*********************************************************** ; From this point, space and sign characters will be invalid ;*********************************************************** nextchar: cmp al,0 ;check for end-of-string character jz endinput ;exit the string parsing section cmp al,"." ;is it the "." decimal delimiter ;other delimiters such as the "," used in some ;countries could also be allowed but would need ;additional coding to make it more generalized jnz @F ;****************************************************************** ; Only one decimal delimiter can be acceptable. The sign bit of ECX ; is used to keep a record of the first delimiter identified. ;****************************************************************** or ecx,ecx ;check if a delimiter has already been identified js atoflerr ;exit with error code if more than 1 delimiter stc ;set the carry flag rcr ecx,1 ;set bit31 of ECX (the sign bit) when ;the 1st delimiter is identified lodsb ;get next character jmp nextchar ;continue parsing ;*********************************************************************** ; All ASCII characters other than the numerical ones will now be invalid ;*********************************************************************** @@: cmp al,"0" jb atoflerr cmp al,"9" ja atoflerr sub al,"0" ;convert valid ASCII numerical character to binary xchg eax,ebx ;get the accumulated integer value in EAX ;holding the new digit in EBX mul factor10 ;multiply the accumulated value by 10 add eax,ebx ; and add the new digit xchg eax,ebx ;store this new accumulated value back in EBX or ecx,ecx ;check if a decimal delimiter detected yet js @F ;jump if decimal digits are being processed ;************************************* ; Integer digits still being processed ;************************************* cmp ebx,100 ;verify current value of integer portion ja atoflerr ;abort if input for annual rate is > 100% lodsb ;get next string character jmp nextchar ;continue processing string characters ;******************************************************* ; The CL register is used as a counter of decimal digits ; after the decimal delimiter has been identified ;******************************************************* @@: inc cl ;increment count of decimal digits lodsb ;get next string character jmp nextchar ;continue processing string characters ;*********************************** ; Parsing of the string is completed ;*********************************** endinput: or ebx,ebx ;check if total input was equal to 0 jz atoflerr ;abort if annual rate input is 0% finit ;initialize FPU push ebx ;store value of EBX on stack fild dword ptr[esp] ;-> st(0)=EBX add cl,2 ;increment the number of decimal digits ;to convert from % rate to a decimal rate shl ecx,1 ;get rid of the potential sign "flag" shr ecx,1 ;restore the count of decimal digits fild factor10 ;-> st(0)=10, st(1)=EBX @@: fdiv st(1),st ;-> st(0)=10, st(1)=EBX/10 dec ecx ;decrement counter of decimal digits jnz @B ;continue dividing by 10 until count exhausted fstp st ;get rid of the dividing 10 in st(0) ;-> st(0)=annual rate (as a decimal rate) pop ebx ;clean CPU stack pop esi ;restore the EBX and ESI registers pop ebx or al,1 ;insure EAX != 0 (i.e. no error detected) ret ;******************************* end start
;####################################################################### ; ; Resource script for the Dialog Box ; to be used with the Mortgage Calculator ; August 2003 ; ;####################################################################### #include "\masm32\include\resource.h" 700 DIALOGEX MOVEABLE IMPURE LOADONCALL DISCARDABLE 100, 80, 131, 132, 0 STYLE DS_MODALFRAME | 0x0004 | WS_CAPTION | WS_SYSMENU | WS_VISIBLE | WS_POPUP CAPTION "Mortgage Payments" FONT 8, "MS Sans Serif", 700, 0 /*FALSE*/ BEGIN LTEXT "Mortgage principle", 701, 4,6,66,10, SS_LEFT, , 0 LTEXT "Annual rate, %", 702, 4,22,66,10, SS_LEFT, , 0 LTEXT "Number of years", 703, 4,38,66,10, SS_LEFT, , 0 LTEXT "Monthly payments", 704, 4,84,66,10, SS_LEFT, , 0 EDITTEXT 711, 78, 6,49,11, ES_RIGHT | ES_NUMBER, , 0 EDITTEXT 712, 78,22,49,11, ES_RIGHT, , 0 EDITTEXT 713, 102,38,25,11, ES_RIGHT | ES_NUMBER, , 0 CONTROL "", 714, "Edit", ES_READONLY | ES_RIGHT, 78,84,49,11, , 0 CONTROL "Compute", 720, "Button", BS_DEFPUSHBUTTON, 16,111,41,13, , 0 CONTROL "Exit", 721, "Button", 0, 74,111,41,13, , 0 CONTROL "U.S.A.", 730, "Button", BS_AUTORADIOBUTTON | WS_GROUP, 18,62,44,10, ,0 CONTROL "Canada", 731, "Button", BS_AUTORADIOBUTTON, 75,62,44,10, , 0 END