Mini-Project: All-Purpose Checkbook Balancer

By now, I assume you’ve abandoned Mint.com and embraced the checkbook balancer that we’ve built over the last two chapters. But there’s one feature that you might miss: the ability to track additional accounts beyond checking, savings, and your mattress. For the final version of the checkbook balancer, we’re going to add the ability to add and remove accounts. We’re also going refactor our code into classes (and into more than one file!), making it easier to think of the program in terms of separate, reasonably decoupled components.

Let’s start by installing our dependencies, which will be the same as in checkbooks2:

 $ ​​npm​​ ​​init
 $ ​​npm​​ ​​install​​ ​​--save​​ ​​inquirer
 $ ​​npm​​ ​​install​​ ​​--save​​ ​​numeral
 $ ​​npm​​ ​​install​​ ​​--save​​ ​​jsonfile

We’re going to have a reasonably concise checkbooks3.coffee, containing only our application’s core logic, by require-ing three other project modules that we’re going to define separately.

 inquirer = require(​'inquirer'​)
 
 Account = require(​'./account'​)
 PromptFactory = require(​'./promptFactory'​)
 utils = require(​'./utils'​)
 
 # Define our logic for each prompt the user can reach
 promptFactory = ​new​ PromptFactory({allAccounts: Account.allAccounts})
 
 mainStep = ->
  inquirer.prompt promptFactory.accountPrompt(), ({account}) ->
 if​ account is null
  createAccountStep()
 else
  actionStep({account})
 
 createAccountStep = ->
  inquirer.prompt promptFactory.newAccountPrompt(), ({name}) ->
 new​ Account({balance: 0, name})
  Account.saveState()
  mainStep()
 
 actionStep = ({account}) ->
  inquirer.prompt promptFactory.actionPrompt({account}), ({action}) ->
  postActionStep({account, action})
 
 postActionStep = ({account, action}) ->
  prompts = [promptFactory.amountPrompt({action})]
 if​ action is ​'transfer'
  prompts.unshift promptFactory.toAccountPrompt({fromAccount: account})
 
  inquirer.prompt prompts, ({amount, toAccount}) ->
  amount = utils.inputToNumber(amount)
  account[action]({amount, toAccount})
  mainStep()
 
 # Load data
 Account.loadState()
 
 # Show the first prompt
 mainStep()

I’ve capitalized Account and PromptFactory because these are class names. The new Account class is an amalgam of the old createAccount function and our persistence logic:

 jsonfile = require(​'jsonfile'​)
 
 utils = require(​'./utils'​)
 
 # Account implements all actions that affect data
 
 class​ Account
  constructor: ({@name, @balance}) ->
  Account.allAccounts.push ​@
 return
 
  description: ->
 "​​#{​@name​}​​: ​​#{​utils.dollarsToString(@balance)​}​​"
 
  deposit: ({amount}) ->
  @balance += amount
  Account.saveState()
 @
 
  withdraw: ({amount}) ->
  @balance -= amount
  Account.saveState()
 @
 
  transfer: ({toAccount, amount}) ->
  @balance -= amount
  toAccount.balance += amount
  Account.saveState()
 @
 
  @allAccounts = []
 
  @saveState: ->
  jsonfile.writeFileSync(​'./data.json'​, Account.allAccounts)
 
  @loadState: ->
 try
  Account.allAccounts = ​for​ accountData ​in​ jsonfile.readFileSync(​'./data.json'​)
 new​ Account(accountData)
 catch​ e
  Account.allAccounts = [
 new​ Account({balance: 0, name: ​'checking'​})
 new​ Account({balance: 0, name: ​'savings'​})
 new​ Account({balance: 0, name: ​'mattress'​})
  ]
 return
 
 module.exports = Account

Some of our prompts require the data now stored as Account.allAccounts. We don’t want to create a global, but we also don’t want the repetition of passing that data every time we call a prompt function. So, I’ve attached those prompt functions as methods on a class called PromptFactory, which takes an array of accounts in its constructor and stores the reference:

 utils = require(​'./utils'​)
 
 # PromptFactory defines the presentation of each prompt in the app
 
 class​ PromptFactory
  constructor: ({@allAccounts}) ->
 
  accountPrompt: ->
  {
  name: ​'account'
  message: ​'Pick an account:'
  type: ​'list'
  choices: (​for​ account ​in​ @allAccounts
  {name: account.description(), value: account}
  ).concat({name: ​'new account'​, value: null})
  }
 
  newAccountPrompt: ->
  {
  name: ​'name'
  message: ​'Enter a name for this account:'
  type: ​'input'
  validate: (input) =>
 for​ account ​in​ @allAccounts
 if​ account.name is input
 return​ ​'That account name is already taken!'
  true
  }
 
  actionPrompt: ({account}) ->
  {
  name: ​'action'
  message: ​'Pick an action:'
  type: ​'list'
  choices: [
  {name: ​'Deposit $ into this account'​, value: ​'deposit'​}
  {name: ​'Withdraw $ from this account'​, value: ​'withdraw'​}
  {name: ​'Transfer $ to another account'​, value: ​'transfer'​}
  ]
  }
 
  toAccountPrompt: ({fromAccount}) ->
  {
  name: ​'toAccount'
  message: ​'Pick an account to transfer $ to:'
  type: ​'list'
  choices: ​for​ account ​in​ @allAccounts ​when​ account isnt fromAccount
  {name: account.description(), value: account}
  }
 
  amountPrompt: ({action}) ->
  {
  name: ​'amount'
  message: ​"Enter the amount to ​​#{​action​}​​:"
  type: ​'input'
  validate: (input) =>
 if​ isNaN(utils.inputToNumber(input))
 return​ ​'Please enter a numerical amount.'
 if​ utils.inputToNumber(input) < 0
 return​ ​'Please enter a non-negative amount.'
  true
  }
 
 module.exports = PromptFactory

All that’s left is our utility functions, which now reside in their own handy file of miscellany:

 numeral = require (​'numeral'​)
 
 # Utility functions
 
 module.exports =
  dollarsToString: (dollars) ->
  numeral(dollars).format(​'$0,0.00'​)
 
  inputToNumber: (input) ->
  parseFloat input.replace(​/[$,]/g​, ​''​), 10

And here’s what our final, feature-packed iteration of the checkbook balancer looks like:

 $ ​​coffee​​ ​​checkbooks3.coffee
 [?] Pick an account: (Use arrow keys)
 > checking: $22.00
  savings: -$5.00
  mattress: $0.00
  money market: $5.00
  hedge fund: $9,999.00
  new account
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.255.87