Project 3: Bank management system

UC Irvine - Fall ‘22 - ICS 45C

Read everything before you start!
Updates will be listed here:

  • fixed deposit/withdrawal typo in description
  • numbered the project hints
  • expanded a little about testing your code
  • added more hints about gradescope
  • small fixes to header file:
    • made both versions of updateAccount have the same order of parameters
    • fixed comment of the output format for printDetails to match this page
    • typos on the return description of a few functions
  • added clarification of account IDs on gradescope.

Sections

Due date

This project is due at 10PM PT on Thursday of week 7 (2022-11-10). You have a 15-minute buffer for any technical issues and can use your extra days if needed.

Late Submissions

As we talked about during lecture and later announced on EdStem, if you submit something on 2022-11-11 or 2022-11-12, that will count as only one extra day. Submitting something on 2022-11-13 would use 2 late days, and things get back to normal usage from that point.

Context

You got a lot of money from that parking lot business you were doing. So you decided it might be safer to store at least some of it in a bank. But you don’t really trust the existing ones… so why not create your own? By the way, maybe this will get very popular, so let’s use some of the dynamic data we learned about to serve as many customers as we want.

What you have to do

You will implement functions that help in managing bank accounts and operations. The bank can have an infinite number of customers, so you decided to use a linked list to store them. Your program will help manage that list, opening new accounts, closing old accounts, modifying their information, and finding statistics about the bank.

Unlike our previous projects, your program should not have a main function. Instead, you will provide your solution as a library! In the following sections, we will describe the functions you have to implement, and further down in submission, you can download the header file you need to implement. Remember that you can define extra functions/variables/structures in the source file if needed :)

If you feel this project description is too long, still try to skim it! However, you can check if the comments in the header file are enough for you to implement it. But if you have questions, please come back here first to make sure they’re not answered already.

Data structure

The first thing we need is a structure to use. In the header file, the following data structure is created:

typedef struct Record {
  bool locked;          // is this record locked
  double balance;       // amount of money in the account
  std::string phone;    // customer phone number
  std::string email;    // customer email address
  std::string fname;    // customer first name
  std::string lname;    // customer last name
  unsigned int id;      // customer account id
  struct Record *next;  // pointer to the next record
} Record;

We will use Records extensively across the project.

1) Opening a bank account

Function prototype:

Record *openAccount(Record *accounts, std::string fname, std::string lname,
                    std::string email, std::string phone);

This function creates a new account and adds it to the end of our linked list of accounts. So the first parameter is the list of accounts we currently have.

It also receives the customer’s first name, last name, email, and phone as parameters. For the missing fields in Record, new accounts should start:

  • unlocked;
  • with a balance of 0.0;
  • and the id of all accounts should be sequential. The 1st account we create should have an ID of 0, the next one 1, and so on. We never reuse IDs, even if an account has been deleted.

Finally, it returns the head of the list of accounts. This could be the same pointer you received as a parameter, but there is one edge case where you return a different value ;)

ID values on gradescope

Each set of test cases in gradescope executes run within a same execution. For example, if test 10.1 calls openAccount once, the account created should have id == 0. Then, if test 10.2 calls openAccount twice, it should create one account with id == 1 and one with id == 2. Finally, if test 10.3 calls openAccount once again, the account should have id == 3.

2) Locking an account

Function prototype:

void lockAccount(Record *account);

This function locks an account. It receives a pointer to an account and modifies it to lock it. It does not return anything.

A locked account cannot do many things, which will be described in future functions.

3) Unlocking an account

Function prototype:

void unlockAccount(Record *account);

This function unlocks an account. It receives a pointer to an account and modifies it so it is unlocked. It does not return anything.

4) Finding an account

Function prototype:

Record *searchAccount(Record *accounts, std::string field, std::string keyword);

This function tries to find the account we are looking for. It receives:

  • the list of accounts
  • a string describing the field we’re using as a filter
  • the keyword we’re searching for

First, make sure that the field is one of "ID", "FIRST", "LAST", "EMAIL", or "PHONE". Then, we go through all the accounts in the list, looking for an account that is (i) unlocked and (ii) has the appropriate field matching the keyword.

For example, given this call: searchAccount(accounts, "LAST", "SMITH");
We should look for an unlocked account with a customer’s last name == SMITH. It is safe to assume that we want the first account that matches these conditions.

If we cannot find an unlocked account with a field that matches the keyword, we should return a nullptr.

5) Showing details

This is an overloaded function, so it has more than one prototype.
You need to implement all of them.

Function prototypes:

std::string printDetails(Record *account);
std::string printDetails(Record *accounts, unsigned int id);

This function creates a string with all the account information.

The first prototype receives a pointer to a single account we want to export as text.

The second prototype receives a list of available accounts and an id, then it should find the account with a matching ID and create a report for that account.

There are 3 types of reports:

  1. The account doesn’t exist:
------
Account UNKNOWN
------
Sorry, this account does not exist.
------
  1. The account exists but is locked:
------
Account #[ID]
------
Sorry, this account has been locked.
------
  1. The account exists and is unlocked:
------
Account #[ID]
------
Status: OPEN
Account holder: [FIRST NAME] [LAST NAME]
Contact: [PHONE] / [EMAIL]
Balance: [BALANCE]
------

You should return the appropriate format for the account you received, found, or didn’t find.

Finally, the balance should show only two decimal places – check the hints!

6) Updating an account

This is an overloaded function, so it has more than one prototype.
You need to implement all of them.

Function prototypes:

void updateAccount(Record *account, std::string firstName, std::string lastName,
                   std::string phone, std::string email, bool locked);
void updateAccount(Record *accounts, unsigned int id, std::string firstName,
                   std::string lastName, std::string phone, std::string email,
                   bool locked);

The first prototype receives a pointer to a single account we want to update.

The second prototype receives a list of available accounts and an id, then it should find the account with a matching ID to update it.

The other parameters are the same fields as the Record structure, except for the balance. The balance can only be changed with the following two functions we will discuss.

After modifying the account we wanted, this function should not return anything.

7) Depositing money

This is an overloaded function, so it has more than one prototype.
You need to implement all of them.

Function prototypes:

double deposit(Record *account, double value);
double deposit(Record *accounts, unsigned int id, double value);

This function adds money to an account :)

The first prototype receives a pointer to a single account receiving money.

The second prototype receives a list of available accounts and an id, then it should find the account with a matching ID to deposit.

Once we have an account, we check if it is unlocked. If the account is locked or doesn’t exist, we should not change its balance. We chose the special value of -123.45 to indicate that, so you should return this to show we could not update the account.

If the account exists and it’s unlocked, we will try to add money to it. If the value is greater than 0, we add it to the account’s balance. If it is below 0, we don’t do anything, as the only way to decrease the balance is with the next function.

Finally, we return the up-to-date balance of the account.

8) Withdrawing money

This is an overloaded function, so it has more than one prototype.
You need to implement all of them.

Function prototypes:

double withdrawal(Record *account, double value);
double withdrawal(Record *accounts, unsigned int id, double value);

This function removes money from an account :(

The first prototype receives a pointer to a single account we want to remove money.

The second prototype receives a list of available accounts and an id, then it should find the account with a matching ID to remove money.

Once we have an account, we check if it is unlocked. If the account is locked or doesn’t exist, we should not change its balance. We chose the special value of -123.45 to indicate that, so you should return this to show we could not update the account.

If the account exists and it’s unlocked, we will try to remove money from it. If the value is below 0, we don’t do anything, as the only way to increase the balance is with the previous function.

If the value is greater than 0, we remove it from the account’s balance. It is fine for the balance to go below 0.

Finally, we return the up-to-date balance of the account.

9) Deleting an account

Function prototype:

Record *deleteAccount(Record *accounts, unsigned int id);

A customer decided to move their money elsewhere… we should probably remove their account from our list.

This function receives the linked list of accounts and an id. It should look for the account with a matching id and remove it from the list. All other accounts should still be accessible, though.

Finally, it returns the head of the list of accounts. This could be the same pointer you received as a parameter, but there is one edge case where you return a different value ;)

Remember to free the memory from the deleted account!

10) Counting unlocked accounts

Function prototype:

unsigned int countUnlockedAccounts(Record *accounts);

This function receives the head of our account list and returns the count of unlocked accounts in the list.

11) Counting locked accounts

Function prototype:

unsigned int countLockedAccounts(Record *accounts);

This function receives the head of our account list and returns the count of locked accounts in the list.

12) Counting all accounts

Function prototype:

unsigned int countAllAccounts(Record *accounts);

This function receives the head of our account list and returns the count of all (locked and unlocked) accounts in the list.

13) Average balance

Function prototype:

long double getAverageBalance(Record *accounts);

This function receives the head of our account list and returns the average balance of all unlocked accounts in the list.

Be mindful of a division by zero here!

14) Total bank funds

Function prototype:

long double getBankFunds(Record *accounts);

This function receives the head of our account list and returns the sum of the balances of all unlocked accounts.

15) Shutting it all down

Function prototype:

void closeBank(Record *accounts);

A sad day for our bank; it’s closing.

This function receives the head of our account list and deletes all of them. It frees all memory that our accounts were using.

If other accounts are created in the future, we keep going from whatever ID number we had stopped at; we do not reset those!

Project Hints and Tips

1) Testing your code

To test your code, you can submit it to Gradescope. But that might take too long when you’re debugging something small.

For example, let’s say you’re implementing the lockAccount function. You could create a new file that has a main function like this:

int main() {
  Record test;
  test.locked = false;
  cout << "before: " << test.locked << '\n';
  lockAccount(&test);
  cout << "after: " << test.locked << '\n';
}

That way, you can do a quick test for this function and set it up exactly how you want it.

You could also create a larger driver program that interacts with the user and coordinates the functions in the library. That might take some time to implement, so we have written a sample driver program for this project that you can use as an example. This driver program works similarly to p2, where you enter the command and its parameters in the same line. Here’s some sample input for this program.

If you’re not sure how to compile this driver file with the project header file and your own source file, take a look at the separate compilation notes. If you get errors about undefined references, it means that you haven’t created all funciton in your source file yet. Take a look at one of the hints below that go a little more into detail.

2) Static variables

You might be wondering how to create variables that keep their value across different function calls.

One way to do that is to use global variables. Global variables can work, but they have a problem that anyone can see and modify them, which might cause some hard-to-debug issues. So you need to be careful if you’re using global variables.

Another way to do this is to use static variables. Static variables are created the first time a function is called, and then it keeps whatever old value it had in future calls. You can imagine this as a “local-global” variable.

Here’s an example:

#include <iostream>

using std::cout;

void myFunction() {
  static int x = 0;
  cout << "x = " << x++ << '\n';
}

int main() {
  myFunction();  // should print "x = 0"
  myFunction();  // should print "x = 1"
  myFunction();  // should print "x = 2"
  return 0;
}

3) Creating strings

As we’ve seen previously, strings in C++ are mutable! So you can add more text to it, change existing things, remove parts of it, etc. However, you always have to convert things to text before adding them. Most of the time, that might not be too hard, but sometimes it can be tricky.

A nice workaround is to use a stringstream. Stringstreams allow you to print things to a string just like you would print things to cout. So you just add things to your stream as you go, then you can convert it to a string once you’re done.

For example:

#include <iostream>
#include <sstream>

using std::cout;
using std::stringstream;

int main() {
  stringstream output;
  int x = 2;
  float y = 3.5;

  // Add things to the stream:
  output << "Hello! ";
  output << "x = " << x << "   ";
  output << "y = \n" << y << '\n';

  // Convert the stream into a string:
  cout << output.str();

  return 0;
}

You can also use this to control how many decimal places you want to add in a string: https://stackoverflow.com/a/29200671

4) undefined reference errors

We created many sets of tests in gradescope – where most sets test a single function. So, for example, if you get something like:

Test execution failed, failed to build with error:
["/usr/bin/ld: /f6598e0f48ef8531.o: in function `OpenAccountEvaluator::GetResult(int)':", "openAccountTester.cpp:(.text._ZN20OpenAccountEvaluator9GetResultEi[_ZN20OpenAccountEvaluator9GetResultEi]+0x100): undefined reference to `ICS45C::BankManagementSystem::openAccount(ICS45C::BankManagementSystem::Record*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'", "/usr/bin/ld: openAccountTester.cpp:(.text._ZN20OpenAccountEvaluator9GetResultEi[_ZN20OpenAccountEvaluator9GetResultEi]+0x1c2): undefined reference to `ICS45C::BankManagementSystem::openAccount(ICS45C::BankManagementSystem::Record*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'", "/usr/bin/ld: openAccountTester.cpp:(.text._ZN20OpenAccountEvaluator9GetResultEi[_ZN20OpenAccountEvaluator9GetResultEi]+0x284): undefined reference to `ICS45C::BankManagementSystem::openAccount(ICS45C::BankManagementSystem::Record*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'", "/usr/bin/ld: openAccountTester.cpp:(.text._ZN20OpenAccountEvaluator9GetResultEi[_ZN20OpenAccountEvaluator9GetResultEi]+0x341): undefined reference to `ICS45C::BankManagementSystem::openAccount(ICS45C::BankManagementSystem::Record*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'", "/usr/bin/ld: openAccountTester.cpp:(.text._ZN20OpenAccountEvaluator9GetResultEi[_ZN20OpenAccountEvaluator9GetResultEi]+0x3fb): undefined reference to `ICS45C::BankManagementSystem::openAccount(ICS45C::BankManagementSystem::Record*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)'", "/usr/bin/ld: /f6598e0f48ef8531.o:openAccountTester.cpp:(.text._ZN20OpenAccountEvaluator9GetResultEi[_ZN20OpenAccountEvaluator9GetResultEi]+0x4b5): more undefined references to `ICS45C::BankManagementSystem::openAccount(ICS45C::BankManagementSystem::Record*, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)' follow", 'collect2: error: ld returned 1 exit status', '']

We can see that somewhere in this long error line it says:

... undefined reference to `ICS45C::BankManagementSystem::openAccount ...

This means that this set of tests is trying to test your openAccount function, but you have not defined it yet. It shouldn’t be a problem as you develop your functions separately, failing one of the sets should not prevent you from passing other ones. For instance, if you have only implemented the openAccount function, you should be able to pass set of tests #4, even though sets 5+ will fail.

If you want to avoid these errors (undefined reference), you can defined all functions and have empty bodies before you implement them.
For example:

void lockAccount(Record *account) { }

This would be helpful so you can check if you’re passing the combined (set 22) or hidden (set 23) tests. For example, you should be able to pass test 23.1 after only implementing openAccount, but you will need to have defined all names because other tests in set 23 use other functions. So empty-bodied functions would be helpful!

If you get warnings/errors that you have unused variables, you can just print them out and remove those. Continuing the example above:

void lockAccount(Record *account) {
  cout << "lockAccount hasn't been implemented yet...\n";
  cout << "Arguments received: " << account;
  cout << '\n';
}

5) nullptr errors

Gradescope doesn’t do a good job indicating nullptr issues. We tried to catch those and show a better message, but unfortunately didn’t manage to find a good way so far.

So if you get some test case that’s just named:

6) functionality

And all its output is something like:

Test execution failed: During execution an error occured:

That probably means that you are misusing an invalid pointer somewhere.

Submission

Your solution should be named bankManagementSystem.cpp and implement the functions defined in this header file (click here to download). In other words, you need to create the source file for the bankManagementSystem.h header file. This header file contains the same functions described above, and all of them have comments explaining what they take as parameters, should do, and return.

You should submit your code on gradescope: https://www.gradescope.com/courses/443728/assignments/2384662/ You can submit it as often as you want; just remember that gradescope might not give you instant feedback.

Your code will be checked for compilation (e.g., warnings), style, static problems (e.g., memory leaks), and functionality issues. Compilation, style, and static checks will be completely open, so you know what needs to be changed. Functionality tests will be a mix of open and hidden ones. You should be able to debug your code based on the open ones.

For this project, all functionality tests use unit tests. That means we’re checking if your functions manipulate the structures the way we want and that your returns are valid. You should not have a main function in your submission.