Assignment 2: Grocery Store Simulator

The COVID-19 Pandemic led to a significant shift in American’s grocery shopping habits. While services such as grocery delivery and curbside pick-up were necessities during times of social distancing, their convenience has resulted in their continued use even as many other aspects of life have returned to their pre-pandemic norms. In response to this rise in online grocery shopping, stores such as Wegmans and delivery services such as Instacart have invested in improvements to their apps to provide a seamless experience to their users.

In this assignment, you’ll develop an application that simulates a subset of features of these grocery apps. In particular, your program will allow users to add items items to their shopping cart that the store has in stock, scan coupons to apply to their order, and see a receipt that summarizes their transaction.

As part of this assignment, you’ll get practice implementing a Java interface and leveraging polymorphism. In addition, you’ll implement a mutable class to track which items have been added to a customer’s cart. Throughout the assignment, you will practice defensive programming and test-driven development.

Learning Objectives

Start early.

This assignment is longer and more involved than Assignment 1. We recommend spreading your work over at least 5 days: roughly 1 for each of the numbered sections in the “assignment walk-through” described later in this handout. Here is one example of what a schedule to complete this assignment might be:

Collaboration policy

This assignment is to be completed as an individual. You may talk with others to discuss Java syntax, debugging tips, or navigating the IntelliJ IDE, but you must not discuss algorithms that might be used to solve problems, and you must never show your in-progress or completed code to another student. Consulting hours are the best way to get individualized assistance at the source-code level.

Frequently asked questions

If needed, there will be a pinned post on Ed to collect any clarifications about this assignment. Please review that pinned post before asking a new question, in case your concern was already addressed. You should also review the FAQ before submitting, to see if there have been any clarifications that might help you improve your solution to this assignment.

Prohibited libraries

To ensure that you get sufficient practice with Java’s core facilities, the following are not allowed in the code that you write for this assignment:

You may see some of these being used in the release code, but you shouldn’t need to interact with them in the code that you are responsible for. Don’t worry—we’ll open up more of the standard library in future assignments. Note that Java’s Math class is part of the java.lang package, so you can call math functions for this assignment.

Interoperability

While coding, you must not alter the client interfaces of classes used in this project. That means you must neither modify any of the public method signatures or specifications provided with the release code, nor add new non-private methods. We should be able to mix and match classes and tests from different students and still end up with a working program—this degree of modularity and interoperability is possible thanks to abstraction and encapsulation.

We will mix and match your classes and tests with our reference implementations, so preserving everyone’s client interface is critical for the autograder’s operation. One consequence is that any helper methods you choose to add must be private, and you must not submit unit tests for them (unit tests can’t access private methods, nor would our reference implementations pass tests for methods they don’t have). This testing restriction is unfortunate, but you can still achieve coverage of your helper methods by testing the public methods that call them. See the student handbook for more about this and other restrictions on assignments in this course.

I. Getting Started

Start early.

We said that before, but it doesn’t hurt to repeat a very important piece of advice. Please keep track of how much time you spend on this assignment. There is a place in the reflection document for reporting this time, and to assert authorship.

Skim this entire assignment handout and the release code before starting.

The release code is commented with various TODO statements numbered 1 through 24. We strongly recommend that you implement the classes and tests in increasing order of TODO ID to avoid confusion. Test each function before moving on; otherwise, you won’t be able to determine which code is at fault for a failing test, as later methods rely on the correct functionality of earlier ones. Remember that as you complete each task, you should remove the comment line with the TODO, since the task has been done (this makes it easier for you to spot tasks that haven’t been done yet).

Tip: if you have trouble finding a TODO, check out the dedicated “TODO” panel in IntelliJ via View | Tool Windows, or try Edit | Find | Find in Files….

Setup

Download the release code from the CMSX assignment page; it is a ZIP file named “a2-release.zip”. Extract its contents to your assignments directory for this course and open the enclosed “a2” folder as a project in IntelliJ. Confirm that you can “Run ‘All Tests’” in the “tests” directory. You should find that only 2 of the 20 provided tests pass. Then open src/cs2110/GrocerySimulator.java and run its main() method. You will be prompted to enter a store name. Type “Wegmans” (or “Aldi”) and press Enter, and you will see that an “UnsupportedOperationException” (corresponding to an unfinished TODO) is thrown; much of the code that makes this simulator run properly does not exist yet. You will add this throughout the assignment.

Assert statements

Throughout the assignment you will use assert statements to defend against bugs due to violations of preconditions or class invariants. IntelliJ automatically enables these statements when running unit tests, but they are disabled by default when running main() methods. To enable them, after running Main, click its green arrow again, then select “Modify Run Configuration…”. To the right of “Build and run”, click “Modify options”, and in the menu that appears, under “Java”, select “Add VM options”. A text field will appear named “VM options”; in it, type -ea, then click “OK”. Now assert statements will not be ignored when running Main.

II. Assignment walk-through

UML class diagram of classes used by grocery simulator
Class diagram of classes used by our grocery simulator. A ‘+’ prefix indicates that a class member is public, while a ‘-’ prefix indicates that it is private. Italics indicate interfaces or abstract methods. Classes shaded in yellow are completely implemented in the release code and should not be modified.

1. Class Utility (TODOs 1–2)

Many of the classes in this assignment deal with money: items have a cost, coupons offer a discount, and the register tabulates the total price of the transaction. What is the best way to represent these monetary amounts in our code? Your first instinct might be to use a floating-point type such as a double, with the number to the left of the decimal point representing dollars and the number to the right of the decimal point representing cents. While this seems like a natural choice, doubles do not actually represent base-10 decimals, and floating-point math is susceptible to roundoff errors. Since people don’t like it when their money disappears due to imprecise math, we will instead use integers to represent monetary amounts in cents. So an item with cost() 249 would cost $2.49.

When we print monetary amounts in our simulation program, we want them to appear in the customary $x.xx format as opposed to an integer. Since this behavior will be used by many pieces of code throughout the assignment, it will be helpful to extract it out into a single method, Utility.costAsString(). Notice that this is a static method, just like your work in A1, so you don’t need to construct any objects in order to use it.

Carry out the following steps to complete the implementation and test cases for the Utility.costAsString(). To practice good test driven development, you should continue to use these steps as you implement other methods throughout the assignment.

  1. Read the declaration and specification of the costAsString() function in Utility.java. Make sure you understand its parameter(s), its return type, its expected behavior, and its preconditions. Then look at the first test case in UtilityTest.java to see an example input–output pair. Run this test case and confirm that it currently fails (we would expect an UnsupportedOperationException, since you haven’t implemented costAsString() yet).
  2. Write the body of costAsString() (TODO 1). Assert that its argument preconditions are satisfied as a defensive measure. Ensure that the first test case now passes.
  3. Return to UtilityTest.java and read the descriptions of the remaining testcases. For each of them, come up with at least one input–output pair satisfying the description and replace the fail() call with an appropriate JUnit assertion (TODO 2). Run each testcase as you write it and debug your costAsString() implementation until it passes.

2. The Item interface and its subtypes (TODOs 3–7)

The Item interface models a product that is stocked by the store and can be added to a cart and purchased by a shopper. Our simulator will consider two different types of items:

The item behaviors that the rest of our simulator code will rely on (such as returning their price and returning a string representation to print on a receipt) are common to both single and bulk items; however, the states of single and bulk items are different, as are the ways that they implement these common behaviors. Thus, we have chosen to define an Item interface that specifies the common behavior and currently has two subtype classes: SingleItem and BulkItem.

You have been given the fully-specified Item interface and SingleItem class, and you will be responsible for implementing and testing the BulkItem class. Begin by reading over the provided code and the tests in ItemTest.java. Then use the specifications in the BulkItem class to define test cases in ItemTest (TODO 3). Next, return to the BulkItem class and think about what field variables you will need to capture the state of a BulkItem instance. Declare these variables, and write Javadoc comments that specify their interpretation and invariants. Implement the BulkItem constructor to initialize these fields (TODO 4). Finally, implement the remaining methods of BulkItem according to their specifications (TODOs 5-7). Run the tests in ItemTest.java to verify the correctness of your implementation.

3. Class Cart (TODOs 8-15)

During their shopping trip, a customer keeps track of which items they’d like to purchase by adding and removing them from a cart. Therefore, our program will define a Cart class to represent a mutable collection of Item objects. You might ask: “Why not just model the cart as an Item[] array in our simulations?” A main drawback to this is that arrays have a fixed length. Since the number of items in a cart changes throughout the application, the client code that interacts with the cart would be responsible for establishing and managing the invariants required to model a dynamically-sized collections with a fixed size array. In object oriented design, we prefer to relegate this responsibility to a separate Cart class. This way, the Cart class implementer can assume responsibility for defining and maintaining these invariants and provide the client code with a clean set of methods to interact with the Cart.

The fields and class invariant have been given for the Cart class, as have all public method specifications. You have also been given a partial test suite in CartTests.java, which includes some fully-written tests, some test method skeletons that we will ask you to complete, and some missing tests that you will develop on your own.

Read over the specifications in Cart.java and the provided code and documentation in CartTests.java. Notice that the testing for the Cart class is more involved than for the Utility, SingleItem, and BulkItem classes. Largely, this is because carts are mutable, and more work needs to be done in many of the methods to maintain the class invariant after the state is updated. For example, adding an item to the cart not only changes its contents, but also the number of items it contains, and also (sometimes) whether it is full or not. For our tests to adequately cover the code in the Cart class, we will need to make sure that all of these interactions are handled correctly.

Fill in the code for the skeleton tests (TODO 8) to confirm that you understand Cart’s specifications concretely. Then turn your attention to the class’s implementation. Read over the documentation for its fields to understand the class invariant of Cart, and use this to write the assertInv() method (TODO 9). Once you have this, complete TODOs 10-13 to implement some of the behaviors of Cart (skipping remove() for the time being). Run the relevant tests after you implement each method one to catch bugs as early as possible. Continue to practice good defensive programming by asserting that preconditions and invariants are satisfied.

Now that you can add and retrieve items from a Cart, it’s time to turn your attention to removal (foreshadowing: removal tends to be the trickiest operation to implement in most data structures). As usual, you’ll start by writing tests to refine your understanding of the spec, only this time, you’ll be writing the tests entirely from scratch. Take note of how the existing tests are structured:

Use this as a guide to develop additional tests that exercise the remove() method (TODO 14). Run the test suite to ensure that your new tests fail (you’d be surprised how many tests don’t fulfill their purpose because their author left off @Test or forgot an assertion!). Finally, implement remove() (TODO 15) and check whether your tests now pass. Consider running your tests “with coverage” to ensure you are exercising all of your code.

4. Class RegisterTransaction (TODOs 16–21)

After a user has finished adding (and potentially removing) items to their cart, they can proceed to check out. The RegisterTransaction class models this checkout procedure. During checkout, the customer may attempt to apply coupons to their order, which may reduce its total cost. Once all of their coupons are scanned, the customer can receive a receipt summarizing their transaction.

Before you can begin to work on the RegisterTransaction class, you will need to be familiar with the Coupon class, which has been provided for you. We have made some simplifying assumptions to make it easier to model coupons. In particular, we will only consider a single “type” of coupon. A coupon consists of a collection of item codes (possibly with duplicates) and a discount amount. For example, if bananas have code 4011 and oranges have code 4388, then a coupon with itemCodes = [4011, 4011, 4388] and discount = 75 would take $0.75 off the purchase of two bananas and an orange. In other words, the total price of the transaction would be reduced by $0.75 if the cart included at least two bananas and at least one orange. There is one more important rule for coupons:

At most one coupon can be applied to each item in the cart.

In other words, once a discount has been applied to the transaction because a certain item was in the cart, then this item cannot be used toward the application of any later coupons. Additionally, when a coupon is scanned, it is definitely applied to the purchase if this is feasible, so coupons that are scanned earlier will always take precedence over coupons that are scanned later, no matter what discount they provide.

Read through the specification and implementation of the Coupon class. You will interact with the Coupon class in the client role when you are implementing methods in the RegisterTransaction class.

Now, read over the specification of the RegisterTransaction class. You are responsible for writing three (public) methods of this class: its constructor, the scanCoupon() method, and the getReceipt() method, as well as providing and utilizing a private assertInv() method. Just as you have done for the previous classes that you have written, complete the implementation and testing for this class. For the testing, you have been provided with a test for the constructor/getter methods and a test for the getReceipt(), and you are responsible for adding tests for the scanCoupon() method (TODO 16). You should add at least 3 tests that account for various scenarios that can occur when coupons are scanned. These tests should include at least 2 method calls where a coupon is scanned successfully and at least 2 method calls where the coupon cannot be applied.

Receipt Format

The getReceipt() method is expected to produce a receipt in a very particular format. Please go through this specification very carefully, as deviating from it by even a single character will result in your code failing our autograded tests.

Basic structure:

The purchased items section:

The applied coupons section:

The cost summary section:

For example, suppose that the customer purchased two “Hass avocado"s (with code 4225) for $2.00 each and one gallon of “2% milk” (with code 41900076610) for $2.89, and they applied a coupon (with id 517) that discounted a gallon of 2% milk and one Hass avocado by $0.75. A receipt for this transaction would be formatted:

-------------------------
Items: 
*Hass avocado    $2.00
Hass avocado     $2.00
*2% milk         $2.89
-------------------------
Coupons: 
Coupon 517: [41900076610,4225] discounted $0.75
-------------------------
Subtotal: $6.89
Total Savings: $0.75
Total: $6.14

5. Creating a simulator application (TODOs 22–24)

Now that we have finished writing the “back end” of our grocery simulator—the classes that model the items, cart, coupons, and register—we are ready to write an application that allows a user to simulate a shopping trip. The class GrocerySimulator contains a main() method that serves as the entry point of this application. When you run GrocerySimulator in IntelliJ, you should see a console open with “Enter a store name to begin: " on the first line and a cursor that allows you to type in this console. By entering a series of text commands, the user can simulate the various activities of a shopping trip (adding items to their cart, checking out, scanning coupons, etc.).

The first step of running the simulation is specifying the store. This is the name of a “.sim” file located in the project directory that contains the information necessary to initialize a Store object with the current store inventory, as well as the customer’s collection of clipped coupons. You can open the Wegmans.sim file to see how this information is stored, and you can read the specification (in particular, the in-line comments) in the Store and GrocerySimulator classes to understand the structure of this file. You are not required to write or modify these “.sim” files, though you may wish to in order to test that your application is working correctly. Entering “Wegmans” as the store name in the running application will prompt the program will read the Wegmans.sim file and begin the main simulation. You should see the line “Welcome to Wegmans!”, confirming that the simulation file was parsed correctly, and the line “Enter a command:” which begins the main command loop of the application. Typing “help” will show a list of valid commands that you can run. Typing “exit” will immediately end the program.

You will be responsible for adding code for three of the simulation commands: adding an item to the cart, removing an item from the cart, and scanning a coupon. To do this, you will need to be familiar with the fields and class invariants of GrocerySimulator, as well as the public method specs of Store (so read those now). You do not, however, need to know how Store is implemented, nor how GrocerySimulator handles reading files and commands (tip: read the JavaDocs linked from the assignments page to avoid getting distracted by implementations). Keep your role in mind, and focus on leveraging the behaviors of other types to accomplish your task, without worrying about how the whole application works top-to-bottom. (Don’t worry—we’ll discuss file I/O, array lists, and hash maps later in the course.)

The Store class models the inventory of a grocery store and has a method that checks whether an item with a given code is in stock, and methods to unshelve (remove from inventory) and shelve (return to inventory) an item. The code in this class is entirely provided and should not be modified. After its set-up phase, which is handled by the main() method and GrocerySimulator() constructor, the majority of the time of the simulator program is spent in the processCommands() method. This method runs a loop where it waits for the user to enter a command, parses the command, and then calls another method to process the command that was entered. The “exit” command terminates this loop. You are responsible (TODOS 22-24) for completing the processCartAdd(), processCartRemove(), and processCouponScan() methods. Read over their specification carefully to ensure (in particular) that you are printing the correct messages to the console as you are processing these commands.

Testing your application

After finishing TODO 24, you will have a completed simulator application. To guarantee that everything is functioning correctly, you will want to run your application through a series of “end-to-end” tests, where you run the application and enter a series of commands to make sure that it has the correct behavior.

We provide a sample test script named wegmans_trip.txt in the tests/ directory. The console output that you should see when running the commands in this script can be found in wegmans_trip_expected_output.txt, also in the tests/ directory. There are two ways that you can run this test. You can type the lines of wegmans_trip.txt into the console one at a time, running the simulator in its “interactive” mode. Alternatively, you can pass the name of the file tests/wegmans_trip.txt as a program argument when running GrocerySimulator to run the simulator in its “batch processing” mode. Both of these options should lead to the same output. Congratulations—you just performed an end-to-end test!

6. Reflecting on your work

Stepping back and thinking about how you approached an assignment (a habit called metacognition) will help you make new mental connections and better retain the skills you just practiced. Therefore, each assignment will ask you to briefly document your reflection in a file called reflection.txt. This file is typically divided into three sections:

  1. Submitter metadata: Assert authorship over your work, and let us know how long the assignment took you to complete.
  2. Verification questions: These will ask for a result that is easily obtained by running your assignment. (Do not attempt to answer these using analysis; the intent is always for you to run your program, possibly provide it with some particular input, and copy its output.)
  3. Reflection questions: These will ask you write about your experience completing the assignment. We’re not looking for an essay, but we do generally expect a few complete sentences. Answers should be specific, rather than general or vague.

Respond to the TODOs in reflection.txt (you can edit this file in IntelliJ).

III. Grading

This assignment is evaluated as follows (note: weights are approximate and may be adjusted slightly for the whole class during grading):

Formatting is a subset of style. To be on the safe side, ensure that our style scheme is installed and that you activate “Reformat Code” for each file before submitting. Graders will deduct for obvious violations that detract from readability, including improper indentation and misaligned braces.

Beyond formatting, choose meaningful local variable names, follow Java’s capitalization conventions (camelCase), and look for ways to simplify your logic. If the logic is subtle or the intent of a statement is not obvious, clarify with an implementation comment.

IV. Submission

Upload your Utility.java, BulkItem.java, Cart.java, RegisterTransaction.java, and GrocerySimulator.java files, as well as their corresponding test suites (UtilityTest.java, ItemTest.java, CartTest.java, and RegisterTransactionTest.java) and your reflection.txt, to CMSX before the deadline. If you forgot where your project is saved on your computer, you can right-click on Utility.java in IntelliJ’s project browser and select “Open In”, then your file explorer (e.g. “Explorer” for Windows, “Finder” for Mac). Be careful to only submit “.java” files, not files with other extensions (e.g. “.class”). Note that your test suite will be under tests/cs2110/, while your other files will be under src/cs2110/.

After you submit, CMSX will automatically send your submission to a smoketester, which is a separate system that runs your solution against the same tests that we provided to you in the release code. The purpose of the smoketester is to give you confidence that you submitted correctly. You should receive an email from the smoketester shortly after submitting. Read it carefully, and if it doesn’t match your expectations, confirm that you uploaded the intended version of your file (it will be attached to the smoketester feedback). Be aware that these emails occasionally get mis-classified as spam, so check your spam folder. It is also possible that the smoketester may fall behind when lots of students are submitting at once. Remember that the smoketester is just running the same tests that you are running in IntelliJ yourself, so don’t panic if its report gets lost—we will grade all work that is submitted to CMSX, whether or not you receive the smoketester email.