summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDimitri Sokolyuk <demon@dim13.org>2016-08-27 16:48:26 +0200
committerDimitri Sokolyuk <demon@dim13.org>2016-08-27 16:48:26 +0200
commitc00da54ecddabb9e218029466bb35bca040b87fb (patch)
tree9df70dc496fc81b57833025b95380ff59da66f97
parent39d4afd6d12ba6dd043763f1e65dde5fd8b99497 (diff)
Slove bank account
-rw-r--r--go/bank-account/README.md42
-rw-r--r--go/bank-account/bank_account.go48
-rw-r--r--go/bank-account/bank_account_test.go289
3 files changed, 379 insertions, 0 deletions
diff --git a/go/bank-account/README.md b/go/bank-account/README.md
new file mode 100644
index 0000000..b8f5d8b
--- /dev/null
+++ b/go/bank-account/README.md
@@ -0,0 +1,42 @@
+# Bank Account
+
+Bank accounts can be accessed in different ways at the same time.
+
+A bank account can be accessed in multiple ways. Clients can make
+deposits and withdrawals using the internet, mobile phones, etc. Shops
+can charge against the account.
+
+Create an account that can be accessed from multiple threads/processes
+(terminology depends on your programming language).
+
+It should be possible to close an account; operations against a closed
+account must fail.
+
+## Instructions
+
+Run the test file, and fix each of the errors in turn. When you get the
+first test to pass, go to the first pending or skipped test, and make
+that pass as well. When all of the tests are passing, feel free to
+submit.
+
+Remember that passing code is just the first step. The goal is to work
+towards a solution that is as readable and expressive as you can make
+it.
+
+Have fun!
+
+To run the tests simply run the command `go test` in the exercise directory.
+
+If the test suite contains benchmarks, you can run these with the `-bench`
+flag:
+
+ go test -bench .
+
+For more detailed info about the Go track see the [help
+page](http://exercism.io/languages/go).
+
+
+
+## Submitting Incomplete Problems
+It's possible to submit an incomplete solution so you can see how others have completed the exercise.
+
diff --git a/go/bank-account/bank_account.go b/go/bank-account/bank_account.go
new file mode 100644
index 0000000..bfaa894
--- /dev/null
+++ b/go/bank-account/bank_account.go
@@ -0,0 +1,48 @@
+package account
+
+import "sync"
+
+type Account struct {
+ balance int64
+ open bool
+ sync.Mutex
+}
+
+func Open(inital int64) *Account {
+ if inital < 0 {
+ return nil
+ }
+ return &Account{balance: inital, open: true}
+}
+
+func (a *Account) Close() (int64, bool) {
+ a.Lock()
+ defer a.Unlock()
+ if a.open {
+ a.open = false
+ return a.balance, true
+ }
+ return 0, false
+}
+
+func (a *Account) Balance() (int64, bool) {
+ a.Lock()
+ defer a.Unlock()
+ if a.open {
+ return a.balance, true
+ }
+ return 0, false
+}
+
+func (a *Account) Deposit(amount int64) (int64, bool) {
+ a.Lock()
+ defer a.Unlock()
+ if a.open {
+ if a.balance < -amount {
+ return 0, false
+ }
+ a.balance += amount
+ return a.balance, true
+ }
+ return 0, false
+}
diff --git a/go/bank-account/bank_account_test.go b/go/bank-account/bank_account_test.go
new file mode 100644
index 0000000..26e5b2c
--- /dev/null
+++ b/go/bank-account/bank_account_test.go
@@ -0,0 +1,289 @@
+// API:
+//
+// Open(initalDeposit int64) *Account
+// (Account) Close() (payout int64, ok bool)
+// (Account) Balance() (balance int64, ok bool)
+// (Account) Deposit(amount int64) (newBalance int64, ok bool)
+//
+// If Open is given a negative initial deposit, it must return nil.
+// Deposit must handle a negative amount as a withdrawal.
+// If any Account method is called on an closed account, it must not modify
+// the account and must return ok = false.
+
+package account
+
+import (
+ "runtime"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "time"
+)
+
+func TestSeqOpenBalanceClose(t *testing.T) {
+ // open account
+ const amt = 10
+ a := Open(amt)
+ if a == nil {
+ t.Fatalf("Open(%d) = nil, want non-nil *Account.", amt)
+ }
+ t.Logf("Account 'a' opened with initial balance of %d.", amt)
+
+ // verify balance after open
+ switch b, ok := a.Balance(); {
+ case !ok:
+ t.Fatal("a.Balance() returned !ok, want ok.")
+ case b != amt:
+ t.Fatalf("a.Balance() = %d, want %d", b, amt)
+ }
+
+ // close account
+ switch p, ok := a.Close(); {
+ case !ok:
+ t.Fatalf("a.Close() returned !ok, want ok.")
+ case p != amt:
+ t.Fatalf("a.Close() returned payout = %d, want %d.", p, amt)
+ }
+ t.Log("Account 'a' closed.")
+
+ // verify balance no longer accessible
+ if b, ok := a.Balance(); ok {
+ t.Log("Balance still available on closed account.")
+ t.Fatalf("a.Balance() = %d, %t. Want ok == false", b, ok)
+ }
+}
+
+func TestSeqOpenDepositClose(t *testing.T) {
+ // open account
+ const openAmt = 10
+ a := Open(openAmt)
+ if a == nil {
+ t.Fatalf("Open(%d) = nil, want non-nil *Account.", openAmt)
+ }
+ t.Logf("Account 'a' opened with initial balance of %d.", openAmt)
+
+ // deposit
+ const depAmt = 20
+ const newAmt = openAmt + depAmt
+ switch b, ok := a.Deposit(depAmt); {
+ case !ok:
+ t.Fatalf("a.Deposit(%d) returned !ok, want ok.", depAmt)
+ case b != openAmt+depAmt:
+ t.Fatalf("a.Deposit(%d) = %d, want new balance = %d", depAmt, b, newAmt)
+ }
+ t.Logf("Deposit of %d accepted to account 'a'", depAmt)
+
+ // close account
+ switch p, ok := a.Close(); {
+ case !ok:
+ t.Fatalf("a.Close() returned !ok, want ok.")
+ case p != newAmt:
+ t.Fatalf("a.Close() returned payout = %d, want %d.", p, newAmt)
+ }
+ t.Log("Account 'a' closed.")
+
+ // verify deposits no longer accepted
+ if b, ok := a.Deposit(1); ok {
+ t.Log("Deposit accepted on closed account.")
+ t.Fatalf("a.Deposit(1) = %d, %t. Want ok == false", b, ok)
+ }
+}
+
+func TestMoreSeqCases(t *testing.T) {
+ // open account 'a' as before
+ const openAmt = 10
+ a := Open(openAmt)
+ if a == nil {
+ t.Fatalf("Open(%d) = nil, want non-nil *Account.", openAmt)
+ }
+ t.Logf("Account 'a' opened with initial balance of %d.", openAmt)
+
+ // open account 'z' with zero balance
+ z := Open(0)
+ if z == nil {
+ t.Fatal("Open(0) = nil, want non-nil *Account.")
+ }
+ t.Log("Account 'z' opened with initial balance of 0.")
+
+ // attempt to open account with negative opening balance
+ if Open(-10) != nil {
+ t.Fatal("Open(-10) seemed to work, " +
+ "want nil result for negative opening balance.")
+ }
+
+ // verify both balances a and z still there
+ switch b, ok := a.Balance(); {
+ case !ok:
+ t.Fatal("a.Balance() returned !ok, want ok.")
+ case b != openAmt:
+ t.Fatalf("a.Balance() = %d, want %d", b, openAmt)
+ }
+ switch b, ok := z.Balance(); {
+ case !ok:
+ t.Fatal("z.Balance() returned !ok, want ok.")
+ case b != 0:
+ t.Fatalf("z.Balance() = %d, want 0", b)
+ }
+
+ // withdrawals
+ const wAmt = 3
+ const newAmt = openAmt - wAmt
+ switch b, ok := a.Deposit(-wAmt); {
+ case !ok:
+ t.Fatalf("a.Deposit(%d) returned !ok, want ok.", -wAmt)
+ case b != newAmt:
+ t.Fatalf("a.Deposit(%d) = %d, want new balance = %d", -wAmt, b, newAmt)
+ }
+ t.Logf("Withdrawal of %d accepted from account 'a'", wAmt)
+ if _, ok := z.Deposit(-1); ok {
+ t.Fatal("z.Deposit(-1) returned ok, want !ok.")
+ }
+
+ // verify both balances
+ switch b, ok := a.Balance(); {
+ case !ok:
+ t.Fatal("a.Balance() returned !ok, want ok.")
+ case b != newAmt:
+ t.Fatalf("a.Balance() = %d, want %d", b, newAmt)
+ }
+ switch b, ok := z.Balance(); {
+ case !ok:
+ t.Fatal("z.Balance() returned !ok, want ok.")
+ case b != 0:
+ t.Fatalf("z.Balance() = %d, want 0", b)
+ }
+
+ // close just z
+ switch p, ok := z.Close(); {
+ case !ok:
+ t.Fatalf("z.Close() returned !ok, want ok.")
+ case p != 0:
+ t.Fatalf("z.Close() returned payout = %d, want 0.", p)
+ }
+ t.Log("Account 'z' closed.")
+
+ // verify 'a' balance one more time
+ switch b, ok := a.Balance(); {
+ case !ok:
+ t.Fatal("a.Balance() returned !ok, want ok.")
+ case b != newAmt:
+ t.Fatalf("a.Balance() = %d, want %d", b, newAmt)
+ }
+}
+
+func TestConcClose(t *testing.T) {
+ if runtime.NumCPU() < 2 {
+ t.Skip("Multiple CPU cores required for concurrency tests.")
+ }
+ if runtime.GOMAXPROCS(0) < 2 {
+ runtime.GOMAXPROCS(2)
+ }
+
+ // test competing close attempts
+ for rep := 0; rep < 1000; rep++ {
+ const openAmt = 10
+ a := Open(openAmt)
+ if a == nil {
+ t.Fatalf("Open(%d) = nil, want non-nil *Account.", openAmt)
+ }
+ var start sync.WaitGroup
+ start.Add(1)
+ const closeAttempts = 10
+ res := make(chan string)
+ for i := 0; i < closeAttempts; i++ {
+ go func() { // on your mark,
+ start.Wait() // get set...
+ switch p, ok := a.Close(); {
+ case !ok:
+ if p != 0 {
+ t.Errorf("a.Close() = %d, %t. "+
+ "Want payout = 0 for unsuccessful close", p, ok)
+ res <- "fail"
+ } else {
+ res <- "already closed"
+ }
+ case p != openAmt:
+ t.Errorf("a.Close() = %d, %t. "+
+ "Want payout = %d for successful close", p, ok, openAmt)
+ res <- "fail"
+ default:
+ res <- "close" // exactly one goroutine should reach here
+ }
+ }()
+ }
+ start.Done() // ...go
+ var closes, fails int
+ for i := 0; i < closeAttempts; i++ {
+ switch <-res {
+ case "close":
+ closes++
+ case "fail":
+ fails++
+ }
+ }
+ switch {
+ case fails > 0:
+ t.FailNow() // error already logged by other goroutine
+ case closes == 0:
+ t.Fatal("Concurrent a.Close() attempts all failed. " +
+ "Want one to succeed.")
+ case closes > 1:
+ t.Fatalf("%d concurrent a.Close() attempts succeeded, "+
+ "each paying out %d!. Want just one to succeed.",
+ closes, openAmt)
+ }
+ }
+}
+
+func TestConcDeposit(t *testing.T) {
+ if runtime.NumCPU() < 2 {
+ t.Skip("Multiple CPU cores required for concurrency tests.")
+ }
+ if runtime.GOMAXPROCS(0) < 2 {
+ runtime.GOMAXPROCS(2)
+ }
+ a := Open(0)
+ if a == nil {
+ t.Fatal("Open(0) = nil, want non-nil *Account.")
+ }
+ const amt = 10
+ const c = 1000
+ var negBal int32
+ var start, g sync.WaitGroup
+ start.Add(1)
+ g.Add(3 * c)
+ for i := 0; i < c; i++ {
+ go func() { // deposit
+ start.Wait()
+ a.Deposit(amt) // ignore return values
+ g.Done()
+ }()
+ go func() { // withdraw
+ start.Wait()
+ for {
+ if _, ok := a.Deposit(-amt); ok {
+ break
+ }
+ time.Sleep(time.Microsecond) // retry
+ }
+ g.Done()
+ }()
+ go func() { // watch that balance stays >= 0
+ start.Wait()
+ if p, _ := a.Balance(); p < 0 {
+ atomic.StoreInt32(&negBal, 1)
+ }
+ g.Done()
+ }()
+ }
+ start.Done()
+ g.Wait()
+ if negBal == 1 {
+ t.Fatal("Balance went negative with concurrent deposits and " +
+ "withdrawals. Want balance always >= 0.")
+ }
+ if p, ok := a.Balance(); !ok || p != 0 {
+ t.Fatalf("After equal concurrent deposits and withdrawals, "+
+ "a.Balance = %d, %t. Want 0, true", p, ok)
+ }
+}