Project 4 - MovieTix

MovieTix, a hybrid debit/credit card for purchasing movie tickets with different purchasing plans.

Introduction

PayTex is a (fictitious) company that develops technologies for electronic payment and crypto-currency systems. You have been hired by them to develop several components of a system called MovieTix.

MovieTix is, essentially, a hybrid debit/credit card for purchasing movie tickets. A MovieTix card can be pre-loaded with a particular number of tickets, can be used as a credit card, or can be used like a combination of the two. The prototype MovieTix system is being developed for university students and supports types of plans with the following “default” properties (though the system must support other properties, as described below).

  • Movie Plan - a plan with both pre-paid tickets and the ability to purchase tickets on credit. Any pre-paid tickets that are not used during the semester are lost (and the purchase price is not refunded).
  • Tiered Plan - a plan with both pre-paid tickets and the ability to purchase tickets on credit. It differs from a Movie Plan in that the first group of "purchases" is at a different price from the subsequent "purchases".
  • Limited Plan - a plan with both pre-paid tickets and the ability to purchase a limited dollar amount of tickets on credit.

Existing Applications

Another PayTex employee has written two applications that will make use of your code when you complete it, and provided you with the source code.

The UsageSummarizer can be used to print a table of costs-per-movie for different amounts of usage (i.e., movies seen) for different common plans. The Planalyzer can be used to print a table that contains the best plan (of the same common plans) for different amounts of usage.

System Design

The relationships between the components that encapsulate plans can be modeled using the following UML class diagram.

classDiagram
class MoviePlan:::whitebg
MoviePlan: -double movieCost {readOnly}
MoviePlan: -double planCost {readOnly}
MoviePlan: -int prepaid {readOnly}
MoviePlan: -String name {readOnly}
MoviePlan: -double spent
MoviePlan: -int numberPurchased
MoviePlan: -int punches
MoviePlan: #Interval APPROVED_MOVIE_COSTS {readOnly}$
MoviePlan: #Interval APPROVED_PLAN_COSTS {readOnly}$
MoviePlan: +MoviePlan()
MoviePlan: +MoviePlan(name String, prepaid int, planCost double, movieCost double)
MoviePlan: #costOfPurchasedMovie() double
MoviePlan: #costToDate() double
MoviePlan: +getCategory() Category {exceptions=IllegalStateException}
MoviePlan: +getCostOfNextMovie() String
MoviePlan: +getCostPerMovie() double {exceptions=IllegalStateException}
MoviePlan: +getName() String
MoviePlan: +getPlanCost() double
MoviePlan: #numberPurchased() int
MoviePlan: #numberSeen() int
MoviePlan: #remainingPrepaid() int
MoviePlan: #spent() double
MoviePlan: +toString() String
MoviePlan: +use() boolean
class TieredPlan:::whitebg
TieredPlan: -double tierCost {readOnly}
TieredPlan: -int tierLimit {readOnly}
TieredPlan: +TieredPlan()
TieredPlan: +TieredPlan(name String, prepaid int, planCost double, movieCost double, tierLimit int, tierCost double)
TieredPlan: #costOfPurchasedMovie() double
MoviePlan<|--TieredPlan
class LimitedPlan:::whitebg
LimitedPlan: -double creditLimit {readOnly}
LimitedPlan: +LimitedPlan()
LimitedPlan: +LimitedPlan(name String, int prepaid, planCost double)
LimitedPlan: +LimitedPlan(name String, prepaid int, planCost double, movieCost double, creditLimit double)
LimitedPlan: +getCostOfNextMovie() String
LimitedPlan: +use() boolean
MoviePlan<|--LimitedPlan
Larger View

The supporting components can be modeled using the following UML class diagram.

classDiagram
class Interval:::whitebg
<<immutable>> Interval
Interval: -double left {readOnly}
Interval: -double right {readOnly}
Interval: -boolean leftClosed {readOnly}
Interval: -boolean rightClosed {readOnly}
Interval: +Interval(original Interval)
Interval: +Interval(leftSymbol char, left double, right double, rightSymbol char) {exceptions = IllegalArgumentException}
Interval: +contains(value double) boolean
Interval: +closestTo(value double) double
Interval: +toString() String
Interval: +toString(formatString String) String
class Category:::whitebg
<<enumeration>> Category
Category: -Interval interval {readOnly}
Category: -String description {readOnly}
Category: -String symbol {readOnly}
Category: -Category(description String, symbol String, interval Interval)
Category: +getCategoryFor(price double)$ Category
Category: +getDescription() String
Category: +getSymbol() String
Category: +toString() String
Category: BARGAIN
Category: INEXPENSIVE
Category: MODERATE
Category: EXPENSIVE
Category: OUTRAGEOUS
class PlanUtilities:::whitebg
<<utility>> PlanUtilities
PlanUtilities: +findBestPlan(plans MoviePlan...)$ MoviePlan
Larger View

The details of each class in this diagram are provided below. You may add methods but you must not override any existing methods that are not explicitly overridden in the UML diagram and you must not shadow any attributes.

Note that, in Java, the UML property modifier {readOnly} indicates that an attribute must be declared final.

The Interval Class

The Interval class is an encapsulation of an interval on the real line. Interval objects are used in several places.

In addition to the other specifications included in the UML diagram, the Interval class must comply with the following specifications.

Attributes

The double attributes left and right contain the left and right bounds of the interval.

The boolean attribute leftClosed indicates whether left is in the interval (when true) or not (when false). Similarly, the boolean attribute rightClosed indicates whether right is in the interval or not.

Interval(Interval)

The one-parameter constructor is a copy constructor. You need not validate the parameter.

Interval(char, double, double, char)

The four-parameter constructor is passed a char to indicate whether the interval is closed on the left (i.e., left is in the interval), the left and right bounds, and a char to indicate whether the interval is closed on the right. A leftSymbol of '[' indicates that the interval must be closed on the left and a rightSymbol of ']' indicates that the interval must be closed on the right. Any other char indicates that the interval is open on that side (though the traditional characters are '(' and ')'.

This constructor must throw an IllegalArgumentException if left is greater than right.

contains(double)

Must return true if the given value is contained in the interval and false otherwise. Note that this method does not use tolerances. Hence, it is not appropriate for some kinds of calculations.

closestTo(double)

Must return the value in the closure of the interval (i.e., the interval and its bounds, whether or not it is closed) that is closest to the given number. Note that this method can, if the value is in the interval, return the value itself. (This operation is sometimes described as projecting the value onto the closure of the interval.)

For example, the closure of the open interval (3.00, 8.00) is [3.00, 8.00]. So, the point in the closure of the interval (3.00, 8.00) that is closest to 5.30 is 5.30, and the point in the closure of the same interval that is closest to 100.50 is 8.00.

toString()

Must return a String representation of the interval. The result must contain a '[' or '(' as appropriate, followed by the left bound (in a field of width 6 with 2 digits to the right of the decimal point), followed by a ',', followed by a space, followed by the right bound (in a field of width 6 with 2 digits to the right of the decimal point), followed by a ']' or ')' as appropriate.

toString(String)

Like the version with no parameters, this method must return a String representation of the interval. However, the left and right bounds must be formatted using the String parameter as the format specifier (instead of being in a field of width 6 with 2 digits to the fight of the decimal point). In other words, if this method is passed a parameter of "%3.0f" it must format each bound in a field of width 3 with no digits to the right of the decimal point.

This method need not validate the String parameter. In other words, it may assume that the format specifier is valid.

The Category Enum

The Category enum encapsulates a predefined set of price categories.

In addition to the other specifications included in the UML diagram, the Category enum must comply with the following specifications.

Constructor

By default, enum constructors are private , which is conveniently aligned with this specification (see the UML). So, you should not explicitly mark the Category constructor as private in your source code or Checkstyle will complain about the redundancy.

Elements

The Category enum must contain five elements:

  1. BARGAIN with a description of Bargain, a symbol of $, and an interval of [0.00, 5.00];
  2. INEXPENSIVE with attributes Inexpensive, $$, and (5.00, 11.00];
  3. MODERATE, with attributes Moderate, $$$, and (11.00, 15.00];
  4. EXPENSIVE, with attributes Expensive, $$$$, and (15.00, 25.00]; and
  5. OUTRAGEOUS with attributes Outrageous, $$$$$, and (25.00, ∞].

getCategoryFor(double)

Must return the Category object that contains the given price (or null if there is no corresponding Category).

toString()

Must return a String containing the description, a single space, and the symbol (in parentheses). For example, BARGAIN.toString() must return the String literal Bargain ($).

The MoviePlan Class

The MoviePlan class encapsulates pre-paid movie plans with a fixed up-front cost and number of tickets, and the ability to purchase “extra” tickets on credit at a pre-determined cost.

In addition to the other specifications included in the UML diagram, the MoviePlan class must comply with the following specifications.

Attributes

APPROVED_PLAN_COSTS contains the range of allowable plan costs (as determined by the company PayTex). Valid plan costs must be in the interval [0.00, 200.00]. Similarly, APPROVED_MOVIE_COSTS contains the range of allowable ticket costs (which, though not relevant for MoviePlan objects is relevant for other plans). Valid ticket costs must be in the interval [0.00, 25.00].

The attributes prepaid and punches contain the number of prepaid tickets in the plan and the number of pre-paid tickets used to-date (which must be 0 initially).

The attribute numberPurchased contains the number of “extra” movies that have been purchased to-date (which must be 0 initially) and the attribute spent contains the amount spent to-date on “extra” movies (which must be 0.00 initially).

Constructors

The default constructor must initialize the attributes to the default values as follows: the name must be Movie Plan, the number of pre-paid tickets must be 5, the plan cost must be $50.00, and the cost per “extra” movie must be $15.00.

The explicit value constructor must initialize the attributes in the obvious way, with a few exceptions. The plan cost attribute must be assigned the value in APPROVED_PLAN_COSTS that is closest to the parameter planCost and the movie cost attribute must be assigned the value in APPROVED_MOVIE_COSTS that is closest to the parameter movieCost.

costOfPurchasedMovie()

Must return the cost of a purchased (i.e., not pre-paid) movie.

Note: The use of the term “purchased” in the name of this method is not intended to convey the past tense (i.e., it does not imply that the movie was purchased in the past). This method could just as easily have been named costOfPurchasingAMovie().

costToDate()

Must return the cost-to-date of the plan (i.e., the sum of the plan cost and the amount spent to-date on purchased/“extra” movies). So, for example, if the plan cost $100.00, all of the pre-paid movies have been used, and one “extra” movie has been purchased at a price of $12.50 then this method must return 112.50.

getCategory()

Must return the current Category for the plan based on the cost per movie (to-date). For example, if the cost per movie is currently $7.00 then this method must return the INEXPENSIVE Category.

If the cost per movie is undefined then this method must throw an IllegalStateException.

getCostOfNextMovie()

Must return a String representation of the cost of the next movie. If the plan still has punches then this method must return “Free”. Otherwise, it must return the cost of a purchased movie (preceded by a dollar sign, in a field of width 6, with 2 digits to the right of the decimal point).

getCostPerMovie()

If the number of movies seen to-date is 0 it must throw an IllegalStateException (since the cost per movie is undefined when no movies have been seen). Otherwise, it must return the cost-to-date divided by the number of movies seen to-date.

numberPurchased()

Must return the number of movies that have been purchased to-date (which does not include the number of pre-paid movies that have been seen).

numberSeen()

Must return the number of movies that have been seen (whether pre-paid or purchased) to-date.

remainingPrepaid()

Must return the number of unused pre-paid tickets.

spent()

Must return the amount of money that has been spent on purchased movies (which does not include the plan cost).

toString()

Must return a String representation of the plan. The String must be tab-delimited and include four items (in order): the name, the cost per movie (preceded by a dollar sign in a field of width 6, with 2 digits to the right of the decimal point), the String representation of the current Category, and the cost of the next movie (formatted as described above).

If the cost per movie is undefined, then this method must return: the name, followed by two tabs, followed by the String literal “Unused”, followed by the cost of the next movie (formatted as described above)

use()

The use() method is called when a user wants to “see” a movie. It must adjust the number of punches, the amount spent, and/or the number purchased (as appropriate, depending on whether there are or aren’t unused punches).

Since it is always possible to “see” a movie under this plan, it must always return true.

An Example

A MoviePlan with the attributes (“A”, 2, 20.00, 12.50) would have the following costs per movie.

Movies     Cost To Date     Cost Per Movie
120.0020.00
220.0010.00
332.5010.83
445.0011.25
557.5011.50

The LimitedPlan Class

The LimitedPlan class is a specialization of the MoviePlan class in which there is a limit on the amount of money that can be spent on credit.

In addition to the other specifications included in the UML diagram, the LimitedPlan class must comply with the following specifications.

Attributes

The creditLimit attribute must contain the limit on the amount of money that can be spent on credit.

Constructors

The default constructor must initialize the name to “Limited Plan”, the number of pre-paid tickets to 5, the plan cost to $50.00, the cost of purchased tickets to $15.00, and the credit limit to $100.00

The 3-parameter constructor is used to construct “pre-paid only” plans. To that end, it must initialize the cost of purchased tickets to the maximum possible value, and the credit limit to $0.00.

getCostOfNextMovie()

Must return a String representation of the cost of the next movie. If the plan is unusable (i.e., has no available punches and insufficient credit to purchase a ticket) then it must return “N/A”. Otherwise, it must exhibit the same behavior as a MoviePlan.

use()

The use() method is called when a user wants to “see” a movie. It must return false if the plan is unusable (as described above). Otherwise, it must exhibit the same behavior as a MoviePlan.

An Example

A LimitedPlan with the attributes (“B”, 2, 25.00, 15.00, 30.00) would have the following costs per movie.

Movies     Cost To Date     Cost Per Movie
125.0025.00
225.0012.50
340.0013.33
455.0013.75
5N/AN/A

The TieredPlan Class

The TieredPlan class is a specialization of a MoviePlan in which there is a special initial tier of purchases (at a different price from ordinary purchases).

In addition to the other specifications included in the UML diagram, the TieredPlan class must comply with the following specifications.

Attributes

The tierLimit is the number of tickets in the initial group of tickets. The tierCost attribute must contain the cost (per movie) of the initial group of purchased tickets.

Constructors

The default constructor must initialize the name to “Tiered Plan”, the number of pre-paid tickets to 5, the plan cost to $100.00, the normal purchase price to $10.00, the number of tickets in the initial group to 5, and the price of the initial purchases to $5.50.

The explicit value constructor must initialize the attributes in the obvious way, with a few exceptions. The plan cost attribute must be assigned the value in APPROVED_PLAN_COSTS that is closest to the parameter planCost, and the movie cost and tier cost attributes must be assigned the value in APPROVED_MOVIE_COSTS that are closest to the parameters movieCost and tierCost, respectively.

costOfPurchasedMovie()

Must return the cost of a purchased (i.e., not pre-paid) movie. Note that the value returned by this method will depend on the number of movies that have been purchased (because of the two price tiers).

An Example

A TieredPlan with the attributes (“C”, 2, 30.00, 12.50, 2, 7.50) would have the following costs per movie.

Movies     Cost To Date     Cost Per Movie
130.0030.00
230.0015.00
337.5012.50
445.0011.25
557.5011.50

The PlanUtilities Class

The PlanUtilities class is a utility class that can be used to perform operations on MoviePlan objects.

In addition to the other specifications included in the UML diagram, the PlanUtilities class must comply with the following specifications.

findBestPlan()

Must return the least expensive MoviePlan (among those passed to it) based on each plan’s current cost-per-movie.

This method must account for several special situations:

  1. In the event of a tie, it must return the MoviePlan that appears earliest in the argument list.
  2. Any null parameters must be ignored.
  3. It must return null if all of the parameters are null.
  4. It must return null if there are 0 parameters.
  5. It must ignore parameters that have an undefined cost-per-movie.
  6. It must return null if all of the parameters have an undefined cost-per-movie.

Submission and Grading

You must submit complete implementations of all classes/enums, as well as JUnit tests for each class/enum.

For full credit your submission must satisfy three conditions:

  • The classes/enums must pass all of the student-provided JUnit tests.
  • The student-provided unit tests must achieve 100% method, statement, and branch coverage of the classes/enums.
  • The classes/enums must pass all of the instructor-provided correctness tests.

Grading

Autograder Code Review Files to Submit
Part A:
Due Tue 3/22
30 pts 0* pts Interval.java
IntervalTest.java
Category.java
CategoryTest.java
Part B:
Due Thu 3/24
30 pts 0* pts MoviePlan.java
MoviePlanTest.java
Part C:
Due Mon 3/28
40 pts 0* pts LimitedPlan.java
LimitedPlanTest.java
TieredPlan.java
TieredPlanTest.java
PlanUtilities.java
PlanUtilitiesTest.java

For all three parts, you may submit up to 10 times without penalty. Additional submissions (if any) will receive a penalty of 1 point each. Be sure to use all the development tools offline: Checkstyle, JUnit, coverage analysis, and the debugger.

The code reviews are worth 0 additional points this time. However, points may be deducted if your code is difficult to understand. For example: confusing variable names, too few or too many comments, etc.

Hints and Suggestions

In addition to the information above, you might find the following information helpful.

You are strongly encouraged to use test-driven development (TDD) (i.e., to develop the tests for a class/method before developing the class/method itself). To facilitate that process, stubbed-out versions of all of the classes/enums are available in p4-stubs.zip .

Whether or not you use TDD, you should work on the classes/enums one at a time, in the following order:

  1. The Interval class and associated JUnit tests.
  2. The Category class and associated JUnit tests.
  3. The MoviePlan class and associated JUnit tests.
  4. The LimitedPlan class and associated JUnit tests.
  5. The TieredPlan class and associated JUnit tests.
  6. The PlanUtilities class and associated JUnit tests.

After that, you should perform system testing using the UsageSummarizer and Planalyzer classes.

Getting 100% Coverage

To get 100% coverage of enums and utility classes, you need to use some “tricks”. For more information, see the following EclEmma help page (which also explains how to run an entire test suite).

Questions to Think About

The following question will help you understand the material from this assignment (and from earlier in the semester). You do not have to submit your answers, but you should make sure that you can answer them.

  1. The findBestPlan() method in the PlanUtilities can be passed a variable number of parameters. From the invoker's standpoint, what is the advantage of using a formal parameter of MoviePlan... rather than a formal parameter of MoviePlan[]?
  2. Each plan in the Planalyzer application is declared to be a MoviePlan object but some are instantiated as other objects. Why does the code compile?
  3. Given the signature of the findBestPlan() method, would the following fragment compile?
    LimitedPlan limited;
    TieredPlan  tiered;
    MoviePlan   best;
    
    limited = new LimitedPlan();
    tiered  = new TieredPlan();
    
    best = PlanUtilities.findBestPlan(tiered, limited);
    
  4. Suppose the following methods were added to the PlanUtilities class:
    public static void printPlanInfo(MoviePlan plan)
    {
      System.out.println("Movie Plan");
      System.out.println(plan.toString());
    }
    
    public static void printPlanInfo(LimitedPlan plan)
    {
      System.out.println("Tiered Plan");
      System.out.println(plan.toString());
    }
    
    public static void printPlanInfo(TieredPlan plan)
    {
      System.out.println("Tiered Plan");
      System.out.println(plan.toString());
    }
    

    What would be printed by the following fragment?

    MoviePlan    a, b, c;
    LimitedPlan  d;
    TieredPlan   e;
    
    
    a = new MoviePlan();
    a.use();
    
    b = new LimitedPlan("Limited Plan", 1, 15.00);
    b.use();
    
    c = new TieredPlan();
    c.use();
    
    d = new LimitedPlan("Limited Plan", 1, 15.00);
    d.use();
    
    e = new TieredPlan();
    e.use();
    
    PlanUtilities.printPlanInfo(a);
    PlanUtilities.printPlanInfo(b);
    PlanUtilities.printPlanInfo(c);
    PlanUtilities.printPlanInfo(d);
    PlanUtilities.printPlanInfo(e);
    
  5. Last modified April 30, 2022: practice coding exam (a2ce8c8)