Created
February 10, 2012 18:12
-
-
Save actsasgeek/1791374 to your computer and use it in GitHub Desktop.
fanstasy banking assignment with multimethods
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (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