From 426ac02893caf8103816ca624629e8fa197ba990 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Wed, 27 May 2026 18:03:35 +1000 Subject: [PATCH] add bank-account exercise --- bin/detect-unknown-words | 10 +- config.json | 15 +- .../bank-account/.docs/instructions.md | 10 ++ .../bank-account/.docs/introduction.md | 20 +++ .../practice/bank-account/.meta/config.json | 17 ++ .../bank-account/.meta/example.factor | 49 ++++++ .../practice/bank-account/.meta/generator.jl | 44 +++++ .../practice/bank-account/.meta/tests.toml | 61 +++++++ .../bank-account/bank-account-tests.factor | 160 ++++++++++++++++++ .../bank-account/bank-account.factor | 22 +++ .../exercism-tools/exercism-tools.factor | 37 ++++ .../practice/nth-prime/.meta/example.factor | 8 +- .../nth-prime/nth-prime/nth-prime.factor | 4 +- 13 files changed, 447 insertions(+), 10 deletions(-) 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.factor create mode 100644 exercises/practice/bank-account/.meta/generator.jl create mode 100644 exercises/practice/bank-account/.meta/tests.toml create mode 100644 exercises/practice/bank-account/bank-account/bank-account-tests.factor create mode 100644 exercises/practice/bank-account/bank-account/bank-account.factor create mode 100644 exercises/practice/bank-account/exercism-tools/exercism-tools.factor diff --git a/bin/detect-unknown-words b/bin/detect-unknown-words index df0b85a..2152d1f 100755 --- a/bin/detect-unknown-words +++ b/bin/detect-unknown-words @@ -88,16 +88,20 @@ def parse_solution(paths): text = '\n'.join(p.read_text() for p in paths if p.exists()) defined, locals_ = set(), set() - # Top-level word definitions: `:` and `::`. + # Top-level word definitions: `:`, `::`, `TYPED:`, `TYPED::`. for m in re.finditer(r'^::?\s+(\S+)\s*\(', text, re.M): defined.add(m.group(1)) + for m in re.finditer(r'^TYPED::?\s+(\S+)\s*\(', text, re.M): + defined.add(m.group(1)) # Locals-bearing definitions: # `:: ( ... -- ... )` # `M:: ( ... -- ... )` + # `TYPED:: ( name: class ... -- ... )` # The names aren't "defined" by `M::` (the generic was declared - # elsewhere), but the parameter list does introduce locals. + # elsewhere), but the parameter list does introduce locals. For + # `TYPED::` the class names also land in `locals_`, harmlessly. for m in re.finditer( - r'^(?:::\s+\S+|M::\s+\S+\s+\S+)\s*\(([^)]*)--[^)]*\)', + r'^(?:::\s+\S+|M::\s+\S+\s+\S+|TYPED::\s+\S+)\s*\(([^)]*)--[^)]*\)', text, re.M, ): for tok in m.group(1).split(): diff --git a/config.json b/config.json index 0ea8523..4106260 100644 --- a/config.json +++ b/config.json @@ -1625,6 +1625,18 @@ ], "difficulty": 6 }, + { + "slug": "bank-account", + "name": "Bank Account", + "uuid": "d17aa096-63a8-4855-a8ee-f906a993a916", + "practices": [], + "prerequisites": [ + "concurrency", + "errors", + "generics" + ], + "difficulty": 7 + }, { "slug": "book-store", "name": "Book Store", @@ -1716,7 +1728,8 @@ "higher-order-sequences", "curry-compose-fry", "errors", - "indexed-iteration" + "indexed-iteration", + "generics" ], "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 0000000..7398fbe --- /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 are 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 0000000..650b5d9 --- /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 0000000..ba70ff5 --- /dev/null +++ b/exercises/practice/bank-account/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "bank-account/bank-account.factor" + ], + "test": [ + "bank-account/bank-account-tests.factor" + ], + "example": [ + ".meta/example.factor" + ] + }, + "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.factor b/exercises/practice/bank-account/.meta/example.factor new file mode 100644 index 0000000..8383296 --- /dev/null +++ b/exercises/practice/bank-account/.meta/example.factor @@ -0,0 +1,49 @@ +USING: accessors concurrency.locks kernel math typed ; +IN: bank-account + +TUPLE: bank-account open? balance lock ; + +: ( -- account ) + bank-account new + f >>open? + 0 >>balance + >>lock ; + +: check-open ( account -- ) + open?>> [ "account not open" throw ] unless ; + +: check-positive ( amount -- ) + 0 <= [ "amount must be greater than 0" throw ] when ; + +TYPED:: open-account ( account: bank-account -- ) + account lock>> [ + account open?>> [ "account already open" throw ] when + account t >>open? 0 >>balance drop + ] with-lock ; + +TYPED:: close-account ( account: bank-account -- ) + account lock>> [ + account check-open + account f >>open? drop + ] with-lock ; + +TYPED:: balance ( account: bank-account -- n: integer ) + account lock>> [ + account check-open + account balance>> + ] with-lock ; + +TYPED:: deposit ( amount: integer account: bank-account -- ) + account lock>> [ + account check-open + amount check-positive + account [ amount + ] change-balance drop + ] with-lock ; + +TYPED:: withdraw ( amount: integer account: bank-account -- ) + account lock>> [ + account check-open + amount check-positive + amount account balance>> > [ "amount must be less than balance" throw ] when + account [ amount - ] change-balance drop + ] with-lock ; diff --git a/exercises/practice/bank-account/.meta/generator.jl b/exercises/practice/bank-account/.meta/generator.jl new file mode 100644 index 0000000..70bfc79 --- /dev/null +++ b/exercises/practice/bank-account/.meta/generator.jl @@ -0,0 +1,44 @@ +module BankAccount + +const EXTRA_VOCABS = ["concurrency.combinators", "locals", "sequences"] + +function fmt_op(op) + operation = op["operation"] + if operation == "open" + return "a open-account" + elseif operation == "close" + return "a close-account" + elseif operation == "deposit" + return "$(op["amount"]) a deposit" + elseif operation == "withdraw" + return "$(op["amount"]) a withdraw" + elseif operation == "balance" + return "a balance" + elseif operation == "concurrent" + inner = join(map(fmt_op, op["operations"]), " ") + return "$(op["number"]) [ drop $(inner) ] parallel-each" + else + error("unknown operation: " * operation) + end +end + +function gen_test_case(case) + ops = case["input"]["operations"] + expected = case["expected"] + body = join(map(fmt_op, ops), "\n ") + if expected isa AbstractDict # { "error": "..." } — the last op throws + return """[ + [let :> a + $(body) + ] +] must-fail""" + else # the final `balance` op leaves the number + return """{ $(expected) } [ + [let :> a + $(body) + ] +] unit-test""" + end +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 0000000..4e42d4d --- /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/bank-account-tests.factor b/exercises/practice/bank-account/bank-account/bank-account-tests.factor new file mode 100644 index 0000000..f313af6 --- /dev/null +++ b/exercises/practice/bank-account/bank-account/bank-account-tests.factor @@ -0,0 +1,160 @@ +USING: bank-account concurrency.combinators exercism-tools io kernel locals sequences tools.test unicode ; +IN: bank-account.tests + +"Newly opened account has zero balance" description +{ 0 } [ + [let :> a + a open-account + a balance + ] +] unit-test + +STOP-HERE + +"Single deposit" description +{ 100 } [ + [let :> a + a open-account + 100 a deposit + a balance + ] +] unit-test + +"Multiple deposits" description +{ 150 } [ + [let :> a + a open-account + 100 a deposit + 50 a deposit + a balance + ] +] unit-test + +"Withdraw once" description +{ 25 } [ + [let :> a + a open-account + 100 a deposit + 75 a withdraw + a balance + ] +] unit-test + +"Withdraw twice" description +{ 0 } [ + [let :> a + a open-account + 100 a deposit + 80 a withdraw + 20 a withdraw + a balance + ] +] unit-test + +"Can do multiple operations sequentially" description +{ 20 } [ + [let :> a + a open-account + 100 a deposit + 110 a deposit + 200 a withdraw + 60 a deposit + 50 a withdraw + a balance + ] +] unit-test + +"Cannot check balance of closed account" description +[ + [let :> a + a open-account + a close-account + a balance + ] +] must-fail + +"Cannot deposit into closed account" description +[ + [let :> a + a open-account + a close-account + 50 a deposit + ] +] must-fail + +"Cannot deposit into unopened account" description +[ + [let :> a + 50 a deposit + ] +] must-fail + +"Cannot withdraw from closed account" description +[ + [let :> a + a open-account + a close-account + 50 a withdraw + ] +] must-fail + +"Cannot close an account that was not opened" description +[ + [let :> a + a close-account + ] +] must-fail + +"Cannot open an already opened account" description +[ + [let :> a + a open-account + a open-account + ] +] must-fail + +"Reopened account does not retain balance" description +{ 0 } [ + [let :> a + a open-account + 50 a deposit + a close-account + a open-account + a balance + ] +] unit-test + +"Cannot withdraw more than deposited" description +[ + [let :> a + a open-account + 25 a deposit + 50 a withdraw + ] +] must-fail + +"Cannot withdraw negative" description +[ + [let :> a + a open-account + 100 a deposit + -50 a withdraw + ] +] must-fail + +"Cannot deposit negative" description +[ + [let :> a + a open-account + -50 a deposit + ] +] must-fail + +"Can handle concurrent transactions" description +{ 0 } [ + [let :> a + a open-account + 1000 [ drop 1 a deposit 1 a withdraw ] parallel-each + a balance + ] +] unit-test diff --git a/exercises/practice/bank-account/bank-account/bank-account.factor b/exercises/practice/bank-account/bank-account/bank-account.factor new file mode 100644 index 0000000..1caa4bd --- /dev/null +++ b/exercises/practice/bank-account/bank-account/bank-account.factor @@ -0,0 +1,22 @@ +USING: kernel math typed ; +IN: bank-account + +TUPLE: bank-account ; + +: ( -- account ) + "unimplemented" throw ; + +TYPED:: open-account ( account: bank-account -- ) + "unimplemented" throw ; + +TYPED:: close-account ( account: bank-account -- ) + "unimplemented" throw ; + +TYPED:: balance ( account: bank-account -- n: integer ) + "unimplemented" throw ; + +TYPED:: deposit ( amount: integer account: bank-account -- ) + "unimplemented" throw ; + +TYPED:: withdraw ( amount: integer account: bank-account -- ) + "unimplemented" throw ; diff --git a/exercises/practice/bank-account/exercism-tools/exercism-tools.factor b/exercises/practice/bank-account/exercism-tools/exercism-tools.factor new file mode 100644 index 0000000..428c2d3 --- /dev/null +++ b/exercises/practice/bank-account/exercism-tools/exercism-tools.factor @@ -0,0 +1,37 @@ +USING: accessors command-line continuations debugger io kernel + lexer namespaces sequences source-files.errors.debugger + system tools.test vocabs vocabs.loader ; +IN: exercism-tools + +SYNTAX: STOP-HERE + lexer get [ text>> length ] keep line<< ; + +SYNTAX: TASK: + lexer get next-line ; + +! Label the test that follows with its description. The marker lets the +! wrapper strip this line from captured output and attach it to the next +! test as a name, rather than leaving it in the previous test's output. +: description ( str -- ) + "###DESC### " write print ; + +! Print one failure block in a stable, parser-friendly form. Bracketed by +! markers so a wrapper can split the stream reliably and avoid Factor's +! noisy callstack output (which is interleaved with subsequent failures). +:: print-failure ( failure -- ) + "###FAIL_BEGIN###" print + failure error-location print + failure error>> [ error. ] [ 2drop ] recover + "###FAIL_END###" print + flush ; + +: print-failures ( -- ) + test-failures get [ print-failure ] each ; + +: run-exercism-tests ( -- ) + command-line get first + [ require ] [ test ] bi + test-failures get empty? + [ 0 exit ] [ print-failures 1 exit ] if ; + +MAIN: run-exercism-tests diff --git a/exercises/practice/nth-prime/.meta/example.factor b/exercises/practice/nth-prime/.meta/example.factor index 1bb5a81..4f3ce11 100644 --- a/exercises/practice/nth-prime/.meta/example.factor +++ b/exercises/practice/nth-prime/.meta/example.factor @@ -1,9 +1,9 @@ -USING: kernel math math.primes ; +USING: kernel math math.primes typed ; IN: nth-prime : after-prime ( n -- p ) 1 + dup prime? [ after-prime ] unless ; -: nth-prime ( n -- prime ) - dup 0 = [ "there is no zeroth prime" throw ] when - 1 swap [ after-prime ] times ; +TYPED:: nth-prime ( n: integer -- prime: integer ) + n 0 = [ "there is no zeroth prime" throw ] when + 1 n [ after-prime ] times ; diff --git a/exercises/practice/nth-prime/nth-prime/nth-prime.factor b/exercises/practice/nth-prime/nth-prime/nth-prime.factor index 671f06a..85315e1 100644 --- a/exercises/practice/nth-prime/nth-prime/nth-prime.factor +++ b/exercises/practice/nth-prime/nth-prime/nth-prime.factor @@ -1,5 +1,5 @@ -USING: kernel ; +USING: kernel math typed ; IN: nth-prime -: nth-prime ( n -- prime ) +TYPED:: nth-prime ( n: integer -- prime: integer ) "unimplemented" throw ;