Skip to content

Instantly share code, notes, and snippets.

@Matthewacon
Created September 22, 2017 00:38
Show Gist options
  • Save Matthewacon/9676ca92c3ef07bd80c8e920e90f8551 to your computer and use it in GitHub Desktop.
Save Matthewacon/9676ca92c3ef07bd80c8e920e90f8551 to your computer and use it in GitHub Desktop.
ICS3UH Course Assignment #1...
/**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