#!/usr/local/bin/python # hb.py - A Home Budget Management Program # Copyright (c) 2001, 2002 TundraWare Inc. All Rights Reserved. # If you don't already, learn how to use RCS for version control. # It can save your bacon if you fumble your keyboard during editing. # This is a nice way to embedd version information in your # program. In the BANNER definition below, you'll see VERSION # getting split, so I can pick up just the version number. VERSION = "$Id: hb.py,v 1.89 2002/09/08 23:31:43 tundra Exp $" ############################################################ ### Variables User Might Change ### ############################################################ #################### # Default budget file in "Name Burn Bal" format. At a minimum, # this file must have a CASH account. The budget must also have # a BALACCT account or the Balance feature will do nothing. #################### from os import getenv BASEDIR = getenv("HOME") + "/" BUDFIL = "hb.txt" BALACCT = "Savings" # Account used to Balance Budgets CASH = "Cash" # Name of cash account #################### # Default printer #################### LPTW = "lpt1:" # Default print device for Win32 LPTX = BASEDIR + "HB-Report.txt" # Default print file for non-Win32 #################### # Screen Dimensions #################### CWIDTH = 12 # Column width OHEIGHT = 25 # Output height OWIDTH = 79 # Output width #------------------- Nothing Below Here Should Need Changing ------------------# ############################################################ ### Imports ### ############################################################ # If you use 'import libname', then every feature of that # library has to be referenced in its namespace as in # 'os.path.exists("file")'. OTOH, if you import a single # part of a library like 'from string import atof', that # feature is in your namespace and you can refer to it # directly as in 'atof(x)' - not 'string.atof(x)'. import getopt import os import re from string import atof from time import ctime import sys ############################################################ ### Aliases & Redefinitions ### ############################################################ # I don't like verbs like Python's 'print' that stick # spaces into my output without my permission. There is # a way to suppress that with 'print', but I wanted to # demonstrate how object name assignment can help you alias # things for clarity and simplicity's sake. This is also # a way to be able to reference something in your namespace # that is actually in another (its library's) namespace. tprint = sys.stdout.write ############################################################ ### Constants & Literals ### ############################################################ #################### # Constants #################### FALSE = 0 == 1 # Booleans TRUE = not FALSE APL = OWIDTH/(CWIDTH * 2) # Accounts per line to display #################### # Literals #################### BKUEXT = "~" # Extension string for backup file CLSUNIX = "clear" # Clear screen on unix CLSWIN = "cls" # Clear screen on windows OSWIN32 = 'nt' # Operating system name - Win32 OSUNIX = 'posix' # Operating system name - Unix SEP = '-' * 79 # Separator bar # Regular Expression used to validate legitimate currency formats. # Will not allow negative values or leading zeros. If a decimal # point is present, requires two digits following it. ISCURRENCY = r"^(([1-9]\d*(\.\d{2})?)|(\.\d{2}))$" ############################################################ ### Prompts, & Application Strings ### ### ### ### These are separated out both for ease of maintenance ### ### as well as making it easy to later change if we want ### ### to Internationalize the code. ### ### ### ### i.e., By "destringing" the code, there are no ### ### literals embedded in the logic. Any changes to ### ### prompts (for content, language...) can be done ### ### here without touching the code proper itself. ### ############################################################ #################### # Display Strings #################### BANNER = ("HomeBudget - Version " + VERSION.split()[2] + " - " + \ ctime()).center(OWIDTH) + "\n" + \ "Copyright (c) 2001, 2002 TundraWare Inc. All Rights Reserved.".center(OWIDTH) +\ "\n" ACASH = "Available Cash: " TCASH = "Total Cash: " TDEBIT = "Total Debits: " ############################## # Common Prompt & Delimiter Strings ############################## DELIMITL = "<" # Delimiters for emphasized text DELIMITR = ">" PROMPT = "? " # Command prompt TRAILER = "...\n" # Ending for informational messages #################### # Error Messages #################### eCONTINUE = "Press Enter To Continue." eNOCASH = "Cannot Continue! - No Cash Account Defined." eNOLOAD = "Cannot Load Budget: %s%s!" eNOUNDO = "Nothing To Undo!" eUNKNOWN = "Unknown Error Occurred." #################### # Informational Messages #################### iBALANCING = "Balancing" + TRAILER iBUDGET = "Budget: " iCREDIT = "Credit" iDEBIT = "Debit" iEXITING = "Exiting" + TRAILER iLOADING = "Loading Budget From %s%s" + TRAILER iMONTHLY = "Doing Monthly Account Update" + TRAILER iPAY = "Pay" iPRINTING = "Printing To %s" + TRAILER iSAVING = "Saving Budget" + TRAILER iTRANSFER = "Transfer" iUNDO = "UnDoing Last Transaction" + TRAILER iUSAGE = "hb " + VERSION.split()[2] + \ " Copyright (c), 2001, 2002 TundraWare Inc." + \ " All Rights Reserved.\n" + \ " usage:\n hb [-d budget-directory] [-f budget-file]\n" iWHOLEAMT = "(Enter For Entire Account Balance)" #################### # Interactive User Prompts #################### pACCOUNT = "Account to %s" + PROMPT pAMOUNT = "Amount to %s" + PROMPT pBALANCING = "Balancing Budget" + TRAILER pCOMMAND = "Command" + PROMPT pLOADFROM = "Load New Budget From " + DELIMITL + "%s" + DELIMITR pPRINTTO = "Print To " + DELIMITL + "%s" + DELIMITR pTXFROM = iTRANSFER + " From" pTXTO = iTRANSFER + " To" ############################################################ ### Command Options ### ### ### ### This is a tuple of tuples containing all the options ### ### to be presented to the user. At program startup, ### ### this is read and a Jump Table is created for the ### ### Command Interpreter to use. Each option is ### ### described with a tuple in the following format: ### ### ### ### ( ("&ItemName", "Name of Main Handler Function"), ### ### (("Func to get 1st argument" ", 1st prompt"), ### ### ... ### ### ("Func to get last argument", " last prompt") ### ### ) ### ### ) ### ### ### ### The letter following the "&" is understood to be ### ### the hotkey by which the user selects that feature, ### ### so make sure they are all unique. ### ### ### ### At runtime, the Command Interpreter will first ### ### look for a command name (hotkey) it recognizes. ### ### Then, it will serially execute each argument- ### ### getting function, passing the corresponding ### ### prompt when doing so. When all the arguments ### ### have been retrieved, the Command Interpreter ### ### ships them off to the Main Handler Function ### ### in the form of a list. ### ### ### ### Each time the a Main Handler Function is called, ### ### a record of that call and it's parameters is ### ### stored on the Undo stack so that the user has ### ### unlimited levels of undo. It is the job ### ### of each Main Handler Function to implement its ### ### own correct Undo logic which is indicated by ### ### a flag setting in the invocation. ### ### At the very least, a Main Handler should be ### ### able to ignore an Undo, otherwise the function ### ### will be executed when it is called AND when ### ### it is Undone - usually not what you want. ### ### ### ### This approach makes is very easy to add new ### ### features or change the prompt string (into, say, ### ### another language) or assign a different hot key ### ### for a given feature. ### ### ### ### If you change this table, do a quick check and ### ### see if any of the constant(s) in the next section ### ### also need updating. ### ### ### ############################################################ Options = ( (("&Balance", "Balance"), (("BalAmount", ""),) ), # Explanation of an entry: # In the entry above, the feature is named "Balance". # It's user hotkey will be 'B'. # The Main Handler Function that supports it is # Balance() - At runtime, the Jump Table loader # does an eval("Balance") and thereby loads the # Jump Table with the correct object corresponding # to Balance(). # Balance() only needs one argument when it is called # and this is computed by BalAmount() which needs no # prompt string. Notice that there is a trailing # comma in this tuple. Without it, we would have # (("BalAmount", "")) # and Python would not be able to tell that this is # supposed to be a tuple of tuples. The trailing # comma tells Python to arrange the data structure # as such. In the entries below with multiple # argument handlers, this is not, strictly speaking, # required, but I put them in for consistency of style. (("&Credit", "Credit"), (("GetAccount", iCREDIT), ("GetAmount", iCREDIT),) ), (("&Debit", "Debit"), (("GetAccount", iDEBIT), ("GetDebitAmount", iDEBIT),) ), (("&Monthly", "Monthly"), ()), (("P&rint", "Print"), (("GetPrintDevice", pPRINTTO),) ), (("&Load", "Load"), (("GetBudgetFile", pLOADFROM),) ), (("&Save", "Save"), ()), (("&Transfer", "Transfer"), (("GetAccount", pTXFROM), ("GetAccount", pTXTO,), ("GetAmount", iTRANSFER )) ), (("Save+&Quit", "SaveExit"), ()), (("&Pay", "Pay"), (("GetAccount", iPAY), ("GetPayAmount", iPAY),) ), (("&Undo", "Undo"), ()), (("&!Abandon", "Exit"), ()) ) #################### # Features That Have Dependencies Or Special Behavior # # The constant(s) below, are the hotkeys for program features # that have special dependencies in order to be enabled. # They *must* be identical to the hotkey selected for the # option in the table above (the character preceded by '&'). # If the table above is changed, make sure to reflect any # relevant changes here as well. While this is a little # clumsy, it is nessary in order to keep from having to embed # literal strings in the code itself. # # BuiltJT() - the routine that builds the Jump Table for the # Command Interpreter - needs this information to determine whether # these features should be enabled or disabled for a given # budget. The information is kept here in this manner to keep # code below destringed. #################### sBALANCE = "B" ############################################################ ### Global Variables & Data Structures ### ############################################################ # Global Variables ALLDONE = FALSE # Controls exit from main execution loop DEBIT = 0 # Running debit total INPUTABORT = FALSE # Input aborted by user OSNAME = "" # Name of the operating system we're using # Global Data Structures Budget = {} # Dictionary of all account objects LoadStack = [] # Place we preserve old budgets when loading new AccountNames = [] # Account names discovered at load time JumpTable = {} # Command Interpreter dispatch UndoStack = [] # The Undo stack #---------------------------Code Begins Here----------------------------------# ############################################################ # General Note # # # # Python has no special block delimiters like {} in 'C' # # or DO-END in other languages. It uses *INDENTATION* # # to indicate block scope. Personally, I like this a # # lot - it's much harder to write ugly code in Python # # and you don't waste all those keystrokes ;) Just # # pay plenty of attention to indentation or you will # # get strange results. # ############################################################ ############################################################ ### Object Base Class Definitions ### ############################################################ #################### # Account Base Class #################### # Stuff declared in __init__() is local to a given instance # of a class. Stuff declared elsewhere in the class # is global to all instances of that class. This can bite # you if you have variables declared outside of __init__() # because one instance of a class will be changing a # variable and all other instances will see the change # as well. This can be very painful to debug if you # do this unintentionally. (DAMHIKT ;) class Account: def __init__(self): self.Burn = 0 # Periodic burn rate self.Bal = 0 # Current balance def Credit(self, amt): # Credit the account self.Bal += amt def Debit (self, amt): # Debit the account self.Bal -= amt def Monthly(self, Undo): # Monthly update of account if Undo: # Support Undoing self.Bal -= self.Burn else: self.Bal += self.Burn #################### # Jump Table Entry Base Class #################### class JumpEntry: def __init__(self): self.Handler = None # Function handling this action self.Name = "" # Name of action self.Args = [] # List of argument function-prompt pairs ############################################################ ### Main Handler Functions ### ############################################################ # There is one Main Handler Function for each feature # the program presents to the user. ############################################################ # Balance debits against savings account (if present) # # Excess available cash is put into BALACCT. If there # is a cash deficit, BALACCT is reduced accordingly to # eliminate it. # # Notice the two forms of the arguments in this function # protype. Args has no assigned value so it is *required* # to be passed at call time. Undo is not required in the call # and defaults to FALSE if not passed by the caller. def Balance(Args, Undo=FALSE): if Undo: Budget[BALACCT].Debit(Args[0]) else: # Balance the budget tprint(pBALANCING) Budget[BALACCT].Credit(Args[0]) ############################################################ # Add to an account def Credit(Args, Undo=FALSE): if Undo: Budget[Args[0]].Debit(Args[1]) else: Budget[Args[0]].Credit(Args[1]) ############################################################ # Decrement an account. A 0 amount passed into this function # is understood to mean that the entire account balance should # be debited. def Debit(Args, Undo=FALSE): if Undo: Budget[Args[0]].Credit(Args[1]) else: # Find out the amount being requested amt = Args[1] # If a 0 amount was passed it means we want to # pay off the whole account. # If amt is 0, it is logically FALSE if not amt: amt = Budget[Args[0]].Bal # What's the account balance? ir = UndoStack.pop() # Fix the Invocation Record ir[1][1] = amt UndoStack.append(ir) Budget[Args[0]].Debit(amt) # And do the math ############################################################ # Exit the program # # As a matter of clean coding style, I don't like to # just exit from a called subroutine. Instead, I change # state in the variable ALLDONE and let the main control # loop exit when control is passed back to it. # # Variables such as ALLDONE are globally *readable* # when they are instantiated in a scope higher than # the one in which it is referenced. However, if you are # going to *write* them, you have explicitly tell Python that # the variable is global or it will create a new variable with # local scope when you first write the value. That's what # 'global ALLDONE' does - it tells Python you want to # modify the global copy of ALLDONE when it is set to # TRUE at the end of the function, not create a new, local # variable called ALLDONE. def Exit(Args, Undo=FALSE): tprint(iEXITING) global ALLDONE ALLDONE = TRUE ############################################################ # Load budget from disk and initialize account objects def Load(Args, Undo=FALSE): global BASEDIR global AccountNames global Budget global JumpTable # A little Armor - Make sure BASEDIR ends with slash if BASEDIR[-1] != "/": BASEDIR = BASEDIR + "/" if Undo: # Reestablish the program state as it was before the last Load() AccountNames = LoadStack.pop() JumpTable = LoadStack.pop() Budget = LoadStack.pop() else: # If there is currently a budget, save for later undo # We also save the current command Jump Table because some # budgets having features others do not - the presence of # a BALACCT account, for instance, enables the Balance feature. # Finally, we have to save the AccountNames data structure, # because it is a globally used *sorted* list of available accounts. if Budget: LoadStack.append(Budget) LoadStack.append(JumpTable) LoadStack.append(AccountNames) # Initialize the global data structures for this budget Budget = {} AccountNames = {} # Open the new budget # The 'iLOADING % Args[0]' business is Python's version # of output formatting in the spirit of (and very similar to) # printf() and sprintf() you find in 'C'. try: tprint(iLOADING % (BASEDIR, Args[0])) f = open(BASEDIR + Args[0]) accounts = f.read().splitlines() f.close() except IOError: Error(eNOLOAD % (BASEDIR, Args[0]), Fatal=TRUE) # Intitialize all account objects for BudgetItem in accounts: # Split the text line we read from disk # into a list with three strings: [Name, Burn, Bal] x = BudgetItem.split() # Make sure dictionary index is always capitalized index = x[0].capitalize() # Instantiate a new Account object in the budget dictionary Budget[index] = Account() Budget[index].Burn = float(x[1]) # Convert the numeric strings Budget[index].Bal = float(x[2]) # to float and store # Build and sort list of available account names # So they'll display alphabetically AccountNames = Budget.keys() AccountNames.sort() # Every budget must have a cash account if not Budget.has_key(CASH): Error(eNOCASH, Fatal=TRUE) # We cannot recover from this error. # Build the Command Interpreter Jump Table for this budget BuildJT() ############################################################ # Update each account by its monthly increment # # The second entry on each line of our budget file is the # so-called monthly "Burn Rate" - It is the amount you have to # budget every month for that account. def Monthly(Args, Undo=FALSE): tprint(iMONTHLY) for x in AccountNames: Budget[x].Monthly(Undo) ############################################################ # Pay off some or all of an account. This means the account # is decremented by the desired amount and so it the CASH # account. # # If Pay() is passed 0 as the amount, it understands # this to mean paying off the entire amount in that account. # In this special case, the Undo logic of Pay() has to make sure # the amount paid off is properly preserved on the Undo stack for # potential Undoing later. This is necessary, because GetPayAmount() # sends a value of 0 if the user enters a blank line. # # When Pay() is actually dispatched out of the Jump Table, it's # Invocation Record (which is pushed on the UnDo stack), will have # 0 passed in the argument list. So, an Undo later, would do nothing # more than "UnPay" 0 Dollars (or Euros, or Yen, or Pesos...) into # the appropriate account and CASH - this is not all that useful ;) # # So, in the event we get a 0 amount passed, Pay() simply pops # its own Invocation Record off the Undo stack, loads that # record with the actual amount about to be paid, and pushes # the record back onto the Undo stack. This assures that a # subsequent Undo can "remember" how much to put back into the # account and into the CASH accounts. # # To see another way to handle this kind of situation, see how # the Balance() function is implemented without having to fiddle # with the UnDo stack. def Pay(Args, Undo=FALSE): # Find out the amount being requested amt = Args[1] if Undo: Budget[Args[0]].Credit(amt) if Args[0] != CASH: Budget[CASH].Credit(amt) else: # If a 0 amount was passed it means we want to # pay off the whole account. # If amt is 0, it is logically FALSE if not amt: amt = Budget[Args[0]].Bal # What's the account balance? ir = UndoStack.pop() # Fix the Invocation Record ir[1][1] = amt UndoStack.append(ir) Budget[Args[0]].Debit(amt) # And do the math if Args[0] != CASH: # If we are "paying" from CASH, Budget[CASH].Debit(amt) # don't debit the account twice ############################################################ # Print the budget to the file or device passed in Args def Print(Args, Undo=FALSE): if not Undo: # Without this we'd print on do *and* undo tprint(iPRINTING % Args[0]) f = open(Args[0], "w") for x in ReCalc(): f.write(x) f.write("\f") f.close() ############################################################ # Save budget to disk def Save(Args, Undo=FALSE): if not Undo: tprint(iSAVING) # All budget files are presumed to be relative to BASEDIR BUDGET = BASEDIR + BUDFIL BKU = BUDGET + BKUEXT if os.path.exists(BKU): os.remove(BKU) if os.path.exists(BUDGET): os.rename(BUDGET, BKU) f = open(BUDGET, "w") for x in AccountNames: Burn = str(Budget[x].Burn) Bal = str(Budget[x].Bal) f.write(x + (CWIDTH - len(x)) * " " + Burn + (CWIDTH - len(Burn)) * " " + Bal + (CWIDTH - len(Bal)) * " " + "\n") f.close() ############################################################ # Save budget and exit program # # We just call the other Main Handler Function that do # this for us directly (as opposed to having the Command # Interpreter do it). But, we have to send an Args argument # along because that's how it's done when these functions # are called via the Jump Table. def SaveExit(Args, Undo=FALSE): Save(Args=[]) Exit(Args=[]) ############################################################ # Transfer funds between accounts def Transfer(Args, Undo=FALSE): if Undo: Budget[Args[0]].Credit(Args[2]) Budget[Args[1]].Debit(Args[2]) else: Budget[Args[0]].Debit(Args[2]) Budget[Args[1]].Credit(Args[2]) ############################################################ # Undo the last transaction # # This simply involves popping the last Invocation # Record (if there is one) off the UnDo stack # and manually executing the Main Handler Function # found in that record with the Undo flag passed as TRUE. # (Once again, we're just doing manually what the Command # Interpreter did in the first place when the user first # did that function.) It's up to each Main Handler Function # to do the right thing with UnDo. If it does not care, # it can just ignore the UnDo calls and return quietly. def Undo(Args, Undo=FALSE): # Whoops, the Undo Stack is empty - there's nothing to undo # We'll tell the user, but it's a recoverable error if not len(UndoStack): Error(eNOUNDO, Fatal=FALSE) # There's UnDoing to be done else: tprint(iUNDO) action = UndoStack.pop() action[0](Args=action[1], Undo=TRUE) ############################################################ ### Support Functions ### ############################################################ # These are one of two kinds of functions. They are either # argument producing functions named in the Options table # above (so the Command Interpreter can get the required # arguments before calling a given Main Handler Function). # Or, they are internal routines the program needs to # do its job. ############################################################ # Figure out how out-of-balance the budget is # # We calculate this here as an argument producing function # invoked by the Command Interpreter, not in the Balance() # Main Handler Function (even though it could be done there). # By doing it here, the balance amount is pushed onto the # UnDo stack as part of invoking Balance() so we can later # 'un-Balance' if we want to. If we just had the Command # Interpreter call Balance() directly, and computed the amount # there, we'd have to fiddle with the UnDo stack to keep # track of the amount. This is exactly the scenario in Pay() # so you can see it done both ways. def BalAmount(str): return Budget[CASH].Bal - DEBIT ############################################################ # Build the runtime Jump Table from the options list # # The Jump Table is a dictionary of JumpEntry objects - # one for each main feature in the program . Each feature prompt has # a "hotkey" which is used to invoke it. The relevant hotkey is # delimited for visual emphasis. # # The contents of the Jump Table is described with *strings* in the # Options tuple above (to separate string literals from the body # of the code itelf). Here, we read in those strings and turn # them into an actual Jump Table. def BuildJT(): global JumpTable # Initialize a new Jump Table JumpTable = {} # The next line of code uses so-called 'tuple unpacking' - very handy # shorthand for tearing data structures apart. for ((name, handler), argpairs) in Options: # Instantiate a new JumpEntry je = JumpEntry() # Find the hotkey y = name.split("&") index = y[1][0].upper() # Form the prompt je.Name = y[0].lower() + DELIMITL + index + DELIMITR + y[1][1:].lower() # Find the Main Handler Function # The name of the Main Handler Function is stored in Options # as a *string*. This line evaluates the string and stores # a reference to the actual handler object so we can # execute it when we want to. Kinda nifty, no? # # This indirectly illustrates an important central idea in Python: # **** PYTHON VARIABLES ARE JUST NAMES WHICH REFER TO OBJECTS!** # These variables themselves have no type - it is the objects # that carry the type. Understanding this idea well is kinda # at the heart of "getting" Python- Most everything is # an object *reference* not a copy of the object. je.Handler = eval(handler) # Now build the list of ArgFunction-prompt pairs for z in argpairs: p = [] p.append(eval(z[0])) p.append(z[1]) je.Args.append(p) # Now add this entry to the Jump Table # Only install the Balance feature if the corresponding # BALACCT is present in the current budget. if index == sBALANCE.upper(): # list.count(val) returns how many instances of val # exist in list. An account should only appear 0 or 1 # times in AccountNames so we use the call as a logic # test. (If you look and see how AccountNames gets built # in the first place, you'll see that each entry has # to be unique - its an index into a dictionary and these # can never be duplicates.) if AccountNames.count(BALACCT): JumpTable[index] = je else: JumpTable[index] = je ############################################################ # Error display handler def Error(msg=eUNKNOWN, Fatal=TRUE): tprint(msg + "\n") if Fatal: sys.exit() else: UserInput(eCONTINUE + TRAILER) ############################################################ # Clear screen # # A cheater's way of clearing the screen without resorting # to (non-portable) cursor addressing libaries like 'curses'. def cls(): if OSNAME == OSUNIX: os.system(CLSUNIX) elif OSNAME == OSWIN32: os.system(CLSWIN) else: # OK, I have no idea how to do it on a Mac # so we'll just shove newlines at it until # nothing is left. Anyone who recognizes this # technique also knows what an ASR-33 is ;)) tprint("\n" * OHEIGHT) ############################################################ # Display current budget on screen # # All we do is call ReCalc which creates a list of lines # to display (or print - the Print() Main Handler Function # calls ReCalc() too) and then display them. def Display(): # Clear the screen cls() # Calculate and display the current budget for x in ReCalc(): tprint(x) # Display the command options toolbar ToolBar() ############################################################ # Ask user for an account name # # The prompt to use is supplied as an input parameter. # The user can respond with either the exact name or a # unique starting substring which identifies the account. # Return the name of the selected account. def GetAccount(Action): DONE = FALSE retval = "" while not DONE: # Ask user for account name x = UserInput(pACCOUNT % Action, "").lower() if INPUTABORT: # Did the user abort the action? DONE = TRUE else: account = [] for y in AccountNames: # See if it matches if y.lower() == x: # Exact match retval = y DONE = TRUE break else: if y.lower().startswith(x): # beginning of any account.append(y) # known account name # If we got here it means there was no exact # match. We have to make sure we have a *unique* # starting substring or we won't know which account # the user is talking about. if len(account) == 1: retval = account[0] DONE = TRUE return retval ############################################################ # Ask user for a currency amount # # The prompt to use is supplied as an input parameter. # Returns floating point value of amount specified. # # By default, AllowBlank is FALSE which means if the user # enters a blank line, they want to abort input. # # However, if AllowBlank is set TRUE, a blank line is allowed and # will force the return value of 0. This can be interpreted # by the calling function as it likes. The Pay() function, # for example, takes this to mean paying off the whole amount # in the selected account. def GetAmount(Action, AllowBlank = FALSE): global INPUTABORT DONE = FALSE while not DONE: # If we are allowing blank lines, don't set abort flag, # but return a value of 0. # # The 'AllowBlank=AllowBlank' needs some explaining. # The left side refers to the formal parameter that # UserInput() is expecting from us. The right side # refers to the the parameter that was passed to *us* # in this routine. This is *really* crappy coding # style but I kind of backed myself into it as I # hacked away. I was going to fix it, but decided # to leave it in to illustrate this very point - # That's my story, and I'm stickin' to it. y = UserInput(pAMOUNT % Action, AllowBlank=AllowBlank) # If y is a blank line, then 'not y' will be TRUE if AllowBlank and not y: # We're allowing blank lines y = "0" DONE = TRUE elif not INPUTABORT: # They did not type a blank line DONE = IsCurrency.match(y) else: # User typed a blank - they want to abort input y = "0" DONE = TRUE return atof(y) ############################################################ # Find out what budget file the user wants to load # # A blank line from the user here means that they want # to accept the current default value for a file name. def GetBudgetFile(Action): global BUDFIL while 1: BUDFIL = UserInput(Action % BUDFIL + PROMPT, BUDFIL, AllowBlank=TRUE) if INPUTABORT: return "" else: if os.path.exists(BASEDIR + BUDFIL): return BUDFIL else: Error(eNOLOAD % (BASEDIR, BUDFIL), Fatal=FALSE) ############################################################ # Get the amount to debit # # If the user just hits Enter, then debit the entire amount in that # account. This function is nothing more than a wrapper to # GetAmount() with the proper flags set. def GetDebitAmount(Action): return GetAmount(Action + "\n" + iWHOLEAMT, AllowBlank = TRUE) ############################################################ # Get the amount to pay # # If the user just hits Enter, then pay the entire amount in that # account. This function is nothing more than a wrapper to # GetAmount() with the proper flags set. def GetPayAmount(Action): return GetAmount(Action + "\n" + iWHOLEAMT, AllowBlank = TRUE) ############################################################ # Find out what printer or file the user want to print to # # A blank line from the user here means they want to # accept the current default printer or file name. # If the user enters something other than a device name, # we will 'print' to a file. # # Under WinDoze, the intial default is the first printer device, # LPT1: Under anything else, the default is a report file. def GetPrintDevice(Action): global LPTW while 1: LPTW = UserInput(Action % LPTW + PROMPT, LPTW, AllowBlank=TRUE) if INPUTABORT: return "" else: return LPTW ############################################################ # Calculate the current budget and return as a list of strings # # We can then display, print, or save these strings as desired. # # I'm fond of the Python idiom: 'string' * number' which returns # a new string of 'number' copies of 'string' concatenated together. # # Notice that Python strings are *objects* which have methods. # This is very handy. You can do things like "mystring".center(80) # or "mystring".upper() or you can do the same to string variables # like 'stringvar.lower()'. Mighty useful... def ReCalc(): global DEBIT retval =[] retval.append(BANNER) retval.append((iBUDGET + BASEDIR + BUDFIL).center(OWIDTH)) retval.append("\n" * 5) DEBIT = 0 apl = 0 for cat in AccountNames: if cat.capitalize() != CASH: if apl == APL: retval.append("\n") apl = 0 bal = Budget[cat].Bal DEBIT += bal bal = str(bal) retval.append(cat + (CWIDTH-len(cat))*" " + bal + (CWIDTH-len(bal))*" ") apl += 1 retval.append("\n" + SEP + "\n") income = Budget[CASH].Bal retval.append(TCASH + str(income) + 4 * " " + TDEBIT + str(DEBIT) + 5 * " " + ACASH + str(income-DEBIT) + 3 * "\n") return retval ############################################################ # Display command toolbar def ToolBar(): # Display the command in alphabetic order y = JumpTable.keys() y.sort() apl = 0 for x in y: tprint(JumpTable[x].Name + " " * (CWIDTH - len(JumpTable[x].Name))) apl += 1 if apl == APL * 2: tprint("\n") apl = 0 if apl != 0: tprint("\n") ############################################################ # Primitive used by all routines asking for user input. # # By default, a blank line from the user means they # are aborting the input underway. However, if AllowBlank # is passed as TRUE, this tells the routine that a blank line # is legitimate input and to pass it back as such. def UserInput(Prompt="", Default="", AllowBlank=FALSE): global INPUTABORT INPUTABORT = FALSE x = "" # Go talk to the user # Watch for keyboard interrupts and EOFs. try: x = raw_input(Prompt) except (EOFError, KeyboardInterrupt): Exit(Args=[]) # We got some input if x: retval = x # We got a blank line else: if not AllowBlank: INPUTABORT = TRUE # User can abort w/blank line retval = Default return retval ############################################################ ### hp.py program entry point ### ############################################################ # Do some setup to prepare for program runtime. # First, we compile our regular expression string for # validating numeric input into an re object. We can then # use it to see if a given string matches our requirements. # in the GetAmount() function, you'll see this done via: # # IsCurrency.match(StringToCheck) IsCurrency = re.compile(ISCURRENCY) # Find out what operating system we're running on. Some # functions like cls() and Print() care. OSNAME = os.name # Setup the default print device/file if OSNAME != OSWIN32: # We're not printing on WinDoze LPTW = LPTX # so default to writing a file. # Command line argument processing try: opts, args = getopt.getopt(sys.argv[1:], '-d:f:') except getopt.GetoptError: tprint(iUSAGE) sys.exit(2) for opt, val in opts: if opt == "-d": BASEDIR = val if opt == "-f": BUDFIL = val # Load budget from disk. Must give it an # Args parameter to satisfy the loading function. # Load() is setup to be table driven as a Main # Handler Function. These are always invoked # with an 'Args' argument by the command interpeter. # We're just calling it here manually to get things rolling. # # Load also initializes the Command Interpreter Jump Table. # This is done at load time because some program # features are enabled/disabled on a per-budget # basis. For example, the Balance function only works # if the account named in BALACCT is present. If such an # account is not present, then Load() sees to it that the # Balance feature is not presented to the user. Load(Args=[BUDFIL]) ############################# # OK , now we're ready to actually start the program. # Run The Command Interpreter ############################# # We'll do this until something decides we're done while not ALLDONE: # Show the user the current state of the budget Display() # Wait for them to tell us what they want x = UserInput("\n" + pCOMMAND, "").upper() # If it's something we recognize, we'll do it if JumpTable.has_key(x): # First, get the function to execute func = JumpTable[x].Handler # Then, go get the arguments. # This is done by iterating through all # arg function-prompt pairs until every # function has been executed and its results # appended to the args[] list. Then we'll # ship the args off the the Main Handler Function # for this feature to finish the work. args = [] for y in JumpTable[x].Args: if not INPUTABORT: # Somwhere during input, the user args.append(y[0](y[1])) # might have decided to bag it # Unless the user aborted input (thereby indicating their # desire to get out of the selected function), go do it. if not INPUTABORT: # Save the function and its arguments for potential Undo later. # Don't save the Undos themselves, however. if JumpTable[x].Handler != Undo: UndoStack.append([func, args]) # Actually execute the request func(Args=args) # So, that's it - if you have comments or questions, ship 'em # off to: tundra@tundraware.com # Thank you for playing...