I want to make sure that the code is correct in terms of its design, code correctness, best practices & Junit testing.
The complete description is given below:
Functioning of the app quickly estimates the Final Cost depending on different markups. The following are the markups:
- Flat Markup of 5% on all jobs
- Markup of 1.2% for per Working person
- Type of materials markup:
- For pharmaceuticals, 7.5% markup
- For food, 13% markup
- For electronics,2% markup
- For everything else, No markup
The markup calculator should accept the initial base price along with the different categories of markups and calculate a final cost for a project. Example:
Input 1: $1299.99
3 people
food
Output 1: $1591.58
The app is designed like a MVC (Model view controller) logic app where the Model is used to store the data & perform calculations, Controller acts as the getter & setter for values from user & View is used to print the final input & output to the user as MVC format helps any developer to understand the structure & can also embed this app into another MVC app for use
MainCalculator
public class MainCalculator { //BASE PRICE private static String BASE_PRICE="$1299.99"; //Number of People private static String NUM_OF_PEOPLE= "3 people"; //Types of materials private static String TYPE_OF_MATERIAL[] = new String[]{"FOOD","food"}; //Error messages are stored in this private static String message; //status remains true if validation for error checking passes the test i.e. program has handled all test cases private static boolean status= true; /* * MSG_ARGUMENT_NULL, MSG_INVALID_BASE_PRICE, MSG_INVALID_NUM_OF_PEOPLE, MSG_INSUFFICIENT_ARGUMENTS are the messages * displayed when a particular test case fails in the below methodsS * */ //@param MSG_ARGUMENT_NULL is printed when validateNullArgument() returns false private static final String MSG_ARGUMENT_NULL = "Mandatory inputs cannot be null, <BASE_PRICE>, <NUM_OF_PEOPLE> is mandatory"; private static final String MSG_ARGUMENT_NULL_TYPE_OF_MATERIAL = "One of the arguments is null in <Type of Material>"; //@param MSG_INSUFFICIENT_ARGUMENTS is printed when validateEmptyArgument() returns false private static final String MSG_INSUFFICIENT_ARGUMENTS = "Insufficient number of values <BASE PRICE>, <NUM OF PEOPLE>, <TYPE OF MATERIAL>(Optional)"; //@param MSG_INVALID_BASE_PRICE and/or MSG_INVALID_NUM_OF_PEOPLE is printed when validateNumberFormat() returns false private static final String MSG_INVALID_VALUE = "The <BASE PRICE>/<NUM OF PEOPLE> is not in correct number format"; //System error code 1 denotes invalid argument from user private static final int SYSTEM_ERROR_CODE_INVALID_INPUT = 1; //Main function instantiates the default constructor public static void main(String[] args) { //Default Constructor MarkupController markupControllerObject = new MarkupController(); //sets base price from MarkupController's setter method markupControllerObject.setBasePrice(BASE_PRICE); //sets number of people from MarkupController's setter method markupControllerObject.setNumOfPeople(NUM_OF_PEOPLE); /* * sets the types of materials from MarkupControllers setter method & is a array as multiple types * of materials can be involved */ for(int i=0;i< TYPE_OF_MATERIAL.length;i++) { markupControllerObject.setTypeOfMaterial(TYPE_OF_MATERIAL); } //gets base price from MarkupController's getter method BASE_PRICE = markupControllerObject.getBasePrice(); //gets number of people from MarkupController's getter method NUM_OF_PEOPLE = markupControllerObject.getNumOfPeople(); /* * Gets the types of materials from MarkupControllers getter method & is a array as multiple types * of materials can be involved */ for(int i=0;i< markupControllerObject.getTypeOfMaterial().length;i++) { TYPE_OF_MATERIAL = markupControllerObject.getTypeOfMaterial(); } //checks if the values in constructor are valid otherwise exits system if(!validateNullArgument(BASE_PRICE,NUM_OF_PEOPLE,TYPE_OF_MATERIAL) || !MainCalculator.validateEmptyArgument(BASE_PRICE,NUM_OF_PEOPLE) || !MainCalculator.validateNumberFormat(BASE_PRICE, NUM_OF_PEOPLE)) { //System must exit if any of the conditions are true in if condition exit(SYSTEM_ERROR_CODE_INVALID_INPUT); } else { /* * Removes $ sign & whitespace from Base Price if present * and returns only the double value */ BASE_PRICE = validateDollarCheck(BASE_PRICE); /* * Removes keyword 'people' & whitespace from Number of * people if present & returns only number */ NUM_OF_PEOPLE = validateNumOfPeopleKeyword(NUM_OF_PEOPLE); MarkupModel.calculateMarkupSystemFormula(BASE_PRICE, NUM_OF_PEOPLE, TYPE_OF_MATERIAL); } } //Validates if BASE PRICE OR NUMBER OF PEOPLE or TYPE OF MATERIAL is NULL public static boolean validateNullArgument(String BASE_PRICE,String NUM_OF_PEOPLE, String[] TYPE_OF_MATERIAL) { //sizeTypeOfMaterial is the length of TYPE_OF_MATERIAL int sizeTypeOfMaterial = TYPE_OF_MATERIAL.length; //countNullCheck counts the number of null arguments in the string array of type of material int countNullCheck=0; for(int i=0;i< sizeTypeOfMaterial;i++) { if(TYPE_OF_MATERIAL[i] == null) { countNullCheck++; } } /* * If one of the arguments in string array "Type of Material" * is null, prints an error to the user but continues execution */ if(countNullCheck > 0) { message = MSG_ARGUMENT_NULL_TYPE_OF_MATERIAL; System.out.println(message); } //if countNullCheck is equal to size of type of material which means all elements in array are null if(BASE_PRICE == null || NUM_OF_PEOPLE == null || TYPE_OF_MATERIAL == null || countNullCheck == sizeTypeOfMaterial) { message = MSG_ARGUMENT_NULL; System.out.println(message); return false; } return status; } /* * Validates if BASE PRICE OR NUMBER OF PEOPLE IS EMPTY * If empty, then returns a message or returns true */ public static boolean validateEmptyArgument(String BASE_PRICE, String NUM_OF_PEOPLE) { if(BASE_PRICE.isEmpty() || NUM_OF_PEOPLE.isEmpty()) { message = MSG_INSUFFICIENT_ARGUMENTS; System.out.println(message); return false; } return status; } //Validates if BASE PRICE OR NUMBER OF PEOPLE Is in valid format public static boolean validateNumberFormat(String BASE_PRICE, String NUM_OF_PEOPLE) { /* * Removes $ sign & whitespace from Base Price if present * and returns only the double value */ BASE_PRICE = validateDollarCheck(BASE_PRICE); /* * Removes keyword 'people' & whitespace from Number of * people if present & returns only number */ NUM_OF_PEOPLE = validateNumOfPeopleKeyword(NUM_OF_PEOPLE); /* * If base price is a proper double value & number of people is a proper integer * else throw exception */ try { Double.parseDouble(BASE_PRICE); Integer.parseInt(NUM_OF_PEOPLE); } catch(NumberFormatException exception) { message = MSG_INVALID_VALUE; System.out.println(message); return false; } return status; } //validates if a '$' sign is in front of Base Price & trims it public static String validateDollarCheck(String BASE_PRICE) { if(BASE_PRICE.length() > 0) { if(BASE_PRICE.charAt(0) == '$') { BASE_PRICE = BASE_PRICE.trim().substring(1); } } return BASE_PRICE; } //Validates if user enters 'Number of people' as '8 people' & removes keyword people with whitespace public static String validateNumOfPeopleKeyword(String NUM_OF_PEOPLE) { //Regular expression where s* removes whitespace & \b checks for people String regexPeople = "\\s*\\bpeople\\b\\s*"; //Regular expression where s* removes whitespace & \b removes 'person' String regexPerson = "\\s*\\bperson\\b\\s*"; NUM_OF_PEOPLE = NUM_OF_PEOPLE.replaceAll(regexPeople, "").replaceAll(regexPerson, ""); return NUM_OF_PEOPLE; } //Exits the system public static void exit(int status) { System.exit(status); } }
MarkupController
public class MarkupController { /** * MarkupController acts as a library for getter & setter methods for * BASE_PRICE, NUM_OF_PEOPLE and TYPE_OF_MATERIAL * * @author Ankita Kulkarni */ private String BASE_PRICE; private String NUM_OF_PEOPLE; private String[] TYPE_OF_MATERIAL; /*Constructor with 3 parameters * @param BASE_PRICE base price of the system * @param NUM_OF_PEOPLE number of people in the system * @param TYPE_OF_MATERIAL type of materials in the system, an array since user can enter multiple materials */ public MarkupController() { } public MarkupController(String BASE_PRICE, String NUM_OF_PEOPLE,String[] TYPE_OF_MATERIAL) { this.BASE_PRICE = BASE_PRICE; this.NUM_OF_PEOPLE = NUM_OF_PEOPLE; this.TYPE_OF_MATERIAL= TYPE_OF_MATERIAL; } //gets the base Price public String getBasePrice() { return this.BASE_PRICE; } //sets the base Price public void setBasePrice(String BASE_PRICE) { this.BASE_PRICE = BASE_PRICE; } //gets the Number of people public String getNumOfPeople() { return NUM_OF_PEOPLE; } //sets the number of people public void setNumOfPeople(String NUM_OF_PEOPLE) { this.NUM_OF_PEOPLE = NUM_OF_PEOPLE; } //Gets the array of types of material public String[] getTypeOfMaterial() { return TYPE_OF_MATERIAL; } //Sets the array of types of material public void setTypeOfMaterial(String[] TYPE_OF_MATERIAL) { this.TYPE_OF_MATERIAL= TYPE_OF_MATERIAL; } }
MarkupModel
import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.math.BigDecimal; import code.MarkupView; public class MarkupModel { /** * MarkupModel acts as a Model where it stores values of the system obtained from * MarkupController & performs calculations * @Author Ankita Kulkarni */ //Flat Markup on all jobs is 5% private static final String FLAT_MARKUP_ALL_JOBS = "0.05"; //For each working person, markup is 1.2% private static final String MARKUP_PER_WORKING_PERSON = "0.012"; /* * HashMap stores <key,value> pairs where key is the type of material & value is * its respective percentage markup * Used HashMap as its easy to lookup any existing value stored in HashMap * Runtime Complexity of finding a element is O(1) hence faster */ public static HashMap<String,String> markupTypeOfMaterials = new HashMap<String,String>(){ { // puts <key,value> in HashMap i.e. <type of material,markup Percent> put("pharmaceuticals", "0.075"); put("drugs", "0.075"); put("food", "0.13"); put("electronics", "0.02"); } }; //FlatMarkup is stored in a Big Decimal object as its a decimal value private static BigDecimal getFlatMarkup() { return new BigDecimal(FLAT_MARKUP_ALL_JOBS); } //Markup for every working person is stored in a Big Decimal object for calculations private static BigDecimal getMarkupPerWorkingPerson() { return new BigDecimal(MARKUP_PER_WORKING_PERSON); } /* * @param TYPE_OF_MATERIAL provides type of material string array from user * This method checks if any key of hashmap matches to a key provided by user & only returns * that key */ public static BigDecimal getMarkupTypeOfMaterialsValues(String TYPE_OF_MATERIAL) { /* * containsKey() checks if there is any matching key in hashmap & TYPE_OF_MATERIAL * toLowerCase() all keys in Type of material is converted to lower case to match the * hashmap key example: FOOD and food are identical materials */ if(markupTypeOfMaterials.containsKey(TYPE_OF_MATERIAL.toLowerCase())) { //Only matched key is returned return new BigDecimal(markupTypeOfMaterials.get(TYPE_OF_MATERIAL.toLowerCase())); } //If key doesn't match, 0 is returned as there is 'No- Markup' return new BigDecimal("0"); } /* * Calculates the main functionality of the system * */ public static String calculateMarkupSystemFormula(String BASE_PRICE, String NUM_OF_PEOPLE, String[] TYPE_OF_MATERIAL) { /* * Removes $ sign & whitespace from Base Price if present * and returns only the double value */ BASE_PRICE = MainCalculator.validateDollarCheck(BASE_PRICE); /* * Removes keyword 'people' & whitespace from Number of * people if present & returns only number */ NUM_OF_PEOPLE = MainCalculator.validateNumOfPeopleKeyword(NUM_OF_PEOPLE); /* * Converts base price, number of people into BigDecimal for * calculations */ BigDecimal basePrice = new BigDecimal(BASE_PRICE); BigDecimal numOfPeople = new BigDecimal(NUM_OF_PEOPLE); /* * The elements from Type of materials are added in a Set as * it helps remove duplicates * for eg: In condition like 'food' and 'food', only 1 type * 'food' markup will be calculated */ Set<String> typeOfMaterialSet = null; for(String element:TYPE_OF_MATERIAL) { //Only adds elements which are not empty or not null to the Set if(!element.isEmpty() && element !=null) { typeOfMaterialSet = new HashSet<String>(Arrays.asList(element)); } } //When Type of Material does not match hashmap keys then '0' is added BigDecimal totalTypeOfMaterialMarkup = new BigDecimal("0"); /* * Only those elements from Type of material that match * the hashmap keys are added for calculations */ for(String typeOfMaterial:typeOfMaterialSet) { BigDecimal typeOfMaterialMarkup = getMarkupTypeOfMaterialsValues(typeOfMaterial); totalTypeOfMaterialMarkup = totalTypeOfMaterialMarkup.add(typeOfMaterialMarkup); } /* * Formula for calculating flat markups on all jobs * newBasePrice = basePrice * FlatMarkup (0.05)+ basePrice */ BigDecimal newBasePrice = basePrice.multiply(getFlatMarkup()).add(basePrice); /* * Formula for calculating the markup for the number of people * Markup for working people = num of people * markup per working person (0.012) */ BigDecimal totalMarkupForWorkingPeople = numOfPeople.multiply(getMarkupPerWorkingPerson()); /* * Formula for Final Base Price is: * finalbaseprice = newBasePrice * (1+totalMarkupForWorkingPeople+totalTypeOfMaterialMarkup) * */ BigDecimal finalBasePrice = newBasePrice.multiply(BigDecimal.ONE. add(totalMarkupForWorkingPeople). add(totalTypeOfMaterialMarkup)); /* * returns the final base price by rounding upto 2 digits after decimal along with '$' sign */ MarkupView.printInput(basePrice, numOfPeople, typeOfMaterialSet); String outputBasePrice = MarkupView.printOutputFormat(finalBasePrice); System.out.println("Final Output: "+outputBasePrice); return outputBasePrice; } }
MarkupView
import java.math.BigDecimal; import java.util.Set; /** * MarkupView is responsible for displaying the final output to the user * It formats the input & output to provide a clean UI to the user * @author Ankita Kulkarni * */ public class MarkupView { /* * This method formats the basePrice by rounding the 2 numbers after decimal using ROUND_HALF_UP * BigDecimal ROUND_HALF_UP is the ideal way for performing monetary calculations * & ROUND_HALF_UP providing the least bias is recommended * @link http://www.javapractices.com/topic/TopicAction.do?Id=13 */ public static String printOutputFormat(BigDecimal basePrice) { /* * Sets scale of base price to 2 decimal round half up * example, 1591.5777570 turns to 1591.58 */ BigDecimal finalBasePrice = basePrice.setScale(2, BigDecimal.ROUND_HALF_UP); //Prepends '$' sign to base price for representing money String dollarBasePrice = "$"+finalBasePrice.toString(); return dollarBasePrice; } /** * * @param basePrice gets base price that was entered by user * @param numOfPeople gets number of people that was entered by user * @param typeOfMaterial gets the type of material * This method prints all the parameters values on the console along with the * final base price * */ public static void printInput(BigDecimal basePrice,BigDecimal numOfPeople, Set<String> typeOfMaterial) { System.out.println("Base Price: $"+basePrice); System.out.println("Number of People: "+numOfPeople); System.out.println("Type of Material: "+typeOfMaterial.toString().replaceAll("\\[", "").replaceAll("\\]", "") +" "); } }