Assignment 18: Streams
Goals: Practice using streams and collectors visitor pattern.
You should submit one .java file containing the solution to this problem.
Be sure to properly test your code and write purpose statements for your methods. A lack of tests and documentation will result in a lower grade! Remember that testing requires you to make some examples of data in an examples class.
18.1 The Setup
You have just begun your first day of your summer internship at Generyc, a very exciting new startup in Boston’s booming tech scene. You have been tasked with enhancing the company’s internal tools that allow them to track their customers and sales. The developer who began to build this, but has since quit to pursue their dreams of joining the circus, left you with the following classes.
Note that all of the fields are marked as private final, indicating they cannot be accessed outside their declaring class, and may never be reassigned once they’ve been initialized in the constructors. We’ll discuss what these words mean in more depth later, but basically, they’re (mostly) enforcing their immutability.
A line item is akin to a single entry on a receipt at a grocery store.
class Customer { private final int id; private final String name; public Customer(int id, String name) { this.id = id; this.name = name; } public int getId() { return this.id; } public String getName() { return this.name; } } class Inventory { private final int id; private final String description; private final int costPerUnit; public Inventory(int id, String description, int costPerUnit) { this.id = id; this.description = description; this.costPerUnit = costPerUnit; } public int getId() { return this.id; } public String getDescription() { return this.description; } public int getCostPerUnit() { return this.costPerUnit; } } class Purchase { private final int id; private final List<LineItem> lineItems; private final int customerIdOfPurchaser; private final int numberOfDaysAgo; public Purchase(int id, List<LineItem> lineItems, int customerIdOfPurchaser, int numberOfDaysAgo) { this.id = id; this.lineItems = new ArrayList<>(lineItems); this.customerIdOfPurchaser = customerIdOfPurchaser; this.numberOfDaysAgo = numberOfDaysAgo; } public int getId() { return this.id; } public List<LineItem> getLineItems() { return this.lineItems; } public int getCustomerIdOfPurchaser() { return this.customerIdOfPurchaser; } public int getNumberOfDaysAgo() { return this.numberOfDaysAgo; } } class LineItem { private final int inventoryId; private final int numberOfItemsPurchased; public LineItem(int inventoryId, int numberOfItemsPurchased) { this.inventoryId = inventoryId; this.numberOfItemsPurchased = numberOfItemsPurchased; } public int getInventoryId() { return this.inventoryId; } public int getNumberOfItemsPurchased() { return this.numberOfItemsPurchased; } }
Java’s built in libraries around dates are clunky at best, so for simplicity’s sake, you can assume the databse properly instantiates the numberOfDaysAgo field in the Purchase class. Working robustly with dates can be surprisingly difficult.
interface CustomerDB { Optional<Customer> getCustomerById(int id); List<Customer> getAllCustomers(); } interface InventoryDB { Optional<Inventory> getInventoryById(int id); List<Inventory> getAllInventory(); } interface PurchaseDB { Optional<Purchase> getPurchaseById(int id); // get purchases made numberOfDaysAgo, numberOfDaysAgo - 1... today List<Purchase> getPurchasesSince(int numberOfDaysAgo); List<Purchase> getPurchasesFor(int customerId); List<Purchase> getPurchasesFor(List<Integer> customerIds); List<Purchase> getPurchasesForSince(int customerId, int numberOfDaysAgo); List<Purchase> getPurchasesForSince(List<Integer> customerIds, int numberOfDaysAgo); }
Retrieving the information for multiple customers at once from a databse can have strong performance improvements over querying the database once per customer.
As the developer didn’t take CS2, however, there is neither a single example nor test to be found!
Import the java libraries you think will be necessary to work with these classes and interfaces.
Develop examples of customers, inventories, purchases, and line items. Be sure to create a healthy amount of each.
Develop at least one kind of example per database (using anonymous classes if you prefer) that uses your examples. For the PurchaseDB, be sure to abstract when possible. Streams will likely be helpful.
Write tests that ensure your mock databases work as you expect them to.
18.2 Stream On
Now that you have data to properly test on, it’s time to implement the features that have been requested. Be sure to test your methods as you go, and use streams where appropriate.
Design a class that will be responsible for computing the sales information for your company. Be sure it has all of the above databse interfaces as fields.
Design a method that determines how much revenue was created over the past n days. Hint: delegate where appropriate; you are free to add methods onto the classes provided.
Design a method that given the id of a piece of inventory, determines how many units of it have been sold in the past month (which can be estimated as 30 days).
Design a method that produces the name of the customer which has spent the most money at your company. If such a customer doesn’t exist, throw an error. If there is a tie, return the name of any tying customer.
18.3 Collector
Design a method that given a customer id, produces the list of ids of the pieces of inventory they have purchased the most times (which is not necessarily the same as the pieces of inventory they have purchased the largest quantities of). The method should error if such a customer does not exist. Hint: Since Stream::max only returns one optional value, you should create your own Collector in a util class which will make this method as readable as possible. The inputs to the method that returns the collector should likely be very similar to the inputs given to Stream::max.