From 3e932b8b54a3b42d21fbace7db6ec573180d5601 Mon Sep 17 00:00:00 2001 From: meatball <69751659+meatball133@users.noreply.github.com> Date: Fri, 14 Jun 2024 17:32:11 +0200 Subject: [PATCH] Implement bank account exercise (#1692) * Implement bank account * Remove ruby version change * Fix config.json file * Remove duplicate test --------- Co-authored-by: Victor Goff --- config.json | 8 ++ .../bank-account/.docs/instructions.md | 10 ++ .../bank-account/.docs/introduction.md | 20 +++ .../practice/bank-account/.meta/config.json | 15 ++ .../practice/bank-account/.meta/example.rb | 48 +++++++ .../practice/bank-account/.meta/tests.toml | 62 ++++++++ .../practice/bank-account/bank_account.rb | 7 + .../bank-account/bank_account_test.rb | 135 ++++++++++++++++++ 8 files changed, 305 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.rb create mode 100644 exercises/practice/bank-account/.meta/tests.toml create mode 100644 exercises/practice/bank-account/bank_account.rb create mode 100644 exercises/practice/bank-account/bank_account_test.rb diff --git a/config.json b/config.json index 4cf3af1049..5f1e918db3 100644 --- a/config.json +++ b/config.json @@ -1555,6 +1555,14 @@ ], "difficulty": 3 }, + { + "slug": "bank-account", + "name": "Bank Account", + "uuid": "48b91d4d-2fd4-4941-8f3f-030872daec88", + "practices": [], + "prerequisites": [], + "difficulty": 3 + }, { "slug": "gilded-rose", "name": "Gilded Rose", diff --git a/exercises/practice/bank-account/.docs/instructions.md b/exercises/practice/bank-account/.docs/instructions.md new file mode 100644 index 0000000000..0955520bbf --- /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 0000000000..650b5d9c46 --- /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 0000000000..857174088e --- /dev/null +++ b/exercises/practice/bank-account/.meta/config.json @@ -0,0 +1,15 @@ +{ + "authors": ["meatball133"], + "files": { + "solution": [ + "bank_account.rb" + ], + "test": [ + "bank_account_test.rb" + ], + "example": [ + ".meta/example.rb" + ] + }, + "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.rb b/exercises/practice/bank-account/.meta/example.rb new file mode 100644 index 0000000000..c7929af85e --- /dev/null +++ b/exercises/practice/bank-account/.meta/example.rb @@ -0,0 +1,48 @@ +class BankAccount + def initialize + @balance = 0 + @status = false + end + + def balance + unless @status + raise ArgumentError.new("You can't check the balance of a closed account") + end + @balance + end + + def open + if @status + raise ArgumentError.new("You can't open an already open account") + end + @status = true + end + + def close + unless @status + raise ArgumentError.new("You can't close an already closed account") + end + @balance = 0 + @status = false + end + + def deposit(amount) + if amount < 0 + raise ArgumentError.new("You can't deposit a negative amount") + elsif !@status + raise ArgumentError.new("You can't deposit money into a closed account") + end + @balance += amount + end + + def withdraw(amount) + if amount < 0 + raise ArgumentError.new("You can't withdraw a negative amount") + elsif amount > @balance + raise ArgumentError.new("You can't withdraw more than you have") + elsif !@status + raise ArgumentError.new("You can't withdraw money into a closed account") + end + @balance -= amount + end +end diff --git a/exercises/practice/bank-account/.meta/tests.toml b/exercises/practice/bank-account/.meta/tests.toml new file mode 100644 index 0000000000..655ea7ae5a --- /dev/null +++ b/exercises/practice/bank-account/.meta/tests.toml @@ -0,0 +1,62 @@ +# 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" +include = false \ No newline at end of file diff --git a/exercises/practice/bank-account/bank_account.rb b/exercises/practice/bank-account/bank_account.rb new file mode 100644 index 0000000000..85c4555869 --- /dev/null +++ b/exercises/practice/bank-account/bank_account.rb @@ -0,0 +1,7 @@ +=begin +Write your code for the 'Bank Account' exercise in this file. Make the tests in +`bank_account_test.rb` pass. + +To get started with TDD, see the `README.md` file in your +`ruby/bank-account` directory. +=end diff --git a/exercises/practice/bank-account/bank_account_test.rb b/exercises/practice/bank-account/bank_account_test.rb new file mode 100644 index 0000000000..6eff9d4350 --- /dev/null +++ b/exercises/practice/bank-account/bank_account_test.rb @@ -0,0 +1,135 @@ +require 'minitest/autorun' +require_relative 'bank_account' + +class BankAccountTest < Minitest::Test + def test_newly_opened_account_has_zero_balance + # skip + bank_account = BankAccount.new + bank_account.open + assert_equal bank_account.balance, 0 + end + + def test_single_deposit + skip + bank_account = BankAccount.new + bank_account.open + bank_account.deposit(100) + assert_equal bank_account.balance, 100 + end + + def test_multiple_deposits + skip + bank_account = BankAccount.new + bank_account.open + bank_account.deposit(100) + bank_account.deposit(50) + assert_equal bank_account.balance, 150 + end + + def test_withdraw_once + skip + bank_account = BankAccount.new + bank_account.open + bank_account.deposit(100) + bank_account.withdraw(75) + assert_equal bank_account.balance, 25 + end + + def test_withdraw_twice + skip + bank_account = BankAccount.new + bank_account.open + bank_account.deposit(100) + bank_account.withdraw(80) + bank_account.withdraw(20) + assert_equal bank_account.balance, 0 + end + + def test_can_do_multiple_operations_sequentially + skip + bank_account = BankAccount.new + bank_account.open + bank_account.deposit(100) + bank_account.deposit(110) + bank_account.withdraw(200) + bank_account.deposit(60) + bank_account.withdraw(50) + assert_equal bank_account.balance, 20 + end + + def test_cannot_check_balance_of_closed_account + skip + bank_account = BankAccount.new + bank_account.open + bank_account.close + assert_raises(ArgumentError, "You can't check the balance of a closed account") { bank_account.balance } + end + + def test_cannot_deposit_into_closed_account + skip + bank_account = BankAccount.new + bank_account.open + bank_account.close + assert_raises(ArgumentError, "You can't deposit money into a closed account") { bank_account.deposit(50) } + end + + def test_cannot_deposit_into_unopened_account + skip + bank_account = BankAccount.new + assert_raises(ArgumentError, "You can't deposit money into a closed account") { bank_account.deposit(50) } + end + + def test_cannot_withdraw_from_closed_account + skip + bank_account = BankAccount.new + bank_account.open + bank_account.close + assert_raises(ArgumentError, "You can't withdraw money into a closed account") { bank_account.withdraw(50) } + end + + def test_cannot_close_an_account_that_was_not_opened + skip + bank_account = BankAccount.new + assert_raises(ArgumentError, "You can't close an already closed account") { bank_account.close } + end + + def test_cannot_open_an_already_opened_account + skip + bank_account = BankAccount.new + bank_account.open + assert_raises(ArgumentError, "You can't open an already open account") { bank_account.open } + end + + def test_reopened_account_does_not_retain_balance + skip + bank_account = BankAccount.new + bank_account.open + bank_account.deposit(50) + bank_account.close + bank_account.open + assert_equal bank_account.balance, 0 + end + + def test_cannot_withdraw_more_than_deposited + skip + bank_account = BankAccount.new + bank_account.open + bank_account.deposit(25) + assert_raises(ArgumentError, "You can't withdraw more than you have") { bank_account.withdraw(50) } + end + + def test_cannot_withdraw_negative + skip + bank_account = BankAccount.new + bank_account.open + bank_account.deposit(100) + assert_raises(ArgumentError, "You can't withdraw a negative amount") { bank_account.withdraw(-50) } + end + + def test_cannot_deposit_negative + skip + bank_account = BankAccount.new + bank_account.open + assert_raises(ArgumentError, "You can't deposit a negative amount") { bank_account.deposit(-50) } + end +end