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