Guided Project 1 - Software Development Practices and Tools

Independent Task - Finish Course

Independent Task: Finish Course

Using the WolfScheduler requirements along with the provided JUnit tests, you will finish implementing the Course class to pass the tests!

There are seven setter methods (you’ll see why it is seven shortly) and one getter method that need to be fully implemented to pass all of the provided JUnit tests. The implementation details below provide additional details about how to implement the WolfScheduler requirements and will help you pass the test cases. Note that when a method receive an invalid value in a parameter, you want to throw an IllegalArgumentException. That way the client can catch and handle the error.

You’ll walk through an example of implementing setName() before leaving the other methods for independent implementation. The other methods will review prerequisite concepts.

Learning Outcomes

  • Write setter methods for Course to pass the unit tests and meet the requirements.

Best Practice: Test Driven Development (TDD)

Test driven development is a software development practice where you write your test cases for a class using the requirements BEFORE you write the code for the class. The failing tests then drive the development of your class! You keep fixing your code until your tests pass.

The teaching staff has provided a set of unit tests that you must now pass!

In TDD practice, you may not always write your test cases first. Iterating between code and test (and not putting off testing) is critical for writing high quality code!

Constants

In the Guided Project 1 design there are several constants listed for the Course class. Constants are used to provide names, and therefore meaning, to values in our programs that do not change. Add the constant values to Course and then use the constant values where appropriate. Remember that constants are static and final. These constants should also be private as indicated by the red square in the UML diagram. See the Conceptual Knowledge block about UML Class Diagrams and Notation for more information.

Comment your constants using Javadoc!

Conceptual Knowledge: UML Class Diagrams and Notation

We use a Unified Modeling Language (UML) Class Diagram to provide a visual representation of our design. Class diagram show the classes, their fields, and methods. Class diagrams also show the connection or relationships between classes. By interpreting the icons and information in the class diagram we can create a code skeleton of the classes, fields, and methods for our program. We’ll use the information in the Course class box to create our constants.

The constants are in the middle portion of the Course box, which is labeled as part of the edu.ncsu.csc216.wolf_scheduler.course package. The constants are noted in the left portion of each line by the red outline of a square labeled with an ‘S’ in the upper left corner and ‘F’ in the upper right corner. We interpret the red outline of a square as the keyword private. The ‘S’ represents static. The ‘F’ represents final. The name of the constant is next; an example is SECTION_LENGTH. This if followed by the type of int and the value it is assigned of 3. We would interpret the line in code as private static final int SECTION_LENGTH = 3;.

The other icons in the class diagram tell use different things. The outlines of circles, squares, and triangles represent fields. The solid circle, squares, and triangles represent methods. Green circles are public, red squares are private, and yellow triangles are protected. ‘S’ is static, ‘F’ is final, ‘C’ is a constructor.

The lines connecting the classes represent fields. We’ll discuss those in further detail when we get to the WolfScheduler class.

Provided Tests

The teaching staff tests have been provided for you. The provided tests should NOT change in any way. Be careful that you are not using Eclipse Quick Fixes that might change the provided tests.

The automated grading system (this will be explained in much more detail later) will overwrite your copy of the tests with the teaching staff version. If you changed the provided tests then your code may not compile when you look at the automated grading system feedback. For now, try to avoid changing the provided tests. There will be information about resolving any issues when you reach the section of the Guided Project on automated grading.

Implement Course Methods

Now that we have our constants, fields, and constructors set up, we need to implement the Course class’ methods to meet the requirements defined in Use Case 1: Load Course Catalog. While Use Case 1 defines the information about how to load a file with course schedule information, the list of what makes a course record in the file invalid helps us determine the implementation of the Course object. We want to define Course so that if any of these invalid field characteristics are passed in as parameters, we can let the client (or class that is working with Course objects) know that there is a problem. We will notify the clients’ of Course about the problem by throwing exceptions!

Conceptual Knowledge: Exceptions

Exceptions are objects that we can throw to let a calling method (or the client of your method) know about a problem. By throwing an exception to a client when your method can no longer execute, you are letting the client determine how to handle the problem in that class’ context.

When we want to throw an exception, we must first construct it and then use the throw keyword to send the exception to the caller (see line 9 in the code snippet below). When we see the throw keyword, the flow of control switches from the current method to the caller (if an exception is throw in setInfo(), the flow of control would switch back to line 19 and immediately transfer to the first enclosing catch block). If the caller doesn’t catch or handle the exception, then the exception propagates to the the next caller. If an exception is never caught, then program execution will end.

/**
 * An example of throwing an exception.
 * @param info additional information for the instance of a class
 * @throws IllegalArgumentException when the parameter info is null or an empty string
 */
public void setInfo(String info) {
   if ( /* something is wrong with info */ ) {
      // error path - info is null or an empty string
      throw new IllegalArgumentException(); // construct the exception and throw
   }
   // continue with happy path
}

/**
 * The calling class - it's calling a method that may thrown an exception
 */
public void processInfo() { // note that this method could be in another class, including a test class!
   try { // wrap around code that may throw an exception
      String s = setInfo();
      // continue with happy path
   } catch (IllegalArgumentException e) {
      // handle exception
   }
}

Best practice is to catch and handle exceptions rather than let them escape and stop program execution, but you don’t want to wrap all your code in try/catch blocks. There’s a balance to working with exceptions. If your code is trying things that are likely to throw an exception, then wrap in a try/catch. Otherwise, if the exception is unchecked, meaning that you are not required to handle it for compilation, you could chose not to explicitly try to catch the exception.

You should always provide documentation about exceptions that your method might throw to help clients identify if they should catch a possible exception. Use the @throws tag to document. You can also describe error paths in the main comment body.

Implement setName()

From UC1, Processing Course Information, you need to implement setName() to meet the following the description:

Requirements: Course Name

A course record is invalid if one of more of the following are true:

  • the course name is null or an empty string
  • the course name is contains less than 5 characters or more than 8 characters
  • the course name does not contain one space between letter characters and number characters
  • the course name does not start with 1 to 4 letter characters
  • the course name does not end with three digit characters

Note: Testing Basics

By describing valid and invalid names, we are starting to identify tests to ensure that our method is implemented correctly. We have a test method that considers valid names (e.g., testSetNameValid()) and a test method that considers invalid names (e.g., testSetNameInvalid()).

The requirements describe the characteristics for a course name. For example, “CSC 216” would be valid, but “CSC216” would not be because there is no space. Other valid and invalid examples are provided below. The provided test cases provide additional examples.

  • Valid names: “E 115”, “CSC 116”, “MA 141”, “HESF 101”
  • Invalid names (with reasons invalid)
    • null - cannot be null
    • ”” - cannot be empty string
    • “E 11” - length less than 5, incorrect number of digits
    • “HESFQ 101” - prefix has too many characters
    • “101” - length less than 5, no leading characters, no space
    • “CSC 21” - less than 3 digits
    • “CSC\t216” - no space

Reminder: Characters and the `Character` Class

When processing the name, you will need to look at individual characters. You can get a character from a String at index i using the String.charAt(i) method.

Once you have a character, you can use the methods of the Character class to determine if the character is a letter (Character.isLetter(c)) or a digit (Character.isDigit(c)).

For the implementation of setName() you will need to compare directly with the ` ` (space) character. Using Character.isWhitespace(c) considers more than just a space! Luckily, we have a test to ensure that you’re not using Character.isWhitespace(c).

There are several checks that we need to consider to implement setName(). You should consider them one or two at a time rather than in one big if statement. The one big if statement is harder to debug! Also, by considering one or two checks at a time, we can customize our error messages. The pseudocode below shows one possible implementation of setName().

Implementation Considerations

Constants

  • MIN_NAME_LENGTH
  • MAX_NAME_LENGTH
  • MIN_LETTER_COUNT
  • MAX_LETTER_COUNT
  • DIGIT_COUNT

Exception Messages

  • “Invalid course name.”
    • if name is null
    • if length is incorrect
    • if name structure is incorrect

Testing

  • Run your tests and make sure that testSetNameValid() and testSetNameInvalid() passes.
  • All previously passing tests should continue to test
/**
 * Sets the Course's name.  If the name is null, has a length less than 5 or more than 8,
 * does not contain a space between letter characters and number characters, has less than 1
 * or more than 4 letter characters, and not exactly three trailing digit characters, an
 * IllegalArgumentException is thrown.
 * @param name the name to set
 * @throws IllegalArgumentException if the name parameter is invalid
 */
private void setName(String name) {
    //Throw exception if the name is null
    if name is null
        throw new IllegalArgumentException("Invalid course name.");

    //Throw exception if the name is an empty string
    //Throw exception if the name contains less than 5 character or greater than 8 characters
    if name's length is less than MIN_LENGTH or name's length is more than MAX_LENGTH
        throw new IllegalArgumentException("Invalid course name.");

    //Check for pattern of L[LLL] NNN
    initialize counter for number of letters
    initialize counter for number of digits
    initialize boolean flag for finding a space to false
    for each character in name
        if a space has not yet been found
            if the character at i is a letter
                increment the letter counter
            else if the character at i is a space
                space flag should be set to true
            otherwise
                throw new IllegalArgumentException("Invalid course name.");
        else if a space is found
            if the character is a digit
                increment the digit counter
            otherwise
                throw new IllegalArgumentException("Invalid course name.");
    
    //Check that the number of letters is correct
    if letter counter is less than one or more than 4
        throw new IllegalArgumentException("Invalid course name.");
    
    //Check that the number of digits is correct
    if digit counter is not 3
        throw new IllegalArgumentException("Invalid course name.");
    
    set this.name (field) to name (parameter)
}

Implement setTitle()

From the requirements for UC1, Processing Course Information, you need to implement setTitle() to meet the following:

Requirements: Course Title

A course record is invalid if one of more of the following are true:

  • the course title is null or an empty string

There are two possible conditionals to check for a problem:

//Conditional 1:
if (title == null || "".equals(title)) { ... }

//Conditional 2:
if (title == null || title.length() == 0) { ... }

Both conditionals check if title is null using ==, which is appropriate when doing null checks.

Conditional 1 checks for equality of the title with the empty string. Since Strings are objects, we must use .equals() to check for equality. Additionally, we put the string literal of "" first because a string literal will never be null and the convention is put string literals first. If you swap the order, you’ll get a CheckStyle notification!

Conditional 2 checks the length of the title string. If the length is zero, the string is empty.

Implementation Considerations

Exception Messages

  • “Invalid title.” - if title is null or empty string

Testing

  • Run your tests and make sure that testSetTitleValid() and testSetTitleInvalid() passes.
  • All previously passing tests should continue to test

Implement setSection()

From the requirements for UC1, Processing Course Information, you need to implement setSection () to meet the following

Requirements: Course Section

A course record is invalid if one of more of the following are true:

  • the section number is NOT exactly three digits

Section numbers can start with a leading zero, so section is a String. That means that section cannot be null. Remember that you can use the Character class methods to help you with determining if a character is a digit.

Implementation Considerations

Constants

  • SECTION_LENGTH

Exception Messages

  • “Invalid section.”
    • if section is null or not three characters
    • if any of section’s three characters are not digits

Testing

  • Run your tests and make sure that testSetSectionValid() and testSetSectionInvalid() passes.
  • All previously passing tests should continue to test

Implement setCredits()

From the requirements for UC1, Processing Course Information, you need to implement setCredits() to meet the following:

Requirements: Course Credits

A course record is invalid if one of more of the following are true:

  • the credit hours are not a number
  • the credit hours are less than 1 or greater than 5

You actually don’t need a specific test here to check if the credit hours are not a number. The parameter type of int guarantees that the value will be a number. But later on, you will need to handle this case.

Implementation Considerations

Constants

  • MIN_CREDITS
  • MAX_CREDITS

Exception Messages

  • “Invalid credits.” - if credits is out of bounds

Testing

  • Run your tests and make sure that testSetCreditsValid() and testSetCreditsInvalid() passes.
  • All previously passing tests should continue to test

Implement setInstructorId()

From the requirements for UC1, Processing Course Information, you need to implement setInstructorId() to meet the following:

Requirements: Course Instructor

A course record is invalid if one of more of the following are true:

  • the instructor’s id is null or an empty string

Run your tests and make sure that () passes.

Implementation Considerations

Exception Messages

  • “Invalid instructor id.” - if instructorId is null or empty string

Testing

  • Run your tests and make sure that testSetInstructorIdValid() and testSetInstructorIdInvalid() passes.
  • All previously passing tests should continue to test

Implement setMeetingDaysAndTime()

From the requirements for UC1, Processing Course Information, you need to implement a method in the Course class to meet the following:

Requirements: Course Meeting Days and Time

A course record is invalid if one of more of the following are true:

  • meeting days consist of any characters other than ‘M’, ‘T’, ‘W’, ‘H’, ‘F’, or ‘A’
  • meeting days have a duplicate character
  • if ‘A’ is in the meeting days list, it must be the only character
  • the start time is not between 0000 and 2359 an invalid military time
  • the end time is not between 0000 and 2359 or an invalid military time
  • the end time is less than the start time (i.e., no overnight classes)
  • a start time and/or end time is listed when meeting days is ‘A’

We’re going to interpret the requirements in the following way. If the meeting days for the Course is arranged (e.g., "A"), then startTime and endTime should be 0. If the meeting days for Course are not arranged, then there MUST be a valid startTime and endTime. We could create separate setters for each, but there are too many dependencies and considerations. Instead, we will create a single method to ensure the changes are consistent with all three values.

Before you start your method implementation, do the following:

  • Remove setMeetingDays()
  • Remove setStartTime()
  • Remove setEndTime()
  • Create a method public void setMeetingDaysAndTime(String meetingDays, int startTime, int endTime)
  • Comment the method to describe the functionality.
  • Update the Course constructor to use the new method
  • Uncomment the method in CourseTest called testSetMeetingDaysAndTimesValid()
  • Uncomment the method in CourseTest called testSetMeetingDaysAndTimesInvalid()

This method can have lots of decision points, so pseudocode is provided to get you started on the problem. There are multiple ways to implement this method; you do not need to implement is as per the pseudocode.

To make the method easier to understand and to reduce redundancy, you may want to create helper methods to break out some of the work. If you create helper methods, they must be private!

Implementation Considerations

Hint

  • When working with military time as an integer, you can get the hours by dividing by 100 and the minutes by mod-ing by 100.

Constants

  • UPPER_HOUR
  • UPPER_MINUTE

Exception Messages

  • “Invalid meeting days and times.”
    • if meeting days is null, empty, or contains invalid characters
    • if an arranged class has non-zero start or end times
    • if start time is an incorrect time
    • if end time is an incorrect time
    • if end time is less than start time

Comments

  • Comment the method, including all parameters.

Testing

  • Run your tests and make sure that testSetMeetingDaysAndTimeValid() and testSetMeetingDaysAndTimeInvalid() passes.
  • All previously passing tests should continue to test
// TODO - Add Javadoc here!
public void setMeetingDaysAndTime(String meetingDays, int startTime, int endTime) {
   if meetingDays is null or empty string
      throw IAE("Invalid meeting days and times.") // IAE = IllegalArgumentException

   if meetingDays is "A" // Arranged
      if startTime is NOT 0 OR endTime is NOT 0
	     throw IAE("Invalid meeting days and times.")
      set meetingDays to the parameter; startTime and endTime to 0
	  
   otherwise //not arranged
      create local variables to hold the counts for each weekday letter
	  for all characters in meetingDays
	     increment weekday letter counter if you find the letter // this will take several lines of code 
		 if any invalid letters
		    throw IAE("Invalid meeting days and times.")
	
	  if any weekday letter counts are more than one // checks for duplicates
	     throw IAE("Invalid meeting days and times.")
		 
	  break apart startTime and endTime into hours and minutes //several lines of code
	  
	  if startHour is invalid // not between 0 and 23, inclusive
	     throw IAE("Invalid meeting days and times.")
	
	  if startMin is invalid // not between 0 and 59, inclusive
	     throw IAE("Invalid meeting days and times.")
	  
	  if endHour is invalid // not between 0 and 23, inclusive
	     throw IAE("Invalid meeting days and times.")
	
	  if endMin is invalid // not between 0 and 59, inclusive
	     throw IAE("Invalid meeting days and times.")
	
	  //everything is valid and works together!
	  set fields for meetingDays, startTime, and endTime
}

Implement getMeetingString()

UC 3: View Course Information and UC 9: Display Final Schedule both discuss providing meeting information as a string in standard time format rather than in military format. The getMeetingString() method provides this functionality.

To get started, do the following:

  • Create a method public String getMeetingString()
  • Uncomment the method in CourseTest called testGetMeetingString()

To make the method easier to understand and to reduce redundancy, you may want to create helper methods to break out some of the work. If you create helper methods, they must be private! The teaching staff solution has a helper method, getTimeString(int time), to convert military time to standard time.

Implementation Considerations

Hints

  • When working with military time as an integer, you can get the hours by dividing by 100 and the minutes by mod-ing by 100.
  • If the hour is military time is over twelve, subtract 12 to convert to standard time hours (PM).
  • If the hour in military time is 0, the hour is 12 (AM) in standard time.
  • Minutes less than ten will need a leading ‘0’ character concatenated to the minute value when building the String to return.

Comments

  • Comment the method, including all parameters.

Testing

  • Run your tests and make sure that testGetMeetingString() passes.
  • All previously passing tests should continue to test

Testing and Debugging

At this time, all tests should be passing with a green bar! (You should make sure that you didn’t change the teaching staff tests. You can copy of the test file again to ensure there are no changes.)



Figure: JUnit Results


If you have failing tests, that’s ok! The tests are there to help you find problems in your code. By working incrementally and using test-driven development, that keeps us from implementing too much of our project before finding fundamental problems. If the Course class doesn’t work, a list of Courses won’t work.

There are several ways to debug your code. You can trace your code via pencil and paper. Start with the failing test and keep track of how the variables change as you move through each statement. You can also add print statements to your code and execute each test to see how the values change. If you want to run a single test, you can right click on that test in the JUnit results view and select Run. That will help you narrow down to a single problem.

Best Practice: Ask for Help on Piazza or in Office Hours

For additional help, ask a question on Piazza or come to office hours. When asking a question, please do the following:

  • Push all your source and test code to GitHub, even if it’s not working. The teaching staff can take a look.
  • Provide a link to your GitHub repository in the question.
  • Provide the name of the test method that is failing and provide the specific line number of the failure.
  • Provide the expected and actual output from the test.
  • Provide a explanation of why you think the test is failing.

Requirements and Object Design

You may have noticed that there are two statements in UC1, Processing Course Information that you have not yet fully written code for:

A course record is invalid if one or more of the following are true:

  • an item is missing
  • a course with the same same name and section

You do a basic check for null or empty string items that helps with the “an item is missing” statement, but there is more that will need to be done there when you handle processing the lines of Course information from an input file.

The other item that a record is invalid if “a course with the same same name and section” isn’t something that you can implement at this point. A Course only knows about itself; not about other Courses in the system. You’ll implement that statement when you start working with a collection of Courses.

Conceptual Knowledge: Object Design

A requirement describes what the system must do. Use cases describe the system’s behaviors from the context of a user of the system. When the user interacts with a system, they are not interacting with a specific object, instead, they are interacting with several objects. When designing systems from requirements, a single object may describe the behavior of part of a single use case or parts of many use cases. Part of system design is identifying the objects that are necessary in the system and the relationships between those objects.

Reference: Staging and Pushing to GitHub

GitHub Resources:

Static Analysis Resources:

Check Your Progress

You’ve made a lot of changes to your Course class by implementing the required functionality to pass the provided test cases. Before moving on to the next portion of the Guided Project, complete the following tasks:

  • Make sure that all constants, fields, methods, and constructors are commented.
  • Resolve all static analysis notifications.
  • Fix test failures. DO NOT CHANGE THE PROVIDED TEST CODE!
  • Commit and push your code changes with a meaningful commit message. For example, “[Implementation][Test] Completed Course functionality”.