Problem
Although this question has been asked a lot of times, I was hoping for a feedback on my approach.
Tax Calculator Basic sales tax is applicable at a rate of 10% on all
goods, except books, food, and medical products that are exempt.
Import duty is an additional sales tax applicable on all imported
goods at a rate of 5%, with no exemptions.When I purchase items I receive a receipt which lists the name of all
the items and their price (including tax), finishing with the total
cost of the items, and the total amounts of sales taxes paid. The
rounding rules for sales tax are that for a tax rate of n%, a shelf
price of p contains (np/100 rounded up to the nearest 0.05) amount of
sales tax.Write an application that prints out the receipt details …
Input 1:
1 book at 12.49 1 music CD at 14.99 1 chocolate bar at 0.85
Output 1:
1 book: 12.49 1 music CD: 16.49 1 chocolate bar: 0.85 Sales Taxes: 1.50 Total: 29.83
Input 2:
1 imported box of chocolates at 10.00 1 imported bottle of perfume at 47.50
Output 2:
1 imported box of chocolates: 10.50 1 imported bottle of perfume: 54.65 Sales Taxes: 7.65 Total: 65.15
Input 3:
1 imported bottle of perfume at 27.99 1 bottle of perfume at 18.99 1 packet of headache pills at 9.75 1 box of imported chocolates at 11.25
Output 3:
1 imported bottle of perfume: 32.19 1 bottle of perfume: 20.89 1 packet of headache pills: 9.75 1 imported box of chocolates: 11.85 Sales Taxes: 6.70 Total: 74.68
TaxCalculatorApplication
class TaxCalculatorApplication {
Receipt generateReceipt(String[] inputs) {
List<Item> items = ItemFactory.from(inputs);
return new Receipt(items);
}
}
ItemFactory
public class ItemFactory {
public static List<Item> from(String[] inputs) {
return Arrays.stream(inputs)
.map(ItemFactory::from)
.collect(Collectors.toList());
}
private static Item from(String input) {
Item item = ItemAdapter.createItemFromString(input);
ItemTaxCalculator.applyTaxes(item);
return item;
}
}
ItemAdapter
public class ItemAdapter {
private static final String ITEM_ENTRY_REGEX = "(\d+) ([\w\s]* )at (\d+.\d{2})";
public static Item createItemFromString(String input) {
Pattern pattern = Pattern.compile(ITEM_ENTRY_REGEX);
Matcher matcher = pattern.matcher(input);
matcher.find();
return new Item(matcher.group(1), matcher.group(2), matcher.group(3));
}
}
ItemTaxCalculator
public class ItemTaxCalculator {
private static final double SALES_TAX_PERCENT = 0.1;
private static final double ADDITIONAL_SALES_TAX_PERCENT = 0.05;
public static void applyTaxes(Item item) {
if (!item.isExempted()) {
item.setBasicSalesTaxAmount(SALES_TAX_PERCENT);
}
if (item.isImported()) {
item.setAdditionalSalesTax(ADDITIONAL_SALES_TAX_PERCENT);
}
}
}
Receipt
public class Receipt {
private double totalSalesTax = 0.0;
private double totalAmount = 0.0;
private String itemDetails;
public Receipt(List<Item> items) {
StringBuilder itemDetails = new StringBuilder();
for (Item item : items) {
itemDetails.append(item.toString()).append("n");
totalSalesTax += item.getTaxAmount();
totalAmount += item.getFinalPrice();
}
totalAmount = MathUtils.roundOffAmount(totalAmount);
totalSalesTax = MathUtils.roundOffAmount(totalSalesTax);
this.itemDetails = itemDetails.toString();
}
public double getTotalAmount() {
return totalAmount;
}
public double getTotalSalesTax() {
return totalSalesTax;
}
@Override
public String toString() {
return "Receipt" + "n"
+ itemDetails
+ "Sales Taxes: " + totalSalesTax + "n"
+ "Total: " + totalAmount
+"n*******************************n";
}
}
Item
public class Item {
private String name;
private int quantity;
private double basePrice;
private double basicSalesTaxAmount;
private double additionalSalesTaxAmount;
public Item(String quantity, String name, String basePrice) {
this.name = name;
this.quantity = Integer.valueOf(quantity);
this.basePrice = Double.valueOf(basePrice);
}
public double getFinalPrice() {
return MathUtils.roundOffAmount(quantity * basePrice + getTaxAmount());
}
public double getTaxAmount() {
return quantity * (basicSalesTaxAmount + additionalSalesTaxAmount);
}
public boolean isImported() {
return name.contains("imported");
}
public boolean isExempted() {
return Stream.of("book", "chocolate", "pill")
.anyMatch(exemptedItem -> name.contains(exemptedItem));
}
public void setBasicSalesTaxAmount(double factor) {
basicSalesTaxAmount = basePrice * factor;
}
public void setAdditionalSalesTax(double additionalSalesTaxPercent) {
additionalSalesTaxAmount = MathUtils.roundOffTax(basePrice * additionalSalesTaxPercent);
}
public String toString() {
return String.valueOf(quantity) +
" " +
name +
" : " +
getFinalPrice();
}
}
MathUtils
public class MathUtils {
public static double roundOffTax(double number) {
return Math.ceil(number * 20) / 20;
}
public static double roundOffAmount(double number) {
return Math.round(number * 100.0) / 100.0;
}
}
The whole project is available at this link.
Solution
First don’t mix Item
class with Recipe
(ShoppingCart
) class. the quantity
should be part of RecipeItem
(ShoppingCartItem
), as well as Tax
&Cost
. The TotalTax
&TotalCost
should be part of ShoppingCart
.
My Item
class, has only Name
&Price
& some readonly properties(getters) like IsImported
(in c#):
class Product
{
static readonly IDictionary<ProductType, string[]> productType_Identifiers =
new Dictionary<ProductType, string[]>
{
{ProductType.Food, new[]{ "chocolate", "chocolates" }},
{ProductType.Medical, new[]{ "pills" }},
{ProductType.Book, new[]{ "book" }}
};
public decimal ShelfPrice { get; set; }
public string Name { get; set; }
public bool IsImported { get { return Name.Contains("imported "); } }
public bool IsOf(ProductType productType)
{
return productType_Identifiers.ContainsKey(productType) &&
productType_Identifiers[productType].Any(x => Name.Contains(x));
}
}
class ShoppringCart
{
public IList<ShoppringCartItem> CartItems { get; set; }
public decimal TotalTax { get { return CartItems.Sum(x => x.Tax); } }
public decimal TotalCost { get { return CartItems.Sum(x => x.Cost); } }
}
class ShoppringCartItem
{
public Product Product { get; set; }
public int Quantity { get; set; }
public decimal Tax { get; set; }
public decimal Cost { get { return Quantity * (Tax + Product.ShelfPrice); } }
}
Your tax calculation part is coupled with Item
. An item doesn’t define tax policies it’s Government’s job. Based on the problem’s description, there are two kind of Sales Taxes: Basic
and Duty
taxes. You can use Template Method Design Pattern
to implement it:
abstract class SalesTax
{
abstract public bool IsApplicable(Product item);
abstract public decimal Rate { get; }
public decimal Calculate(Product item)
{
if (IsApplicable(item))
{
//sales tax are that for a tax rate of n%, a shelf price of p contains (np/100)
var tax = (item.ShelfPrice * Rate) / 100;
//The rounding rules: rounded up to the nearest 0.05
tax = Math.Ceiling(tax / 0.05m) * 0.05m;
return tax;
}
return 0;
}
}
class BasicSalesTax : SalesTax
{
private ProductType[] _taxExcemptions = new[]
{
ProductType.Food, ProductType.Medical, ProductType.Book
};
public override bool IsApplicable(Product item)
{
return !(_taxExcemptions.Any(x => item.IsOf(x)));
}
public override decimal Rate { get { return 10.00M; } }
}
class ImportedDutySalesTax : SalesTax
{
public override bool IsApplicable(Product item)
{
return item.IsImported;
}
public override decimal Rate { get { return 5.00M; } }
}
And finally a class to apply taxes:
class TaxCalculator
{
private SalesTax[] _Taxes = new SalesTax[] { new BasicSalesTax(), new ImportedDutySalesTax() };
public void Calculate(ShoppringCart shoppringCart)
{
foreach (var cartItem in shoppringCart.CartItems)
{
cartItem.Tax = _Taxes.Sum(x => x.Calculate(cartItem.Product));
}
}
}
You can try them out at MyFiddle.