From 6bbbe1515f6a44b5cb229a668284b661a5ab4cde Mon Sep 17 00:00:00 2001 From: Kevin Marker <kmarker1101@users.noreply.github.com> Date: Thu, 13 Jun 2024 08:35:36 -0500 Subject: [PATCH] add bank-account (#401) --- config.json | 8 + .../bank-account/.docs/instructions.md | 10 ++ .../bank-account/.docs/introduction.md | 20 +++ .../practice/bank-account/.meta/config.json | 17 +++ .../practice/bank-account/.meta/example.el | 94 ++++++++++++ .../practice/bank-account/.meta/tests.toml | 61 ++++++++ .../bank-account/bank-account-test.el | 142 ++++++++++++++++++ .../practice/bank-account/bank-account.el | 22 +++ 8 files changed, 374 insertions(+) create mode 100644 exercises/practice/bank-account/.docs/instructions.md create mode 100644 exercises/practice/bank-account/.docs/introduction.md create mode 100644 exercises/practice/bank-account/.meta/config.json create mode 100644 exercises/practice/bank-account/.meta/example.el create mode 100644 exercises/practice/bank-account/.meta/tests.toml create mode 100644 exercises/practice/bank-account/bank-account-test.el create mode 100644 exercises/practice/bank-account/bank-account.el diff --git a/config.json b/config.json index a8040b26..a1375fff 100644 --- a/config.json +++ b/config.json @@ -830,6 +830,14 @@ "practices": [], "prerequisites": [], "difficulty": 5 + }, + { + "slug": "bank-account", + "name": "Bank Account", + "uuid": "edc5fa4b-35b3-4313-b4c3-16153708f0ac", + "practices": [], + "prerequisites": [], + "difficulty": 8 } ] }, diff --git a/exercises/practice/bank-account/.docs/instructions.md b/exercises/practice/bank-account/.docs/instructions.md new file mode 100644 index 00000000..0955520b --- /dev/null +++ b/exercises/practice/bank-account/.docs/instructions.md @@ -0,0 +1,10 @@ +# Instructions + +Your task is to implement bank accounts supporting opening/closing, withdrawals, and deposits of money. + +As bank accounts can be accessed in many different ways (internet, mobile phones, automatic charges), your bank software must allow accounts to be safely accessed from multiple threads/processes (terminology depends on your programming language) in parallel. +For example, there may be many deposits and withdrawals occurring in parallel; you need to ensure there is no [race conditions][wikipedia] between when you read the account balance and set the new balance. + +It should be possible to close an account; operations against a closed account must fail. + +[wikipedia]: https://en.wikipedia.org/wiki/Race_condition#In_software diff --git a/exercises/practice/bank-account/.docs/introduction.md b/exercises/practice/bank-account/.docs/introduction.md new file mode 100644 index 00000000..650b5d9c --- /dev/null +++ b/exercises/practice/bank-account/.docs/introduction.md @@ -0,0 +1,20 @@ +# Introduction + +After years of filling out forms and waiting, you've finally acquired your banking license. +This means you are now officially eligible to open your own bank, hurray! + +Your first priority is to get the IT systems up and running. +After a day of hard work, you can already open and close accounts, as well as handle withdrawals and deposits. + +Since you couldn't be bothered writing tests, you invite some friends to help test the system. +However, after just five minutes, one of your friends claims they've lost money! +While you're confident your code is bug-free, you start looking through the logs to investigate. + +Ah yes, just as you suspected, your friend is at fault! +They shared their test credentials with another friend, and together they conspired to make deposits and withdrawals from the same account _in parallel_. +Who would do such a thing? + +While you argue that it's physically _impossible_ for someone to access their account in parallel, your friend smugly notifies you that the banking rules _require_ you to support this. +Thus, no parallel banking support, no go-live signal. +Sighing, you create a mental note to work on this tomorrow. +This will set your launch date back at _least_ one more day, but well... diff --git a/exercises/practice/bank-account/.meta/config.json b/exercises/practice/bank-account/.meta/config.json new file mode 100644 index 00000000..fef55850 --- /dev/null +++ b/exercises/practice/bank-account/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "kmarker1101" + ], + "files": { + "solution": [ + "bank-account.el" + ], + "test": [ + "bank-account-test.el" + ], + "example": [ + ".meta/example.el" + ] + }, + "blurb": "Simulate a bank account supporting opening/closing, withdraws, and deposits of money. Watch out for concurrent transactions!" +} diff --git a/exercises/practice/bank-account/.meta/example.el b/exercises/practice/bank-account/.meta/example.el new file mode 100644 index 00000000..981aa6a9 --- /dev/null +++ b/exercises/practice/bank-account/.meta/example.el @@ -0,0 +1,94 @@ +;;; bank-account.el --- Bank Account (exercism) -*- lexical-binding: t; -*- + +;;; Commentary: + +;;; Code: + +(require 'eieio) + +(define-error 'account-closed "account not open") +(define-error 'account-open "account already open") +(define-error 'account-overdraw "amount must be less than balance") +(define-error 'account-negative-transaction "amount must be greater than 0") + +(defclass bank-account () + ((open + :initarg :open + :initform nil + :type boolean + :documentation "Is the account open?") + (funds + :initarg :funds + :initform 0 + :type number + :documentation "Amount of funds in the account") + (lock + :initarg :lock + :initform (bank-account--make-semaphore) + :documentation "Semaphore lock for the account")) + "Class representing a bank account.") + +(defun bank-account--make-semaphore () + (list 0 (make-mutex))) + +(defun bank-account--semaphore-post (semaphore) + (let ((count (car semaphore)) + (mutex (cadr semaphore))) + (with-mutex mutex + (setcar semaphore (1+ count))))) + +(defun bank-account--semaphore-wait (semaphore) + (let ((count (car semaphore)) + (mutex (cadr semaphore))) + (with-mutex mutex + (while (<= count 0) + (setq count (car semaphore))) + (setcar semaphore (1- count))))) + +(cl-defmethod check-open ((account bank-account)) + (unless (oref account open) + (signal 'account-closed nil))) + +(cl-defmethod check-positive ((account bank-account) amount) + (unless (> amount 0) + (signal 'account-negative-transaction nil))) + +(cl-defmethod open-account ((account bank-account)) + (if (oref account open) + (signal 'account-open nil) + (oset account open t) + (oset account funds 0) + (bank-account--semaphore-post (oref account lock)))) + +(cl-defmethod close-account ((account bank-account)) + (check-open account) + (bank-account--semaphore-wait (oref account lock)) + (oset account open nil) + (bank-account--semaphore-post (oref account lock))) + +(cl-defmethod deposit ((account bank-account) amount) + (check-open account) + (check-positive account amount) + (bank-account--semaphore-wait (oref account lock)) + (oset account funds (+ (oref account funds) amount)) + (bank-account--semaphore-post (oref account lock))) + +(cl-defmethod withdraw ((account bank-account) amount) + (check-open account) + (check-positive account amount) + (if (> amount (oref account funds)) + (signal 'account-overdraw nil) + (bank-account--semaphore-wait (oref account lock)) + (oset account funds (- (oref account funds) amount)) + (bank-account--semaphore-post (oref account lock)))) + +(cl-defmethod balance ((account bank-account)) + (check-open account) + (oref account funds)) + +(defun make-new-bank-account () + (bank-account :open nil :funds 0 :lock (bank-account--make-semaphore))) + + +(provide 'bank-account) +;;; bank-account.el ends here diff --git a/exercises/practice/bank-account/.meta/tests.toml b/exercises/practice/bank-account/.meta/tests.toml new file mode 100644 index 00000000..4e42d4dc --- /dev/null +++ b/exercises/practice/bank-account/.meta/tests.toml @@ -0,0 +1,61 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[983a1528-4ceb-45e5-8257-8ce01aceb5ed] +description = "Newly opened account has zero balance" + +[e88d4ec3-c6bf-4752-8e59-5046c44e3ba7] +description = "Single deposit" + +[3d9147d4-63f4-4844-8d2b-1fee2e9a2a0d] +description = "Multiple deposits" + +[08f1af07-27ae-4b38-aa19-770bde558064] +description = "Withdraw once" + +[6f6d242f-8c31-4ac6-8995-a90d42cad59f] +description = "Withdraw twice" + +[45161c94-a094-4c77-9cec-998b70429bda] +description = "Can do multiple operations sequentially" + +[f9facfaa-d824-486e-8381-48832c4bbffd] +description = "Cannot check balance of closed account" + +[7a65ba52-e35c-4fd2-8159-bda2bde6e59c] +description = "Cannot deposit into closed account" + +[a0a1835d-faae-4ad4-a6f3-1fcc2121380b] +description = "Cannot deposit into unopened account" + +[570dfaa5-0532-4c1f-a7d3-0f65c3265608] +description = "Cannot withdraw from closed account" + +[c396d233-1c49-4272-98dc-7f502dbb9470] +description = "Cannot close an account that was not opened" + +[c06f534f-bdc2-4a02-a388-1063400684de] +description = "Cannot open an already opened account" + +[0722d404-6116-4f92-ba3b-da7f88f1669c] +description = "Reopened account does not retain balance" + +[ec42245f-9361-4341-8231-a22e8d19c52f] +description = "Cannot withdraw more than deposited" + +[4f381ef8-10ef-4507-8e1d-0631ecc8ee72] +description = "Cannot withdraw negative" + +[d45df9ea-1db0-47f3-b18c-d365db49d938] +description = "Cannot deposit negative" + +[ba0c1e0b-0f00-416f-8097-a7dfc97871ff] +description = "Can handle concurrent transactions" diff --git a/exercises/practice/bank-account/bank-account-test.el b/exercises/practice/bank-account/bank-account-test.el new file mode 100644 index 00000000..efc588d7 --- /dev/null +++ b/exercises/practice/bank-account/bank-account-test.el @@ -0,0 +1,142 @@ +;;; bank-account-test.el --- Bank Account (exercism) -*- lexical-binding: t; -*- + +;;; Commentary: + +;;; Code: + + +(load-file "bank-account.el") + +(require 'eieio) + +(ert-deftest newly-opened-account-has-zero-balance () + (let ((account (make-new-bank-account))) + (open-account account) + (should (= (balance account) 0)))) + + +(ert-deftest single-deposit () + (let ((account (make-new-bank-account))) + (open-account account) + (deposit account 100) + (should (= (balance account) 100)))) + + +(ert-deftest multiple-deposits () + (let ((account (make-new-bank-account))) + (open-account account) + (deposit account 100) + (deposit account 50) + (should (= (balance account) 150)))) + + +(ert-deftest withdraw-once () + (let ((account (make-new-bank-account))) + (open-account account) + (deposit account 100) + (withdraw account 75) + (should (= (balance account) 25)))) + + +(ert-deftest withdraw-twice () + (let ((account (make-new-bank-account))) + (open-account account) + (deposit account 100) + (withdraw account 80) + (withdraw account 20) + (should (= (balance account) 0)))) + + +(ert-deftest can-do-multiple-operations-sequentially () + (let ((account (make-new-bank-account))) + (open-account account) + (deposit account 100) + (deposit account 110) + (withdraw account 200) + (deposit account 60) + (withdraw account 50) + (should (= (balance account) 20)))) + + +(ert-deftest cannot-check-balance-of-closed-account () + (let ((account (make-new-bank-account))) + (open-account account) + (close-account account) + (should-error (balance account) :type 'account-closed))) + + +(ert-deftest cannot-deposit-into-closed-account () + (let ((account (make-new-bank-account))) + (open-account account) + (close-account account) + (should-error (deposit account 50) :type 'account-closed))) + + +(ert-deftest cannot-deposit-into-unopened-account () + (let ((account (make-new-bank-account))) + (should-error (deposit account 50) :type 'account-closed))) + + +(ert-deftest cannot-withdraw-from-closed-account () + (let ((account (make-new-bank-account))) + (open-account account) + (close-account account) + (should-error (withdraw account 50) :type 'account-closed))) + + +(ert-deftest cannot-close-an-account-that-was-not-opened () + (let ((account (make-new-bank-account))) + (should-error (close-account account) :type 'account-closed))) + + +(ert-deftest cannot-open-an-already-opened-account () + (let ((account (make-new-bank-account))) + (open-account account) + (should-error (open-account account) :type 'account-open))) + + +(ert-deftest reopened-account-does-not-retain-balance () + (let ((account (make-new-bank-account))) + (open-account account) + (deposit account 50) + (close-account account) + (open-account account) + (should (= (balance account) 0)))) + + +(ert-deftest cannot-withdraw-more-than-deposited () + (let ((account (make-new-bank-account))) + (open-account account) + (deposit account 25) + (should-error (withdraw account 50) :type 'account-overdraw))) + + +(ert-deftest cannot-withdraw-negative () + (let ((account (make-new-bank-account))) + (open-account account) + (deposit account 100) + (should-error (withdraw account -50) :type 'account-negative-transaction))) + + +(ert-deftest cannot-deposit-negative () + (let ((account (make-new-bank-account))) + (open-account account) + (should-error (deposit account -50) :type 'account-negative-transaction))) + + +(ert-deftest can-handle-concurrent-transactions () + (let ((account (make-new-bank-account))) + (open-account account) + (defun concurrent-operation () + (deposit account 1) + (withdraw account 1)) + (let ((threads ())) + (dotimes (_ 1000) + (push (make-thread #'concurrent-operation) threads)) + (dolist (thread threads) + (thread-join thread))) + (should (= (balance account ) 0)))) + + +(provide 'bank-account-test) +;;; bank-account-test.el ends here diff --git a/exercises/practice/bank-account/bank-account.el b/exercises/practice/bank-account/bank-account.el new file mode 100644 index 00000000..d62e3211 --- /dev/null +++ b/exercises/practice/bank-account/bank-account.el @@ -0,0 +1,22 @@ +;;; bank-account.el --- Bank Account (exercism) -*- lexical-binding: t; -*- + +;;; Commentary: + +;;; Code: + + +(define-error 'account-closed + (error "Delete this S-Expression and write your own implementation")) +(define-error 'account-open + (error "Delete this S-Expression and write your own implementation")) +(define-error 'account-overdraw + (error "Delete this S-Expression and write your own implementation")) +(define-error 'account-negative-transaction + (error "Delete this S-Expression and write your own implementation")) + +(defclass bank-account (operations) + (error "Delete this S-Expression and write your own implementation")) + + +(provide 'bank-account) +;;; bank-account.el ends here