diff --git a/java/pom.xml b/java/pom.xml
index 73ba4a7..c62567d 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -45,8 +45,8 @@
maven-compiler-plugin
3.8.0
- 22
- 22
+ 17
+ 17
diff --git a/java/src/main/java/dojo/supermarket/HtmlReceiptFormatter.java b/java/src/main/java/dojo/supermarket/HtmlReceiptFormatter.java
new file mode 100644
index 0000000..a044a59
--- /dev/null
+++ b/java/src/main/java/dojo/supermarket/HtmlReceiptFormatter.java
@@ -0,0 +1,82 @@
+package dojo.supermarket;
+
+import dojo.supermarket.model.*;
+
+import java.util.Locale;
+
+public class HtmlReceiptFormatter implements ReceiptFormatter {
+
+ @Override
+ public String formatReceipt(Receipt receipt, int columns) {
+ StringBuilder result = new StringBuilder();
+
+ result.append("\n
\n");
+ result.append("Receipt
\n");
+ result.append("\n");
+
+ for (ReceiptItem item : receipt.getItems()) {
+ String receiptItem = presentReceiptItem(item);
+ result.append(receiptItem);
+ }
+
+ for (Discount discount : receipt.getDiscounts()) {
+ String discountPresentation = presentDiscount(discount);
+ result.append(discountPresentation);
+ }
+
+ result.append(presentTotal(receipt));
+ result.append("
\n\n");
+ return result.toString();
+ }
+
+ private String presentReceiptItem(ReceiptItem item) {
+ StringBuilder result = new StringBuilder();
+ result.append(" \n");
+ result.append(" | ").append(escapeHtml(item.getProduct().getName())).append(" | \n");
+ result.append(" ").append(presentPrice(item.getTotalPrice())).append(" | \n");
+ result.append("
\n");
+
+ if (item.getQuantity() != 1) {
+ result.append(" \n");
+ result.append(" | ").append(presentPrice(item.getPrice()))
+ .append(" * ").append(presentQuantity(item)).append(" | \n");
+ result.append("
\n");
+ }
+ return result.toString();
+ }
+
+ private String presentDiscount(Discount discount) {
+ String name = discount.getDescription() + "(" + discount.getProduct().getName() + ")";
+ String value = presentPrice(discount.getDiscountAmount());
+
+ return " \n" +
+ " | " + escapeHtml(name) + " | \n" +
+ " " + value + " | \n" +
+ "
\n";
+ }
+
+ private String presentTotal(Receipt receipt) {
+ return " \n" +
+ " | Total: | \n" +
+ " " + presentPrice(receipt.getTotalPrice()) + " | \n" +
+ "
\n";
+ }
+
+ private static String presentPrice(double price) {
+ return String.format(Locale.UK, "%.2f", price);
+ }
+
+ private static String presentQuantity(ReceiptItem item) {
+ return ProductUnit.EACH.equals(item.getProduct().getUnit())
+ ? String.format("%d", (int)item.getQuantity())
+ : String.format(Locale.UK, "%.3f", item.getQuantity());
+ }
+
+ private static String escapeHtml(String text) {
+ return text.replace("&", "&")
+ .replace("<", "<")
+ .replace(">", ">")
+ .replace("\"", """)
+ .replace("'", "'");
+ }
+}
\ No newline at end of file
diff --git a/java/src/main/java/dojo/supermarket/ReceiptFormatter.java b/java/src/main/java/dojo/supermarket/ReceiptFormatter.java
new file mode 100644
index 0000000..e8d131e
--- /dev/null
+++ b/java/src/main/java/dojo/supermarket/ReceiptFormatter.java
@@ -0,0 +1,7 @@
+package dojo.supermarket;
+
+import dojo.supermarket.model.Receipt;
+
+public interface ReceiptFormatter {
+ String formatReceipt(Receipt receipt, int columns);
+}
\ No newline at end of file
diff --git a/java/src/main/java/dojo/supermarket/ReceiptPrinter.java b/java/src/main/java/dojo/supermarket/ReceiptPrinter.java
new file mode 100644
index 0000000..5a802cb
--- /dev/null
+++ b/java/src/main/java/dojo/supermarket/ReceiptPrinter.java
@@ -0,0 +1,30 @@
+package dojo.supermarket;
+
+import dojo.supermarket.model.*;
+
+public class ReceiptPrinter {
+
+ private final int columns;
+ private final ReceiptFormatter formatter;
+
+ public ReceiptPrinter() {
+ this(40, new TextReceiptFormatter());
+ }
+
+ public ReceiptPrinter(int columns) {
+ this(columns, new TextReceiptFormatter());
+ }
+
+ public ReceiptPrinter(ReceiptFormatter formatter) {
+ this(40, formatter);
+ }
+
+ public ReceiptPrinter(int columns, ReceiptFormatter formatter) {
+ this.columns = columns;
+ this.formatter = formatter;
+ }
+
+ public String printReceipt(Receipt receipt) {
+ return formatter.formatReceipt(receipt, columns);
+ }
+}
diff --git a/java/src/test/java/dojo/supermarket/ReceiptPrinter.java b/java/src/main/java/dojo/supermarket/TextReceiptFormatter.java
similarity index 71%
rename from java/src/test/java/dojo/supermarket/ReceiptPrinter.java
rename to java/src/main/java/dojo/supermarket/TextReceiptFormatter.java
index 070a13f..4e155ce 100644
--- a/java/src/test/java/dojo/supermarket/ReceiptPrinter.java
+++ b/java/src/main/java/dojo/supermarket/TextReceiptFormatter.java
@@ -4,39 +4,32 @@
import java.util.Locale;
-public class ReceiptPrinter {
+public class TextReceiptFormatter implements ReceiptFormatter {
- private final int columns;
-
- public ReceiptPrinter() {
- this(40);
- }
-
- public ReceiptPrinter(int columns) {
- this.columns = columns;
- }
-
- public String printReceipt(Receipt receipt) {
+ @Override
+ public String formatReceipt(Receipt receipt, int columns) {
StringBuilder result = new StringBuilder();
+
for (ReceiptItem item : receipt.getItems()) {
- String receiptItem = presentReceiptItem(item);
+ String receiptItem = presentReceiptItem(item, columns);
result.append(receiptItem);
}
+
for (Discount discount : receipt.getDiscounts()) {
- String discountPresentation = presentDiscount(discount);
+ String discountPresentation = presentDiscount(discount, columns);
result.append(discountPresentation);
}
result.append("\n");
- result.append(presentTotal(receipt));
+ result.append(presentTotal(receipt, columns));
return result.toString();
}
- private String presentReceiptItem(ReceiptItem item) {
+ private String presentReceiptItem(ReceiptItem item, int columns) {
String totalPricePresentation = presentPrice(item.getTotalPrice());
String name = item.getProduct().getName();
- String line = formatLineWithWhitespace(name, totalPricePresentation);
+ String line = formatLineWithWhitespace(name, totalPricePresentation, columns);
if (item.getQuantity() != 1) {
line += " " + presentPrice(item.getPrice()) + " * " + presentQuantity(item) + "\n";
@@ -44,23 +37,23 @@ private String presentReceiptItem(ReceiptItem item) {
return line;
}
- private String presentDiscount(Discount discount) {
+ private String presentDiscount(Discount discount, int columns) {
String name = discount.getDescription() + "(" + discount.getProduct().getName() + ")";
String value = presentPrice(discount.getDiscountAmount());
- return formatLineWithWhitespace(name, value);
+ return formatLineWithWhitespace(name, value, columns);
}
- private String presentTotal(Receipt receipt) {
+ private String presentTotal(Receipt receipt, int columns) {
String name = "Total: ";
String value = presentPrice(receipt.getTotalPrice());
- return formatLineWithWhitespace(name, value);
+ return formatLineWithWhitespace(name, value, columns);
}
- private String formatLineWithWhitespace(String name, String value) {
+ private String formatLineWithWhitespace(String name, String value, int columns) {
StringBuilder line = new StringBuilder();
line.append(name);
- int whitespaceSize = this.columns - name.length() - value.length();
+ int whitespaceSize = columns - name.length() - value.length();
for (int i = 0; i < whitespaceSize; i++) {
line.append(" ");
}
@@ -78,4 +71,4 @@ private static String presentQuantity(ReceiptItem item) {
? String.format("%d", (int)item.getQuantity())
: String.format(Locale.UK, "%.3f", item.getQuantity());
}
-}
+}
\ No newline at end of file
diff --git a/java/src/main/java/dojo/supermarket/model/DiscountStrategy.java b/java/src/main/java/dojo/supermarket/model/DiscountStrategy.java
new file mode 100644
index 0000000..414b016
--- /dev/null
+++ b/java/src/main/java/dojo/supermarket/model/DiscountStrategy.java
@@ -0,0 +1,5 @@
+package dojo.supermarket.model;
+
+public interface DiscountStrategy {
+ Discount calculateDiscount(Product product, double quantity, double unitPrice, double argument);
+}
\ No newline at end of file
diff --git a/java/src/main/java/dojo/supermarket/model/DiscountStrategyFactory.java b/java/src/main/java/dojo/supermarket/model/DiscountStrategyFactory.java
new file mode 100644
index 0000000..c6eb91a
--- /dev/null
+++ b/java/src/main/java/dojo/supermarket/model/DiscountStrategyFactory.java
@@ -0,0 +1,18 @@
+package dojo.supermarket.model;
+
+public class DiscountStrategyFactory {
+ public static DiscountStrategy createStrategy(SpecialOfferType offerType) {
+ switch (offerType) {
+ case THREE_FOR_TWO:
+ return new ThreeForTwoDiscountStrategy();
+ case TEN_PERCENT_DISCOUNT:
+ return new TenPercentDiscountStrategy();
+ case TWO_FOR_AMOUNT:
+ return new TwoForAmountDiscountStrategy();
+ case FIVE_FOR_AMOUNT:
+ return new FiveForAmountDiscountStrategy();
+ default:
+ throw new IllegalArgumentException("Unknown offer type: " + offerType);
+ }
+ }
+}
\ No newline at end of file
diff --git a/java/src/main/java/dojo/supermarket/model/FiveForAmountDiscountStrategy.java b/java/src/main/java/dojo/supermarket/model/FiveForAmountDiscountStrategy.java
new file mode 100644
index 0000000..cdc7363
--- /dev/null
+++ b/java/src/main/java/dojo/supermarket/model/FiveForAmountDiscountStrategy.java
@@ -0,0 +1,15 @@
+package dojo.supermarket.model;
+
+public class FiveForAmountDiscountStrategy implements DiscountStrategy {
+ @Override
+ public Discount calculateDiscount(Product product, double quantity, double unitPrice, double argument) {
+ int quantityAsInt = (int) quantity;
+ if (quantityAsInt >= 5) {
+ int x = 5;
+ int numberOfXs = quantityAsInt / x;
+ double discountTotal = unitPrice * quantity - (argument * numberOfXs + quantityAsInt % 5 * unitPrice);
+ return new Discount(product, x + " for " + argument, -discountTotal);
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/java/src/main/java/dojo/supermarket/model/ShoppingCart.java b/java/src/main/java/dojo/supermarket/model/ShoppingCart.java
index 9293086..bfe5a78 100644
--- a/java/src/main/java/dojo/supermarket/model/ShoppingCart.java
+++ b/java/src/main/java/dojo/supermarket/model/ShoppingCart.java
@@ -38,37 +38,13 @@ void handleOffers(Receipt receipt, Map offers, SupermarketCatalo
if (offers.containsKey(p)) {
Offer offer = offers.get(p);
double unitPrice = catalog.getUnitPrice(p);
- int quantityAsInt = (int) quantity;
- Discount discount = null;
- int x = 1;
- if (offer.offerType == SpecialOfferType.THREE_FOR_TWO) {
- x = 3;
-
- } else if (offer.offerType == SpecialOfferType.TWO_FOR_AMOUNT) {
- x = 2;
- if (quantityAsInt >= 2) {
- double total = offer.argument * (quantityAsInt / x) + quantityAsInt % 2 * unitPrice;
- double discountN = unitPrice * quantity - total;
- discount = new Discount(p, "2 for " + offer.argument, -discountN);
- }
-
- } if (offer.offerType == SpecialOfferType.FIVE_FOR_AMOUNT) {
- x = 5;
- }
- int numberOfXs = quantityAsInt / x;
- if (offer.offerType == SpecialOfferType.THREE_FOR_TWO && quantityAsInt > 2) {
- double discountAmount = quantity * unitPrice - ((numberOfXs * 2 * unitPrice) + quantityAsInt % 3 * unitPrice);
- discount = new Discount(p, "3 for 2", -discountAmount);
- }
- if (offer.offerType == SpecialOfferType.TEN_PERCENT_DISCOUNT) {
- discount = new Discount(p, offer.argument + "% off", -quantity * unitPrice * offer.argument / 100.0);
- }
- if (offer.offerType == SpecialOfferType.FIVE_FOR_AMOUNT && quantityAsInt >= 5) {
- double discountTotal = unitPrice * quantity - (offer.argument * numberOfXs + quantityAsInt % 5 * unitPrice);
- discount = new Discount(p, x + " for " + offer.argument, -discountTotal);
- }
- if (discount != null)
+
+ DiscountStrategy strategy = DiscountStrategyFactory.createStrategy(offer.offerType);
+ Discount discount = strategy.calculateDiscount(p, quantity, unitPrice, offer.argument);
+
+ if (discount != null) {
receipt.addDiscount(discount);
+ }
}
}
}
diff --git a/java/src/main/java/dojo/supermarket/model/TenPercentDiscountStrategy.java b/java/src/main/java/dojo/supermarket/model/TenPercentDiscountStrategy.java
new file mode 100644
index 0000000..837d5a9
--- /dev/null
+++ b/java/src/main/java/dojo/supermarket/model/TenPercentDiscountStrategy.java
@@ -0,0 +1,8 @@
+package dojo.supermarket.model;
+
+public class TenPercentDiscountStrategy implements DiscountStrategy {
+ @Override
+ public Discount calculateDiscount(Product product, double quantity, double unitPrice, double argument) {
+ return new Discount(product, argument + "% off", -quantity * unitPrice * argument / 100.0);
+ }
+}
\ No newline at end of file
diff --git a/java/src/main/java/dojo/supermarket/model/ThreeForTwoDiscountStrategy.java b/java/src/main/java/dojo/supermarket/model/ThreeForTwoDiscountStrategy.java
new file mode 100644
index 0000000..3d856fb
--- /dev/null
+++ b/java/src/main/java/dojo/supermarket/model/ThreeForTwoDiscountStrategy.java
@@ -0,0 +1,15 @@
+package dojo.supermarket.model;
+
+public class ThreeForTwoDiscountStrategy implements DiscountStrategy {
+ @Override
+ public Discount calculateDiscount(Product product, double quantity, double unitPrice, double argument) {
+ int quantityAsInt = (int) quantity;
+ if (quantityAsInt > 2) {
+ int x = 3;
+ int numberOfXs = quantityAsInt / x;
+ double discountAmount = quantity * unitPrice - ((numberOfXs * 2 * unitPrice) + quantityAsInt % 3 * unitPrice);
+ return new Discount(product, "3 for 2", -discountAmount);
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/java/src/main/java/dojo/supermarket/model/TwoForAmountDiscountStrategy.java b/java/src/main/java/dojo/supermarket/model/TwoForAmountDiscountStrategy.java
new file mode 100644
index 0000000..472ca15
--- /dev/null
+++ b/java/src/main/java/dojo/supermarket/model/TwoForAmountDiscountStrategy.java
@@ -0,0 +1,15 @@
+package dojo.supermarket.model;
+
+public class TwoForAmountDiscountStrategy implements DiscountStrategy {
+ @Override
+ public Discount calculateDiscount(Product product, double quantity, double unitPrice, double argument) {
+ int quantityAsInt = (int) quantity;
+ if (quantityAsInt >= 2) {
+ int x = 2;
+ double total = argument * (quantityAsInt / x) + quantityAsInt % 2 * unitPrice;
+ double discountN = unitPrice * quantity - total;
+ return new Discount(product, "2 for " + argument, -discountN);
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/java/src/test/java/dojo/supermarket/ReceiptFormatterTest.java b/java/src/test/java/dojo/supermarket/ReceiptFormatterTest.java
new file mode 100644
index 0000000..1d5266e
--- /dev/null
+++ b/java/src/test/java/dojo/supermarket/ReceiptFormatterTest.java
@@ -0,0 +1,115 @@
+package dojo.supermarket;
+
+import dojo.supermarket.model.*;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+
+class ReceiptFormatterTest {
+
+ private Receipt createTestReceipt() {
+ SupermarketCatalog catalog = new FakeCatalog();
+ Product apples = new Product("apples", ProductUnit.KILO);
+ catalog.addProduct(apples, 1.99);
+ Product toothbrush = new Product("toothbrush", ProductUnit.EACH);
+ catalog.addProduct(toothbrush, 0.99);
+
+ Teller teller = new Teller(catalog);
+ teller.addSpecialOffer(SpecialOfferType.TEN_PERCENT_DISCOUNT, toothbrush, 10.0);
+
+ ShoppingCart cart = new ShoppingCart();
+ cart.addItemQuantity(apples, 2.5);
+ cart.addItemQuantity(toothbrush, 1.0);
+
+ return teller.checksOutArticlesFrom(cart);
+ }
+
+ @Test
+ void textFormatter_shouldFormatReceiptCorrectly() {
+ Receipt receipt = createTestReceipt();
+ TextReceiptFormatter formatter = new TextReceiptFormatter();
+
+ String result = formatter.formatReceipt(receipt, 40);
+
+ assertTrue(result.contains("apples"));
+ assertTrue(result.contains("toothbrush"));
+ assertTrue(result.contains("10.0% off"));
+ assertTrue(result.contains("Total:"));
+ // Should be plain text format
+ assertFalse(result.contains(""));
+ assertFalse(result.contains(""));
+ }
+
+ @Test
+ void htmlFormatter_shouldFormatReceiptCorrectly() {
+ Receipt receipt = createTestReceipt();
+ HtmlReceiptFormatter formatter = new HtmlReceiptFormatter();
+
+ String result = formatter.formatReceipt(receipt, 40);
+
+ // Should contain HTML structure
+ assertTrue(result.contains(""));
+ assertTrue(result.contains(""));
+ assertTrue(result.contains(""));
+ assertTrue(result.contains(""));
+
+ // Should contain receipt data
+ assertTrue(result.contains("apples"));
+ assertTrue(result.contains("toothbrush"));
+ assertTrue(result.contains("10.0% off"));
+ assertTrue(result.contains("Total:"));
+
+ // Should escape HTML characters
+ assertTrue(result.contains("<") || !result.contains("