Created
September 22, 2017 00:38
-
-
Save Matthewacon/9676ca92c3ef07bd80c8e920e90f8551 to your computer and use it in GitHub Desktop.
ICS3UH Course Assignment #1...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/**Imports the global input and output streams from <code>java.lang.System</code> so that they may be | |
* referenced within this file, without having to write out the canonical name each time they are needed. | |
*/ | |
import static java.lang.System.out; | |
import static java.lang.System.in; | |
//More convenient, considering the number of classes used out of the "java.util" package | |
import java.util.*; | |
public class Main { | |
//General purpose interface for defining a currency | |
interface Currency { | |
/**A constant global list of registered currencies (variables defined in interfaces are static and final by | |
* default) | |
*/ | |
List<Enum<? extends Currency>> CURRENCIES = new ArrayList<>(); | |
/**Simple overridable method to define the sales tax for a specific currency*/ | |
Double salesTax(); | |
/**Simple overridable method to define the value of each constant*/ | |
Double value(); | |
static <T extends Enum<? extends Currency>> void registerCurrency(T currency) { | |
Currency.CURRENCIES.add(currency); | |
} | |
} | |
/**Enum that defines the coin currency for Canada, and tax for Ontario | |
* | |
* Definitions for other countries / regions would follow this general format | |
*/ | |
enum CAD implements Currency { | |
//Declaration of Canadian coin currency, in ascending order of value | |
PENNY(0.01d), NICKEL(0.05d), DIME(0.1d), QUARTER(0.25d), LOONIE(1d), TOONIE(2d); | |
final Double value; | |
static final Double TAX; | |
/**Initializes the {@link CAD#TAX} constant*/ | |
static { TAX = 1.13d; } | |
/**This static initializer appends this currency to the global currency registry, {@link Currency#CURRENCIES}. | |
* Uses the {@link CAD#PENNY} instance of this class as a dummy reference to the class itself, as Java does | |
* not support first-class namespaces, in anything other than generic identities. | |
*/ | |
static { | |
Currency.registerCurrency(PENNY); | |
} | |
/**Enum constructor, used for defining the value of a constant. | |
* For example: <code>System.out.println(CAD.QUARTER.value);</code> would print "0.25" | |
*/ | |
CAD(final Double value) { this.value = value; } | |
@Override | |
/**Simply defines the sales tax for Ontario*/ | |
public Double salesTax() { return TAX; } | |
@Override | |
/**Returns the value of a constant*/ | |
public Double value() { return this.value; } | |
} | |
/**Defines a global scanner that may be accessed without an instance reference | |
* For example: <code>Main.GLOBAL_SCANNER</code> | |
*/ | |
static final Scanner GLOBAL_SCANNER; | |
/**Static initializer for Main, since it will never be constructed and there are global constants that need to | |
* be assigned | |
*/ | |
static { | |
/**Hacky Double assignment to force the JVM to invoke the static initializer in the {@link CAD} class, and | |
* register it as a valid currency. A proper implementation of a modular currency system would initialize | |
* region based taxation and currency, defined in some simple data hierarchy such as JSON, YAML, or XML. In | |
* that case, use-case-initialization would not apply as the JVM would dynamically invoke all initializers, | |
* static or otherwise, during object construction. | |
*/ | |
Double dummy = CAD.TAX; | |
/**Initializes {@link Main#GLOBAL_SCANNER}*/ | |
GLOBAL_SCANNER = new Scanner(in); | |
/**Sets the prompt delimiter to the new line character so that each prompt is only returned when a carriage | |
* return is pressed | |
*/ | |
GLOBAL_SCANNER.useDelimiter("\n"); | |
} | |
/**A simple utility method to revers the order of an array | |
* | |
* @param arr Original array | |
* @param <T> The type of the array | |
* @return The same array, in reverse order | |
*/ | |
static <T> T[] reverseArray(T[] arr) { | |
List<T> list = Arrays.asList(arr); | |
Collections.reverse(list); | |
return list.toArray(arr); | |
} | |
public static void main(String... args) { | |
out.println("DISCLAIMER: There is an important note about floating-point mathematics in Java, at the " + | |
"bottom of the main method.\n"); | |
out.println( | |
"Welcome to Matthew Barichello's simple tax calculator!\n" + | |
"Now with a new and improved simple API for defining currencies and tax, per region!" + | |
"Our available currencies are:" | |
); | |
//Prints out currencies, tab delimited | |
for (Enum<? extends Currency> enumerator : Currency.CURRENCIES) | |
out.print( | |
enumerator.getClass().getSimpleName() + | |
(Currency.CURRENCIES.indexOf(enumerator)==Currency.CURRENCIES.size()-1?"\n":"\t") | |
); | |
out.println("Which currency will you be using today?"); | |
String selectedCurrencyStr; | |
Enum<? extends Currency> selectedCurrency = null; | |
/**Typically loop labels should be appropriately named to avoid ambiguous interpretation and confusion, | |
* however this use case is quite self explanatory: the loop simply prompts the user for a currency and exits | |
* when a valid one is entered. | |
*/ | |
W: while(!(selectedCurrencyStr = GLOBAL_SCANNER.next()).equalsIgnoreCase("")) { | |
for (Enum<? extends Currency> enumerator : Currency.CURRENCIES) { | |
if (!enumerator.getClass().getSimpleName().equalsIgnoreCase(selectedCurrencyStr)) | |
out.println("Unfortunately, '" + selectedCurrencyStr + "' isn't an available currency. Please enter " + | |
"one from the list"); | |
else { | |
selectedCurrency = enumerator; | |
break W; | |
} | |
} | |
} | |
out.println("Please list the retail price of your item(s):"); | |
Double subtotal = 0d; | |
String lastPromptReturned; | |
//Continue to ask for input until the user is done | |
while(true) { | |
lastPromptReturned = GLOBAL_SCANNER.next(); | |
Double returnedPrice; | |
try { | |
returnedPrice = Double.parseDouble(lastPromptReturned); | |
} catch(NumberFormatException e) { | |
/**While condition ends the loop if the user enters: | |
* 1. "" | |
* 2. "no" | |
* 3. "done" | |
*/ | |
if (lastPromptReturned.contentEquals("") && subtotal == 0) { | |
out.println("You haven't specified any prices... try again?"); | |
continue; | |
} else if ( | |
lastPromptReturned.equalsIgnoreCase("") || | |
lastPromptReturned.equalsIgnoreCase("no") || | |
lastPromptReturned.equalsIgnoreCase("done") | |
) | |
{ | |
out.println("Thank you for using Matthew Barichello's Simple Tax Calculator!"); | |
break; | |
} | |
out.println("'" + lastPromptReturned + "' is not a valid number. Please enter a valid price, or enter" + | |
" 'DONE'"); | |
continue; | |
} | |
//Adds the price that the user has entered to the subtotal | |
subtotal+=returnedPrice; | |
//Prompts the user for another entry | |
out.println("Anything else?"); | |
} | |
//Immutable reference to the selectedEnum for use in enclosed/lambda bodies | |
final Enum<? extends Currency> enumRef = selectedCurrency; | |
//Create a new LinkedHashMap of the currency name and amount | |
LinkedHashMap<String, Double> currencyIntervals = new LinkedHashMap<String, Double>() { | |
{ | |
for (Enum<? extends Currency> enumerator : reverseArray(enumRef.getClass().getEnumConstants())) | |
put(enumerator.name(), ((Currency)enumerator).value()); | |
} | |
}; | |
Double duePayment = subtotal*((Currency)selectedCurrency).salesTax(); | |
//Rounding up to tenths | |
duePayment = Math.round(duePayment*100)/100d; | |
out.println("Your due payment is: " + duePayment); | |
Double payment = 0d; | |
String sPayment; | |
while(true) { | |
out.println("How much will you be paying?"); | |
if (!(sPayment = GLOBAL_SCANNER.next()).contentEquals("")) { | |
try { | |
payment = Double.parseDouble(sPayment); | |
if (payment < duePayment) { | |
out.println("You must pay at least the amount due..."); | |
continue; | |
} | |
} catch(NumberFormatException e) { | |
out.println("'"+sPayment+"' is not a valid number... Try again?"); | |
continue; | |
} | |
} | |
if(payment > 0) break; | |
} | |
/**Printing the change, only prints fields that are greater than 1 | |
* | |
* Something important to understand about math in java is that not all floating point math (which includes | |
* doubles) will be accurate nor precise because not all decimals fit perfectly into powers of 2. This means | |
* that, depending on how redundant your operations are, addition or subtraction operations can be offset by | |
* -2*10^-64 (for doubles) or -2*10^-32 (for floats) and multiplication or division operations can be offset by | |
* n*(-2*10^64) (for doubles) or n*(-2*10^32) (for floats), where 'n' is the divisor or coefficient. This | |
* effect becomes more prominent for exponential and arrow notated calculations... | |
* | |
* I have accounted for these discrepancies in my code so that all of my calculations should be accurate to | |
* within 1 cent, however other students aren't aware of this functionality and this should be taken into | |
* account when marking their assignments. | |
* | |
* For more information on this, there is a great Stack Overflow post that explains why this happens: | |
* https://stackoverflow.com/questions/327544/strange-floating-point-behaviour-in-a-java-program | |
*/ | |
double difference = Math.round((payment-duePayment)*100)/100d; | |
out.println("Your change is: " + (difference>0?"":"nothing!")); | |
for (Map.Entry<String, Double> entry : currencyIntervals.entrySet()) { | |
double numSubset = Math.floor(difference/entry.getValue()); | |
difference -= Math.round((numSubset*entry.getValue())*100)/100d; | |
if (numSubset > 0) out.println("\tNumber of " + entry.getKey().toLowerCase() + "(s): " + numSubset); | |
} | |
//Ensure that the scanner is closed after use to avoid resource leaks | |
GLOBAL_SCANNER.close(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment