Skip to content

Instantly share code, notes, and snippets.

@actsasgeek
Created February 10, 2012 18:12
Show Gist options
  • Select an option

  • Save actsasgeek/1791374 to your computer and use it in GitHub Desktop.

Select an option

Save actsasgeek/1791374 to your computer and use it in GitHub Desktop.
fanstasy banking assignment with multimethods
(ns banking)
(defn zip [& rest] (apply map vector rest))
(defn rand-in-range [x y]
"returns a random int in the range x (inclusive) and y (exclusive)"
(+ x (rand-int (- y x))))
(defn rand-date [start-date end-date]
"generates a random date between the start-date and the end-date. Each should be a string
representation of the date in MM/dd/yyy format"
(let [
date-formatter (java.text.SimpleDateFormat. "MM/dd/yyyy")
start-millis (.getTime (.parse date-formatter start-date))
end-millis (.getTime (.parse date-formatter end-date))]
(java.util.Date. (long (+ start-millis (* (rand) (- end-millis start-millis)))))))
(def year-in-millis (* 1000 60 60 24 365.0))
(defn years-passed [first-date second-date]
(/ (- (.getTime second-date) (.getTime first-date)) year-in-millis))
;; Thanks to Stuart Halloway.
(defn in [number lower upper]
(and (number? number) (<= lower number upper)))
;; there's one of these in unchecked arithmetic but not in core.
(defn negate [x] (- 0 x))
;; NB: you should generally not use standard IEEE floating point numbers to represent monetary values.
(def starting-balance 500.00)
(def account-types [:checking, :savings, :money-market])
(def overdraft-fund-initial-balance 100000.00)
(defn account [account-id transaction-count balance date-opened type]
{:id account-id, :transaction-count transaction-count, :balance balance, :bonus-balance balance, :date-opened date-opened :type type})
; user=> (make-random-account 10 "01/01/2001" "01/01/2012")
; {:id 10, :transaction-count 0, :balance 500.0, :date-opened #<Date Mon Aug 19 10:11:44 EDT 2002>, :type :money-market}
(defn make-random-account [account-id start-date end-date]
(let [transaction-count 0]
(account account-id transaction-count starting-balance (rand-date start-date end-date) (rand-nth account-types))))
(defn bank [accounts] { :accounts accounts, :overdraft-fund overdraft-fund-initial-balance})
; user=> (make-bank 2 "01/01/2001" "01/01/2012")
; {:accounts [{:id 0, :transaction-count 0, :balance 500.0, :date-opened #<Date Mon Mar 06 22:02:08 EST 2006>, :type :savings} {:id 1, :transaction-count 0, :balance 500.0,
; :date-opened #<Date Sat Sep 09 02:28:48 EDT 2006>, :type :money-market}], :overdraft-fund 100000.0}
(defn make-bank [n start-date end-date]
(let [
account-ids (range 0 n)
accounts (into [] (map #(make-random-account % start-date end-date) account-ids))]
(bank accounts)))
(def minimum-transaction 100.00)
(def maximum-transaction 500.00)
(defn transaction [account-id amount] { :account-id account-id, :amount amount})
; user=> (make-random-transaction 2)
; {:account-id 2, :amount 392.0}
(defn make-random-transaction [account-id]
(let [
value (rand-in-range minimum-transaction maximum-transaction)
is-withdrawl (< (rand) 0.5)]
(transaction account-id (if is-withdrawl (negate value) value))))
(defn update-accounts [account new-balance new-overdraft-fund]
{ :updated-account (assoc account :balance new-balance), :updated-overdraft-fund new-overdraft-fund})
;; as with most (all?) dynamic languages, the code is backwards.
;; Basically, we want to calculate three values for each transaction:
;; the net change to the balance as a result of the transaction, the
;; interest earned (if any) and the bonus (if any).
;;
(def minimum-balance -1000.00)
(def default-service-charge -10.00)
(def overdraft-rates { :checking 0.10, :savings 0.075, :money-market 0.05})
; user=> a
; {:id 0, :transaction-count 0, :balance 500.0, :date-opened #<Date Tue Mar 25 06:47:30 EST 2003>, :type :money-market}
; user=> (dispatch-for-calculate-overdraft-balance a {:account-id 0, :amount -2000.00} 100000)
; :overdraft-not-covered
; user=> (dispatch-for-calculate-overdraft-balance a {:account-id 0, :amount -20.00} 100000)
; :overdraft-covered
; user=> (dispatch-for-calculate-overdraft-balance a {:account-id 0, :amount -20.00} 10)
; :overdraft-not-covered
(defn calculate-overdraft [balance amount]
(if (> balance 0.0) (+ balance amount) amount))
(defn dispatch-for-calculate-overdraft-balance [{:keys [balance]} {:keys [amount]} overdraft-fund]
(let [
overdraft (calculate-overdraft balance amount)
estimated-new-balance (+ balance amount)
not-reached-overdraft-limit? (< minimum-balance estimated-new-balance)
overdraft-fund-not-maxed? (> overdraft-fund (Math/abs overdraft))]
(if (and not-reached-overdraft-limit? overdraft-fund-not-maxed?) :overdraft-covered :overdraft-not-covered)))
(defmulti calculate-overdraft-balance dispatch-for-calculate-overdraft-balance)
(defmethod calculate-overdraft-balance :overdraft-covered [{:keys [balance type] :as account} {:keys [amount]} overdraft-fund]
(let [overdraft (calculate-overdraft balance amount)]
(update-accounts account (+ balance (* amount (+ 1.0 (overdraft-rates type)))) (+ overdraft-fund overdraft))))
(defmethod calculate-overdraft-balance :overdraft-not-covered [{:keys [balance] :as account} {:keys [amount]} overdraft-fund]
(update-accounts account (+ balance default-service-charge) overdraft-fund))
;; The calculate-new-balance multimethods handle calculating the new balance for the account
;; based on the transaction and handle any overdraft charges depending on whether the overdraft
;; is permitted or not.
(defn dispatch-for-calculate-new-balance [{:keys [balance]} {:keys [amount]} overdraft-fund]
(let [
estimated-new-balance (+ balance amount)]
(if (>= estimated-new-balance 0.0) :funded :unfunded)))
(defmulti calculate-new-balance dispatch-for-calculate-new-balance)
(defmethod calculate-new-balance :funded [{:keys [balance] :as account} {:keys [amount]} overdraft-fund]
(update-accounts account (+ balance amount) overdraft-fund))
(defmethod calculate-new-balance :unfunded [account transaction overdraft-fund]
(calculate-overdraft-balance account transaction overdraft-fund))
(defn post-transaction [account transaction overdraft-fund]
(let [{:keys [updated-account updated-overdraft-fund]} (calculate-new-balance account transaction overdraft-fund)]
{ :updated-account (assoc updated-account :transaction-count (inc (:transaction-count updated-account))), :updated-overdraft-fund updated-overdraft-fund}))
;; The calculate-interest multimethods calculate the interest payment on positive balances
;; every 25 transactions.
;;
;; Different types of accounts get different interest rates every 25 transactions.
;;
;; interest-rate
;; :checking 0.02
;; :savings 0.04
;; :money-market 0.06
;;
;; the dispatch is on interest/no-interest not the account. The interest rate is looked up
;; in map because the interest rate is used elsewhere.
(defn dispatch-for-calculate-interest [{:keys [transaction-count balance]}]
(let [
time-for-interest? (and (> transaction-count 0) (= 0 (rem transaction-count 25)))
positive-balance? (> balance 0.0)]
(if (and time-for-interest? positive-balance?) :interest :no-interest)))
(def interest-rates { :checking 0.02, :savings 0.04, :money-market 0.06})
(defmulti calculate-interest dispatch-for-calculate-interest)
(defmethod calculate-interest :interest [{:keys [balance type]}] (* (type interest-rates) balance))
(defmethod calculate-interest :no-interest [account] 0.0)
;; the calculate-bonus multimethods calculate the bonus for the positive change in the
;; balance of the account based on
;;
;; Different ages of accounts get different bonuses every 10 transactions
;; based on the balance increase over the last 10 transactions.
;;
;; bonus-rate
;; :recent :small interest-rate/32
;; :recent :large interest-rate/24
;; :mature :small interest-rate/16
;; :mature :large interest-rate/8
;; :default 0.00
;;
;; in the case where we do pay a bonus, we need to record a new bonus balance against which
;; future bonuses will be calculated. Note that we pay a bonus on any positive change
;; even if it's simply decreasing the amount the account is overdrawn. On the flip side,
;; if you go further in debt, we don't change the bonus balance.
(defn age-of-account [years]
(cond
(in years 0 2) :new
(in years 2 5) :recent
:else :mature))
(defn bonus-balance-size [bonus-balance]
(if (< bonus-balance 500) :small :large))
(defn bonus-rate [type factor]
(/ (type interest-rates) factor))
(defn calculate-bonus-package [balance bonus-balance type factor]
{ :bonus-earned (* (bonus-rate type factor) (- balance bonus-balance)), :bonus-balance balance})
(defn dispatch-for-calculate-bonus [{:keys [type balance bonus-balance transaction-count date-opened]}]
(let [
time-for-bonus? (and (> transaction-count 0) (= 0 (rem transaction-count 10)))
change (- balance bonus-balance)
positive-change? (> change 0.0)
years (years-passed date-opened (java.util.Date.))]
(if (and time-for-bonus? positive-change?)
[(age-of-account years) (bonus-balance-size change)]
:default)))
(defmulti calculate-bonus dispatch-for-calculate-bonus)
(defmethod calculate-bonus [:recent :small] [{:keys [balance bonus-balance type]}] (calculate-bonus-package balance bonus-balance type 32))
(defmethod calculate-bonus [:recent :large] [{:keys [balance bonus-balance type]}] (calculate-bonus-package balance bonus-balance type 24))
(defmethod calculate-bonus [:mature :small] [{:keys [balance bonus-balance type]}] (calculate-bonus-package balance bonus-balance type 16))
(defmethod calculate-bonus [:mature :large] [{:keys [balance bonus-balance type]}] (calculate-bonus-package balance bonus-balance type 8))
(defmethod calculate-bonus :default [{:keys [balance bonus-balance]}] {:bonus-earned 0, :bonus-balance bonus-balance})
;; throughout the account processing code, we're passing both the account and the overdraft fund as sometimes both need to be
;; modified.
(defn process-account [account transaction overdraft-fund]
(let [
{:keys [updated-account updated-overdraft-fund]} (post-transaction account transaction overdraft-fund)
interest-earned (calculate-interest updated-account)
{:keys [bonus-earned bonus-balance]} (calculate-bonus updated-account)]
{ :updated-account (assoc updated-account :balance (+ (:balance updated-account) interest-earned bonus-earned) :bonus-balance bonus-balance), :updated-overdraft-fund updated-overdraft-fund}))
(defn apply-transaction [{:keys [accounts overdraft-fund] :as bank} {:keys [account-id amount] :as transaction}]
(let [
account (accounts account-id)
{:keys [updated-account updated-overdraft-fund]} (process-account account transaction overdraft-fund)]
(assoc bank
:overdraft-fund updated-overdraft-fund
:accounts (assoc accounts account-id updated-account))))
(defn run-fantasy-banking [n]
(let [
starting-bank (make-bank n "01/01/2000" "01/01/2012")
transactions (map make-random-transaction (take (* 100 n) (repeatedly #(rand-int n))))
ending-bank (reduce apply-transaction starting-bank transactions)
accounts (zip (:accounts starting-bank) (:accounts ending-bank))]
(do
(println "overdraft fund: " (:overdraft-fund ending-bank))
(println "id" \tab "date opened" \tab\tab\tab\tab "starting" \tab "#" \tab "ending")
(doseq [[account-start account-end] accounts]
(println (:id account-start) \tab (:date-opened account-start) \tab (:balance account-start) \tab (:transaction-count account-end) \tab (:balance account-end)))
)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment