diff --git a/pom.xml b/pom.xml
index ff79ddbb..8949bea2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,6 +60,15 @@
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 25
+ 25
+ --enable-preview
+
+
diff --git a/src/main/java/com/example/Category.java b/src/main/java/com/example/Category.java
new file mode 100644
index 00000000..821bab31
--- /dev/null
+++ b/src/main/java/com/example/Category.java
@@ -0,0 +1,37 @@
+package com.example;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public final class Category {
+ private static final Map CACHE = new HashMap<>();
+ private final String name;
+
+ private Category(String name) {
+ this.name = name;
+ }
+
+ public static Category of(String name) {
+ if (name == null) {
+ throw new IllegalArgumentException("Category name can't be null");
+ }
+
+ if (name.isBlank()) {
+ throw new IllegalArgumentException("Category name can't be blank");
+ }
+
+ String trimmed = name.trim();
+
+ String normalized = trimmed.substring(0, 1).toUpperCase() + trimmed.substring(1).toLowerCase();
+
+ return CACHE.computeIfAbsent(normalized, Category::new);
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public String getName() {
+ return name;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/ElectronicsProduct.java b/src/main/java/com/example/ElectronicsProduct.java
new file mode 100644
index 00000000..3c771be2
--- /dev/null
+++ b/src/main/java/com/example/ElectronicsProduct.java
@@ -0,0 +1,45 @@
+package com.example;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+public class ElectronicsProduct extends Product implements Shippable {
+ private final int warrantyMonths;
+ private final BigDecimal weight;
+
+ public ElectronicsProduct(UUID id,
+ String name,
+ Category category,
+ BigDecimal price,
+ int warrantyMonths,
+ BigDecimal weight) {
+
+ super(id, name, category, price);
+
+ if (warrantyMonths < 0) {
+ throw new IllegalArgumentException("Warranty months cannot be negative.");
+ }
+
+ this.warrantyMonths = warrantyMonths;
+ this.weight = weight;
+ }
+
+ @Override
+ public String productDetails() {
+ return "Electronics: " + name() + ", Warranty: " + warrantyMonths + " months";
+ }
+
+ @Override
+ public BigDecimal calculateShippingCost() {
+ BigDecimal base = BigDecimal.valueOf(79);
+ if (weight.compareTo(BigDecimal.valueOf(5)) > 0) {
+ base = base.add(BigDecimal.valueOf(49));
+ }
+ return base;
+ }
+
+ @Override
+ public double weight() {
+ return weight.doubleValue();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/FoodProduct.java b/src/main/java/com/example/FoodProduct.java
new file mode 100644
index 00000000..aed3c167
--- /dev/null
+++ b/src/main/java/com/example/FoodProduct.java
@@ -0,0 +1,55 @@
+package com.example;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.UUID;
+
+public class FoodProduct extends Product implements Perishable, Shippable {
+ private final LocalDate expirationDate;
+ private final BigDecimal weight;
+
+ public FoodProduct(UUID id,
+ String name,
+ Category category,
+ BigDecimal price,
+ LocalDate expirationDate,
+ BigDecimal weight) {
+
+ if (expirationDate == null) {
+ throw new IllegalArgumentException("Expiration Date cannot be null.");
+ }
+
+ if (price.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Price cannot be negative.");
+ }
+
+ if (weight.compareTo(BigDecimal.ZERO) < 0) {
+ throw new IllegalArgumentException("Weight cannot be negative.");
+ }
+
+ super(id, name, category, price);
+
+ this.expirationDate = expirationDate;
+ this.weight = weight;
+ }
+
+ @Override
+ public String productDetails() {
+ return "Food: " + name() + ", Expires: " + expirationDate;
+ }
+
+ @Override
+ public BigDecimal calculateShippingCost() {
+ return weight.multiply(BigDecimal.valueOf(50));
+ }
+
+ @Override
+ public LocalDate expirationDate() {
+ return expirationDate;
+ }
+
+ @Override
+ public double weight() {
+ return weight.doubleValue();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/Perishable.java b/src/main/java/com/example/Perishable.java
new file mode 100644
index 00000000..5e4a2bc9
--- /dev/null
+++ b/src/main/java/com/example/Perishable.java
@@ -0,0 +1,11 @@
+package com.example;
+
+import java.time.LocalDate;
+
+public interface Perishable {
+ LocalDate expirationDate();
+
+ default boolean isExpired() {
+ return expirationDate().isBefore(LocalDate.now());
+ }
+}
diff --git a/src/main/java/com/example/Product.java b/src/main/java/com/example/Product.java
new file mode 100644
index 00000000..d322d1f0
--- /dev/null
+++ b/src/main/java/com/example/Product.java
@@ -0,0 +1,40 @@
+package com.example;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.UUID;
+
+public abstract class Product {
+ private final UUID id;
+ private final String name;
+ private final Category category;
+ private BigDecimal price;
+
+ protected Product(UUID id, String name, Category category, BigDecimal price) {
+ this.id = id;
+ this.name = name;
+ this.category = category;
+ this.price = price;
+ }
+
+ public UUID uuid() {
+ return id;
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public Category category() {
+ return category;
+ }
+
+ public BigDecimal price() {
+ return price.setScale(2, RoundingMode.HALF_UP);
+ }
+
+ public void price(BigDecimal price) {
+ this.price = price;
+ }
+ public abstract String productDetails();
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/Shippable.java b/src/main/java/com/example/Shippable.java
new file mode 100644
index 00000000..6b1b2e49
--- /dev/null
+++ b/src/main/java/com/example/Shippable.java
@@ -0,0 +1,8 @@
+package com.example;
+
+import java.math.BigDecimal;
+
+public interface Shippable {
+ BigDecimal calculateShippingCost();
+ double weight();
+}
diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java
new file mode 100644
index 00000000..1eb2e199
--- /dev/null
+++ b/src/main/java/com/example/Warehouse.java
@@ -0,0 +1,92 @@
+package com.example;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class Warehouse {
+ private static final Map INSTANCES = new HashMap<>();
+ private final Map products = new HashMap<>();
+ private final Set changedProducts = new HashSet<>();
+
+ private Warehouse(String name) {
+ }
+
+ public static Warehouse getInstance(String name) {
+ return INSTANCES.computeIfAbsent(name, Warehouse::new);
+ }
+
+ public void addProduct(Product product) {
+ if (product == null) {
+ throw new IllegalArgumentException("Product cannot be null.");
+ }
+ if (products.containsKey(product.uuid())) {
+ throw new IllegalArgumentException("Product with that id already exists, use updateProduct for updates.");
+ }
+ products.put(product.uuid(), product);
+ }
+
+ public List getProducts() {
+ return List.copyOf(products.values());
+ }
+
+ public Optional getProductById(UUID id) {
+ return Optional.ofNullable(products.get(id));
+ }
+
+ public void updateProductPrice(UUID id, BigDecimal newPrice) {
+ Product product = products.get(id);
+ if (product == null) {
+ throw new NoSuchElementException("Product not found with id: " + id);
+ }
+ product.price(newPrice);
+ changedProducts.add(product);
+ }
+
+ public List expiredProducts() {
+ List expired = new ArrayList<>();
+ for (Product p : products.values()) {
+ if (p instanceof FoodProduct foodProduct && foodProduct.isExpired()) {
+ expired.add(foodProduct);
+ }
+ }
+ return Collections.unmodifiableList(expired);
+ }
+
+ public List shippableProducts() {
+ List out = new ArrayList<>();
+ for (Product p : products.values()) {
+ if (p instanceof Shippable s) {
+ out.add(s);
+ }
+ }
+ return Collections.unmodifiableList(out);
+ }
+
+ public void remove(UUID id) {
+ Product removedProduct = products.remove(id);
+ if (removedProduct != null) {
+ changedProducts.remove(removedProduct);
+ }
+ }
+
+ public void clearProducts() {
+ products.clear();
+ changedProducts.clear();
+ }
+
+ public boolean isEmpty() {
+ return products.isEmpty();
+ }
+
+ public static Warehouse getInstance() {
+ return getInstance("Default");
+ }
+
+ public Map> getProductsGroupedByCategories() {
+ return products.values()
+ .stream()
+ .collect(Collectors.groupingBy(Product :: category));
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java
index 1779fc33..9f6131dd 100644
--- a/src/main/java/com/example/WarehouseAnalyzer.java
+++ b/src/main/java/com/example/WarehouseAnalyzer.java
@@ -13,11 +13,11 @@
*/
class WarehouseAnalyzer {
private final Warehouse warehouse;
-
+
public WarehouseAnalyzer(Warehouse warehouse) {
this.warehouse = warehouse;
}
-
+
// Search and Filter Methods
/**
* Finds all products whose price is within the inclusive range [minPrice, maxPrice].
@@ -37,7 +37,7 @@ public List findProductsInPriceRange(BigDecimal minPrice, BigDecimal ma
}
return result;
}
-
+
/**
* Returns all perishable products that expire within the next {@code days} days counting from today,
* including items that expire today, and excluding items already expired. Non-perishables are ignored.
@@ -46,21 +46,21 @@ public List findProductsInPriceRange(BigDecimal minPrice, BigDecimal ma
* @param days number of days ahead to include (e.g., 3 includes today, 1, 2, and 3 days ahead)
* @return list of Perishable items expiring within the window
*/
- public List findProductsExpiringWithinDays(int days) {
+ public List findProductsExpiringWithinDays(int days) {
LocalDate today = LocalDate.now();
LocalDate end = today.plusDays(days);
- List result = new ArrayList<>();
+ List result = new ArrayList<>();
for (Product p : warehouse.getProducts()) {
- if (p instanceof Perishable per) {
- LocalDate exp = per.expirationDate();
+ if (p instanceof FoodProduct foodProduct) {
+ LocalDate exp = foodProduct.expirationDate();
if (!exp.isBefore(today) && !exp.isAfter(end)) {
- result.add(per);
+ result.add(foodProduct);
}
}
}
return result;
}
-
+
/**
* Performs a case-insensitive partial name search.
* Test expectation: searching for "milk" returns all products whose name contains that substring,
@@ -79,7 +79,7 @@ public List searchProductsByName(String searchTerm) {
}
return result;
}
-
+
/**
* Returns all products whose price is strictly greater than the given price.
* While not asserted directly by tests, this helper is consistent with price-based filtering.
@@ -96,7 +96,7 @@ public List findProductsAbovePrice(BigDecimal price) {
}
return result;
}
-
+
// Analytics Methods
/**
* Computes the average price per category using product weight as the weighting factor when available.
@@ -136,7 +136,7 @@ public Map calculateWeightedAveragePriceByCategory() {
}
return result;
}
-
+
/**
* Identifies products whose price deviates from the mean by more than the specified
* number of standard deviations. Uses population standard deviation over all products.
@@ -147,24 +147,43 @@ public Map calculateWeightedAveragePriceByCategory() {
*/
public List findPriceOutliers(double standardDeviations) {
List products = warehouse.getProducts();
- int n = products.size();
- if (n == 0) return List.of();
- double sum = products.stream().map(Product::price).mapToDouble(bd -> bd.doubleValue()).sum();
- double mean = sum / n;
- double variance = products.stream()
- .map(Product::price)
- .mapToDouble(bd -> Math.pow(bd.doubleValue() - mean, 2))
- .sum() / n;
- double std = Math.sqrt(variance);
- double threshold = standardDeviations * std;
+ if (products.size() <= 1) return List.of();
+
+ double[] prices = products.stream()
+ .mapToDouble(p -> p.price().doubleValue())
+ .toArray();
+
+ double median = calculateMedian(prices);
+
+ double[] absoluteDeviations = new double[prices.length];
+ for (int i = 0; i < prices.length; i++) {
+ absoluteDeviations[i] = Math.abs(prices[i] - median);
+ }
+ double mad = calculateMedian(absoluteDeviations);
+
+ double threshold = standardDeviations * (mad * 1.4826); // Scale factor for normal distribution
+
List outliers = new ArrayList<>();
- for (Product p : products) {
- double diff = Math.abs(p.price().doubleValue() - mean);
- if (diff > threshold) outliers.add(p);
+ for (int i = 0; i < prices.length; i++) {
+ double modifiedZScore = Math.abs(prices[i] - median) / (mad * 1.4826);
+ if (modifiedZScore >= standardDeviations) {
+ outliers.add(products.get(i));
+ }
}
return outliers;
}
-
+
+ private double calculateMedian(double[] values) {
+ double[] sorted = values.clone();
+ Arrays.sort(sorted);
+ int n = sorted.length;
+ if (n % 2 == 0) {
+ return (sorted[n/2 - 1] + sorted[n/2]) / 2.0;
+ } else {
+ return sorted[n/2];
+ }
+ }
+
/**
* Groups all shippable products into ShippingGroup buckets such that each group's total weight
* does not exceed the provided maximum. The goal is to minimize the number of groups and/or total
@@ -176,7 +195,7 @@ public List findPriceOutliers(double standardDeviations) {
*/
public List optimizeShippingGroups(BigDecimal maxWeightPerGroup) {
double maxW = maxWeightPerGroup.doubleValue();
- List items = warehouse.shippableProducts();
+ List items = new ArrayList<>(warehouse.shippableProducts()); // ändrat
// Sort by descending weight (First-Fit Decreasing)
items.sort((a, b) -> Double.compare(Objects.requireNonNullElse(b.weight(), 0.0), Objects.requireNonNullElse(a.weight(), 0.0)));
List> bins = new ArrayList<>();
@@ -201,7 +220,7 @@ public List optimizeShippingGroups(BigDecimal maxWeightPerGroup)
for (List bin : bins) groups.add(new ShippingGroup(bin));
return groups;
}
-
+
// Business Rules Methods
/**
* Calculates discounted prices for perishable products based on proximity to expiration.
@@ -219,7 +238,7 @@ public Map calculateExpirationBasedDiscounts() {
LocalDate today = LocalDate.now();
for (Product p : warehouse.getProducts()) {
BigDecimal discounted = p.price();
- if (p instanceof Perishable per) {
+ if (p instanceof FoodProduct per) {
LocalDate exp = per.expirationDate();
long daysBetween = java.time.temporal.ChronoUnit.DAYS.between(today, exp);
if (daysBetween == 0) {
@@ -237,7 +256,7 @@ public Map calculateExpirationBasedDiscounts() {
}
return result;
}
-
+
/**
* Evaluates inventory business rules and returns a summary:
* - High-value percentage: proportion of products considered high-value (e.g., price >= some threshold).
@@ -261,7 +280,7 @@ public InventoryValidation validateInventoryConstraints() {
int diversity = (int) items.stream().map(Product::category).distinct().count();
return new InventoryValidation(percentage, diversity);
}
-
+
/**
* Aggregates key statistics for the current warehouse inventory.
* Test expectation for a 4-item setup:
@@ -277,11 +296,11 @@ public InventoryValidation validateInventoryConstraints() {
public InventoryStatistics getInventoryStatistics() {
List items = warehouse.getProducts();
int totalProducts = items.size();
- BigDecimal totalValue = items.stream().map(Product::price).reduce(BigDecimal.ZERO, BigDecimal::add);
+ BigDecimal totalValue = items.stream().map(Product::price).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(2, RoundingMode.HALF_UP);
BigDecimal averagePrice = totalProducts == 0 ? BigDecimal.ZERO : totalValue.divide(BigDecimal.valueOf(totalProducts), 2, RoundingMode.HALF_UP);
int expiredCount = 0;
for (Product p : items) {
- if (p instanceof Perishable per && per.expirationDate().isBefore(LocalDate.now())) {
+ if (p instanceof FoodProduct per && per.expirationDate().isBefore(LocalDate.now())) {
expiredCount++;
}
}
diff --git a/src/test/java/com/example/BasicTest.java b/src/test/java/com/example/BasicTest.java
index 8a45877e..c9575d47 100644
--- a/src/test/java/com/example/BasicTest.java
+++ b/src/test/java/com/example/BasicTest.java
@@ -360,13 +360,13 @@ void should_findExpiredProducts_when_checkingPerishables() {
warehouse.addProduct(laptop);
// Act
- List expiredItems = warehouse.expiredProducts();
+ List expiredItems = warehouse.expiredProducts();
// Assert
assertThat(expiredItems)
.as("Only products that have passed their expiration date should be returned.")
.hasSize(1)
- .containsExactly((Perishable) oldMilk);
+ .containsExactly((FoodProduct) oldMilk);
}
@Test
diff --git a/src/test/java/com/example/EdgeCaseTest.java b/src/test/java/com/example/EdgeCaseTest.java
index fb4f9358..446d2191 100644
--- a/src/test/java/com/example/EdgeCaseTest.java
+++ b/src/test/java/com/example/EdgeCaseTest.java
@@ -110,7 +110,7 @@ void should_findProductsExpiringWithinDays() {
warehouse.addProduct(nonPerishable);
// Act
- List expiringWithin3Days = analyzer.findProductsExpiringWithinDays(3);
+ List expiringWithin3Days = analyzer.findProductsExpiringWithinDays(3);
// Assert
assertThat(expiringWithin3Days)