From 4aaeb4d550d0b1e97b6f9343bf0ee606962c4923 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 06:13:02 +0000 Subject: [PATCH 01/10] Setting up GitHub Classroom Feedback From d1f0a89b23f840bc02a227d7415f499506514890 Mon Sep 17 00:00:00 2001 From: Martin Blomberg Date: Tue, 14 Oct 2025 09:56:29 +0200 Subject: [PATCH 02/10] Adding missing testmethods --- src/test/java/com/example/BasicTest.java | 134 ++++++++++++++++++++++- 1 file changed, 131 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/example/BasicTest.java b/src/test/java/com/example/BasicTest.java index a11fc97..8a45877 100644 --- a/src/test/java/com/example/BasicTest.java +++ b/src/test/java/com/example/BasicTest.java @@ -6,6 +6,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.UUID; @@ -118,22 +119,149 @@ void should_beEmpty_when_newlySetUp() { .isTrue(); } + // --- Singleton and Factory Pattern Tests --- + @Nested @DisplayName("Factory and Singleton Behavior") class FactoryTests { - // ... (omitted for brevity, same as before) + + @Test + @DisplayName("✅ should not have any public constructors") + void should_notHavePublicConstructors() { + Constructor[] constructors = Warehouse.class.getConstructors(); + assertThat(constructors) + .as("Warehouse should only be accessed via its getInstance() factory method.") + .isEmpty(); + } + + @Test + @DisplayName("✅ should be created by calling the 'getInstance' factory method") + void should_beCreated_when_usingFactoryMethod() { + Warehouse defaultWarehouse = Warehouse.getInstance(); + assertThat(defaultWarehouse).isNotNull(); + } + + @Test + @DisplayName("✅ should return the same instance for the same name") + void should_returnSameInstance_when_nameIsIdentical() { + Warehouse warehouse1 = Warehouse.getInstance("GlobalStore"); + Warehouse warehouse2 = Warehouse.getInstance("GlobalStore"); + assertThat(warehouse1) + .as("Warehouses with the same name should be the same singleton instance.") + .isSameAs(warehouse2); + } } @Nested @DisplayName("Product Management") class ProductManagementTests { - // ... (most tests omitted for brevity, same as before) + @Test + @DisplayName("✅ should be empty when new") + void should_beEmpty_when_new() { + assertThat(warehouse.isEmpty()) + .as("A new warehouse instance should have no products.") + .isTrue(); + } + + @Test + @DisplayName("✅ should return an empty product list when new") + void should_returnEmptyProductList_when_new() { + assertThat(warehouse.getProducts()) + .as("A new warehouse should return an empty list, not null.") + .isEmpty(); + } + + @Test + @DisplayName("✅ should store various product types (Food, Electronics)") + void should_storeHeterogeneousProducts() { + // Arrange + Product milk = new FoodProduct(UUID.randomUUID(), "Milk", Category.of("Dairy"), new BigDecimal("15.50"), LocalDate.now().plusDays(7), new BigDecimal("1.0")); + Product laptop = new ElectronicsProduct(UUID.randomUUID(), "Laptop", Category.of("Electronics"), new BigDecimal("12999"), 24, new BigDecimal("2.2")); + + // Act + warehouse.addProduct(milk); + warehouse.addProduct(laptop); + + // Assert + assertThat(warehouse.getProducts()) + .as("Warehouse should correctly store different subtypes of Product.") + .hasSize(2) + .containsExactlyInAnyOrder(milk, laptop); + } + + + + @Test + @DisplayName("❌ should throw an exception when adding a product with a duplicate ID") + void should_throwException_when_addingProductWithDuplicateId() { + // Arrange + UUID sharedId = UUID.randomUUID(); + Product milk = new FoodProduct(sharedId, "Milk", Category.of("Dairy"), BigDecimal.ONE, LocalDate.now(), BigDecimal.ONE); + Product cheese = new FoodProduct(sharedId, "Cheese", Category.of("Dairy"), BigDecimal.TEN, LocalDate.now(), BigDecimal.TEN); + warehouse.addProduct(milk); + + // Act & Assert + assertThatThrownBy(() -> warehouse.addProduct(cheese)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Product with that id already exists, use updateProduct for updates."); + } + + @Test + @DisplayName("✅ should update the price of an existing product") + void should_updateExistingProductPrice() { + // Arrange + Product milk = new FoodProduct(UUID.randomUUID(), "Milk", Category.of("Dairy"), new BigDecimal("15.50"), LocalDate.now().plusDays(7), new BigDecimal("1.0")); + warehouse.addProduct(milk); + BigDecimal newPrice = new BigDecimal("17.00"); + + // Act + warehouse.updateProductPrice(milk.uuid(), newPrice); + + // Assert + assertThat(warehouse.getProductById(milk.uuid())) + .as("The product's price should be updated to the new value.") + .isPresent() + .hasValueSatisfying(product -> + assertThat(product.price()).isEqualByComparingTo(newPrice) + ); + } + + @Test + @DisplayName("✅ should group products correctly by their category") + void should_groupProductsByCategories() { + // Arrange + Product milk = new FoodProduct(UUID.randomUUID(), "Milk", Category.of("Dairy"), BigDecimal.ONE, LocalDate.now(), BigDecimal.ONE); + Product apple = new FoodProduct(UUID.randomUUID(), "Apple", Category.of("Fruit"), BigDecimal.ONE, LocalDate.now(), BigDecimal.ONE); + Product laptop = new ElectronicsProduct(UUID.randomUUID(), "Laptop", Category.of("Electronics"), BigDecimal.TEN, 24, BigDecimal.TEN); + warehouse.addProduct(milk); + warehouse.addProduct(apple); + warehouse.addProduct(laptop); + + Map> expectedMap = Map.of( + Category.of("Dairy"), List.of(milk), + Category.of("Fruit"), List.of(apple), + Category.of("Electronics"), List.of(laptop) + ); + + // Act & Assert + assertThat(warehouse.getProductsGroupedByCategories()) + .as("The returned map should have categories as keys and lists of products as values.") + .isEqualTo(expectedMap); + } @Test @DisplayName("🔒 should return an unmodifiable list of products to protect internal state") void should_returnUnmodifiableProductList() { - // ... (same as before) + // Arrange + Product milk = new FoodProduct(UUID.randomUUID(), "Milk", Category.of("Dairy"), BigDecimal.ONE, LocalDate.now(), BigDecimal.ONE); + warehouse.addProduct(milk); + List products = warehouse.getProducts(); + + // Act & Assert + assertThatThrownBy(products::clear) + .as("The list returned by getProducts() should be immutable to prevent external modification.") + .isInstanceOf(UnsupportedOperationException.class); } @Test From 1134b6bde3563d55fa73f2704fb7d1fd4f4d63f9 Mon Sep 17 00:00:00 2001 From: mattknatt Date: Tue, 14 Oct 2025 18:24:00 +0200 Subject: [PATCH 03/10] Basic Test passed --- src/main/java/com/example/Category.java | 39 +++++++ .../java/com/example/ElectronicsProduct.java | 34 ++++++ src/main/java/com/example/FoodProduct.java | 37 +++++++ src/main/java/com/example/Perishable.java | 12 ++ src/main/java/com/example/Product.java | 43 ++++++++ src/main/java/com/example/Shippable.java | 9 ++ src/main/java/com/example/Warehouse.java | 104 ++++++++++++++++++ src/test/java/com/example/BasicTest.java | 4 +- 8 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/Category.java create mode 100644 src/main/java/com/example/ElectronicsProduct.java create mode 100644 src/main/java/com/example/FoodProduct.java create mode 100644 src/main/java/com/example/Perishable.java create mode 100644 src/main/java/com/example/Product.java create mode 100644 src/main/java/com/example/Shippable.java create mode 100644 src/main/java/com/example/Warehouse.java diff --git a/src/main/java/com/example/Category.java b/src/main/java/com/example/Category.java new file mode 100644 index 0000000..e49b706 --- /dev/null +++ b/src/main/java/com/example/Category.java @@ -0,0 +1,39 @@ +package com.example; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class Category { + private static final Map CATEGORIES = new ConcurrentHashMap<>(); + private final String name; + + private Category(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"); + this.name = normalizedName(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"); + } + name = normalizedName(name); + return CATEGORIES.computeIfAbsent(name, Category::new); + } + + public String getName() { + return name; + } + + public static String normalizedName(String name) { + return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase(); + } + +} + + diff --git a/src/main/java/com/example/ElectronicsProduct.java b/src/main/java/com/example/ElectronicsProduct.java new file mode 100644 index 0000000..a6377bb --- /dev/null +++ b/src/main/java/com/example/ElectronicsProduct.java @@ -0,0 +1,34 @@ +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; + + protected 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; + } + + public String productDetails() { + return "Electronics: " + name() + ", Warranty: " + warrantyMonths + " months"; + } + + public BigDecimal calculateShippingCost() { + BigDecimal shippingCost = BigDecimal.valueOf(79); + if (this.weight.compareTo(BigDecimal.valueOf(5.0)) > 0) { + return shippingCost.add(BigDecimal.valueOf(49)); + } else { + return shippingCost; + } + } + + public double weight() { + return this.weight.doubleValue(); + } + +} diff --git a/src/main/java/com/example/FoodProduct.java b/src/main/java/com/example/FoodProduct.java new file mode 100644 index 0000000..1962a17 --- /dev/null +++ b/src/main/java/com/example/FoodProduct.java @@ -0,0 +1,37 @@ +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; + + protected FoodProduct(UUID id, String name, Category category, BigDecimal price, LocalDate expirationDate, BigDecimal weight) { + if (price.doubleValue() < 0) throw new IllegalArgumentException("Price cannot be negative."); + super(id, name, category, price); + this.expirationDate = expirationDate; + if (weight.doubleValue() < 0) throw new IllegalArgumentException("Weight cannot be negative."); + this.weight = weight; + } + + + public String productDetails() { + return "Food: " + name() + ", Expires: " + this.expirationDate; + } + + public LocalDate expirationDate() { + return this.expirationDate; + } + + public BigDecimal calculateShippingCost() { + return this.weight.multiply(BigDecimal.valueOf(50)); + } + + @Override + public double weight() { + return this.weight.doubleValue(); + } + +} diff --git a/src/main/java/com/example/Perishable.java b/src/main/java/com/example/Perishable.java new file mode 100644 index 0000000..d2f1af2 --- /dev/null +++ b/src/main/java/com/example/Perishable.java @@ -0,0 +1,12 @@ +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 0000000..4cac369 --- /dev/null +++ b/src/main/java/com/example/Product.java @@ -0,0 +1,43 @@ +package com.example; + +import java.math.BigDecimal; +import java.util.Objects; +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 = Objects.requireNonNull(id, "ID can't be null"); + this.name = Objects.requireNonNull(name, "Name can't be null"); + this.category = Objects.requireNonNull(category, "Category can't be null"); + this.price = Objects.requireNonNull(price, "Price can't be null"); + } + + public UUID id() { + return id; + } + + public String name() { + return name; + } + + public Category category() { + return category; + } + + public BigDecimal price() { + return price; + } + + public void setPrice(BigDecimal price) { + if (price.doubleValue() < 0) throw new IllegalArgumentException("Price can't be null"); + this.price = price; + } + + public abstract String productDetails(); +} diff --git a/src/main/java/com/example/Shippable.java b/src/main/java/com/example/Shippable.java new file mode 100644 index 0000000..2e84311 --- /dev/null +++ b/src/main/java/com/example/Shippable.java @@ -0,0 +1,9 @@ +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 0000000..60a7b1c --- /dev/null +++ b/src/main/java/com/example/Warehouse.java @@ -0,0 +1,104 @@ +package com.example; + +import java.util.stream.Collectors; +import java.math.BigDecimal; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + + +public class Warehouse { + private static final Map WAREHOUSES = new ConcurrentHashMap<>(); + + private final String name; + + private final Map products; + + private final Set changedProductIds; + + private Warehouse(String name) { + this.name = name; + this.products = new ConcurrentHashMap<>(); + this.changedProductIds = Collections.newSetFromMap(new ConcurrentHashMap<>()); + } + + public static Warehouse getInstance(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Warehouse name must be provided"); + } + return WAREHOUSES.computeIfAbsent(name, Warehouse::new); + } + + public void addProduct(Product product) { + if (product == null) { + throw new IllegalArgumentException("Product cannot be null."); + } + products.putIfAbsent(product.id(), product); + } + + public List getProducts() { + return Collections.unmodifiableList(products.values().stream().toList()); + } + + public Optional getProductById(UUID id) { + if (id == null) { + return Optional.empty(); + } + return Optional.ofNullable(products.get(id)); + } + + public Map> getProductsGroupedByCategories() { + return products.values() + .stream() + .collect(Collectors.groupingBy(Product::category)); + + } + + public void clearProducts() { + Warehouse warehouse = WAREHOUSES.get(name); + if (warehouse != null) { + warehouse.products.clear(); + warehouse.changedProductIds.clear(); + } + } + + public boolean isEmpty() { + return products.isEmpty(); + } + + public void updateProductPrice(UUID id, BigDecimal newPrice) { + Product product = getProductById(id). + orElseThrow(() -> new NoSuchElementException("Product not found with id: " + id)); + product.setPrice(newPrice); + changedProductIds.add(id); + } + + public List getChangedProducts() { + return changedProductIds.stream() + .map(products::get) + .filter(java.util.Objects::nonNull) + .toList(); + } + + public List expiredProducts() { + return products.values().stream() + .filter(p -> p instanceof Perishable) + .filter(p -> ((Perishable) p).isExpired()) + .map(p -> (Perishable) p) + .collect(Collectors.toList()); + + } + + public List shippableProducts() { + return products.values().stream() + .filter(p -> p instanceof Shippable) + .map(p -> (Shippable) p) + .collect(Collectors.toList()); + } + + public void remove(UUID id) { + if (id != null) { + products.remove(id); + changedProductIds.remove(id); + } + } +} diff --git a/src/test/java/com/example/BasicTest.java b/src/test/java/com/example/BasicTest.java index a11fc97..5b0bd04 100644 --- a/src/test/java/com/example/BasicTest.java +++ b/src/test/java/com/example/BasicTest.java @@ -145,13 +145,13 @@ void should_removeExistingProduct() { assertThat(warehouse.getProducts()).hasSize(1); // Act - warehouse.remove(milk.uuid()); + warehouse.remove(milk.id()); // Assert assertThat(warehouse.isEmpty()) .as("Warehouse should be empty after the only product is removed.") .isTrue(); - assertThat(warehouse.getProductById(milk.uuid())) + assertThat(warehouse.getProductById(milk.id())) .as("The removed product should no longer be found.") .isEmpty(); } From 1b767a3e91fbe9d4a817d278302af98cbd18c813 Mon Sep 17 00:00:00 2001 From: mattknatt Date: Tue, 14 Oct 2025 19:15:29 +0200 Subject: [PATCH 04/10] Modified findPriceOutliers in WarehouseAnalyzer to utilize IQR while finding outliers. All tests in BasicTest and EdgeCaseTest are green. --- .../java/com/example/WarehouseAnalyzer.java | 50 ++++++++++++------- src/test/java/com/example/EdgeCaseTest.java | 2 +- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 1779fc3..dc6a419 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -142,27 +142,41 @@ public Map calculateWeightedAveragePriceByCategory() { * number of standard deviations. Uses population standard deviation over all products. * Test expectation: with a mostly tight cluster and two extremes, calling with 2.0 returns the two extremes. * - * @param standardDeviations threshold in standard deviations (e.g., 2.0) + * @param factor threshold in IQR calculation * @return list of products considered outliers */ - public List findPriceOutliers(double standardDeviations) { + public List findPriceOutliers(double factor) { 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; - List outliers = new ArrayList<>(); - for (Product p : products) { - double diff = Math.abs(p.price().doubleValue() - mean); - if (diff > threshold) outliers.add(p); - } - return outliers; + if (products.size() < 4) return List.of(); + + List prices = products.stream() + .map(p -> p.price().doubleValue()) + .sorted() + .toList(); + + int n = prices.size(); + double q1 = median(prices.subList(0, n / 2)); + double q3 = median(prices.subList((n + 1) / 2, n)); + double iqr = q3 - q1; + + double lowerLimit = q1 - factor * iqr; + double upperLimit = q3 + factor * iqr; + + return products.stream() + .filter(p -> { + double price = p.price().doubleValue(); + return price < lowerLimit || price > upperLimit; + }) + .toList(); + } + + private double median(List sortedList){ + int n = sortedList.size(); + if (n % 2 == 0) + return (sortedList.get(n / 2 - 1) + sortedList.get(n / 2)) / 2.0; + else + return sortedList.get(n / 2); + } /** diff --git a/src/test/java/com/example/EdgeCaseTest.java b/src/test/java/com/example/EdgeCaseTest.java index fb4f935..aa19087 100644 --- a/src/test/java/com/example/EdgeCaseTest.java +++ b/src/test/java/com/example/EdgeCaseTest.java @@ -208,7 +208,7 @@ void should_identifyPriceOutliers_usingStatistics() { warehouse.addProduct(outlierLow); // Act - Find outliers (products with price > 2 standard deviations from mean) - List outliers = analyzer.findPriceOutliers(2.0); // 2 standard deviations + List outliers = analyzer.findPriceOutliers(1.5); // 2 standard deviations // Assert assertThat(outliers) From f5589e3dd1b6cbb1df39cb0449c6ecb8f77d38e3 Mon Sep 17 00:00:00 2001 From: mattknatt Date: Tue, 14 Oct 2025 19:19:27 +0200 Subject: [PATCH 05/10] A little cleanup. All tests green --- src/main/java/com/example/Category.java | 1 - src/main/java/com/example/Warehouse.java | 2 +- src/main/java/com/example/WarehouseAnalyzer.java | 2 -- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/com/example/Category.java b/src/main/java/com/example/Category.java index e49b706..2282ad7 100644 --- a/src/main/java/com/example/Category.java +++ b/src/main/java/com/example/Category.java @@ -1,6 +1,5 @@ package com.example; -import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index 60a7b1c..9e51f5f 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -36,7 +36,7 @@ public void addProduct(Product product) { } public List getProducts() { - return Collections.unmodifiableList(products.values().stream().toList()); + return products.values().stream().toList(); } public Optional getProductById(UUID id) { diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index dc6a419..0cb72e0 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -1,7 +1,6 @@ package com.example; import java.math.BigDecimal; -import java.math.MathContext; import java.math.RoundingMode; import java.time.LocalDate; import java.util.*; @@ -259,7 +258,6 @@ public Map calculateExpirationBasedDiscounts() { * when percentage exceeds 70%. * - Category diversity: count of distinct categories in the inventory. The tests expect at least 2. * - Convenience booleans: highValueWarning (percentage > 70%) and minimumDiversity (category count >= 2). - * * Note: The exact high-value threshold is implementation-defined, but the provided tests create a clear * separation using very expensive electronics (e.g., 2000) vs. low-priced food items (e.g., 10), * allowing percentage computation regardless of the chosen cutoff as long as it matches the scenario. From 81d5df399289e6dd4c83866bb226a2417b8afeeb Mon Sep 17 00:00:00 2001 From: mattknatt Date: Thu, 16 Oct 2025 07:52:04 +0200 Subject: [PATCH 06/10] Changed documentation in WarehouseAnalyzer and EdgeCaseTest to comment on IQR implementation. --- src/main/java/com/example/WarehouseAnalyzer.java | 10 ++++++---- src/test/java/com/example/EdgeCaseTest.java | 10 +++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 0cb72e0..54fdd90 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -137,11 +137,13 @@ public Map calculateWeightedAveragePriceByCategory() { } /** - * Identifies products whose price deviates from the mean by more than the specified - * number of standard deviations. Uses population standard deviation over all products. - * Test expectation: with a mostly tight cluster and two extremes, calling with 2.0 returns the two extremes. + * Identifies products whose prices are statistical outliers, + * using the interquartile range (IQR) method. + * Products with prices outside the IQR threshold are considered outliers. + * Test expectation: with a mostly tight cluster and two extremes, + * calling with 1.5 (the typical IQR threshold) returns the two extremes. * - * @param factor threshold in IQR calculation + * @param factor multiplier in IQR calculation * @return list of products considered outliers */ public List findPriceOutliers(double factor) { diff --git a/src/test/java/com/example/EdgeCaseTest.java b/src/test/java/com/example/EdgeCaseTest.java index aa19087..c35f61a 100644 --- a/src/test/java/com/example/EdgeCaseTest.java +++ b/src/test/java/com/example/EdgeCaseTest.java @@ -188,9 +188,9 @@ void should_calculateWeightedAveragePrice_byCategory() { @Test @DisplayName("📊 should identify products with abnormal pricing (outliers)") /** - * Detects price outliers using mean and standard deviation. + * Detects price outliers using the interquartile range (IQR) method. * Arrange: mostly normal-priced items around 15, plus very cheap and very expensive outliers. - * Act: analyzer.findPriceOutliers(2.0). + * Act: analyzer.findPriceOutliers(1.5) - considers prices outside 1.5×IQR from Q1 or Q3 as outliers. * Assert: returns exactly the two outliers ("Expensive" and "Cheap"). */ void should_identifyPriceOutliers_usingStatistics() { @@ -207,12 +207,12 @@ void should_identifyPriceOutliers_usingStatistics() { warehouse.addProduct(outlierHigh); warehouse.addProduct(outlierLow); - // Act - Find outliers (products with price > 2 standard deviations from mean) - List outliers = analyzer.findPriceOutliers(1.5); // 2 standard deviations + // Act - Find outliers (products with prices outside 1.5×IQR from the interquartile range) + List outliers = analyzer.findPriceOutliers(1.5); // 1.5xIQR threshold // Assert assertThat(outliers) - .as("Should identify statistical outliers beyond 2 standard deviations") + .as("Should identify price outliers beyond the 1.5×IQR threshold") .hasSize(2) .extracting(Product::name) .containsExactlyInAnyOrder("Expensive", "Cheap"); From 2aa6da708dd55b141e0d4e085be23783202c583b Mon Sep 17 00:00:00 2001 From: mattknatt Date: Thu, 16 Oct 2025 08:35:00 +0200 Subject: [PATCH 07/10] Small changes due to GitHub AI feedback. Product constructor access changed to default. --- src/main/java/com/example/Product.java | 4 ++-- src/main/java/com/example/Warehouse.java | 5 ++++- src/main/java/com/example/WarehouseAnalyzer.java | 11 +++++------ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/example/Product.java b/src/main/java/com/example/Product.java index 4cac369..34e7c91 100644 --- a/src/main/java/com/example/Product.java +++ b/src/main/java/com/example/Product.java @@ -11,7 +11,7 @@ public abstract class Product { private final Category category; private BigDecimal price; - protected Product(UUID id, String name, Category category, BigDecimal price) { + Product(UUID id, String name, Category category, BigDecimal price) { this.id = Objects.requireNonNull(id, "ID can't be null"); this.name = Objects.requireNonNull(name, "Name can't be null"); this.category = Objects.requireNonNull(category, "Category can't be null"); @@ -35,7 +35,7 @@ public BigDecimal price() { } public void setPrice(BigDecimal price) { - if (price.doubleValue() < 0) throw new IllegalArgumentException("Price can't be null"); + if (price == null || price.doubleValue() < 0) throw new IllegalArgumentException("Price can't be null or negative."); this.price = price; } diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index 9e51f5f..3a74b18 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -32,7 +32,10 @@ public void addProduct(Product product) { if (product == null) { throw new IllegalArgumentException("Product cannot be null."); } - products.putIfAbsent(product.id(), product); + if (products.putIfAbsent(product.id(), product) == null) { + changedProductIds.add(product.id()); + } + } public List getProducts() { diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 54fdd90..04374d4 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -1,5 +1,6 @@ package com.example; +import java.io.Serializable; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; @@ -173,11 +174,9 @@ public List findPriceOutliers(double factor) { private double median(List sortedList){ int n = sortedList.size(); - if (n % 2 == 0) - return (sortedList.get(n / 2 - 1) + sortedList.get(n / 2)) / 2.0; - else - return sortedList.get(n / 2); - + return n % 2 == 0 + ? (sortedList.get(n/2 - 1) + sortedList.get(n/2)) / 2.0 + : sortedList.get(n/2); } /** @@ -309,7 +308,7 @@ public InventoryStatistics getInventoryStatistics() { /** * Represents a group of products for shipping */ -class ShippingGroup { +class ShippingGroup implements Serializable { private final List products; private final Double totalWeight; private final BigDecimal totalShippingCost; From 96a36d4c79acddcb2811b16a4dab806dec6978af Mon Sep 17 00:00:00 2001 From: mattknatt Date: Thu, 16 Oct 2025 08:57:12 +0200 Subject: [PATCH 08/10] Minor changes due to GitHub AI feedback. --- src/main/java/com/example/Warehouse.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index 3a74b18..af27d02 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -77,8 +77,8 @@ public void updateProductPrice(UUID id, BigDecimal newPrice) { public List getChangedProducts() { return changedProductIds.stream() + .filter(products::containsKey) .map(products::get) - .filter(java.util.Objects::nonNull) .toList(); } From db0fda700b5c3404816e29cbe9010483734c66a9 Mon Sep 17 00:00:00 2001 From: mattknatt Date: Mon, 20 Oct 2025 14:27:11 +0200 Subject: [PATCH 09/10] Merged changes in BasicTest, updated addProducts method. --- src/main/java/com/example/Product.java | 2 +- src/main/java/com/example/Warehouse.java | 11 +++++++++-- src/test/java/com/example/BasicTest.java | 4 ++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/Product.java b/src/main/java/com/example/Product.java index 34e7c91..52bfdac 100644 --- a/src/main/java/com/example/Product.java +++ b/src/main/java/com/example/Product.java @@ -18,7 +18,7 @@ public abstract class Product { this.price = Objects.requireNonNull(price, "Price can't be null"); } - public UUID id() { + public UUID uuid() { return id; } diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index af27d02..404a1cf 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -28,14 +28,21 @@ public static Warehouse getInstance(String name) { return WAREHOUSES.computeIfAbsent(name, Warehouse::new); } + public static Warehouse getInstance() { + return getInstance("DefaultWarehouse"); + } + public void addProduct(Product product) { if (product == null) { throw new IllegalArgumentException("Product cannot be null."); } - if (products.putIfAbsent(product.id(), product) == null) { - changedProductIds.add(product.id()); + Product existingProduct = products.putIfAbsent(product.uuid(), product); + if (existingProduct != null) { + throw new IllegalArgumentException("Product with that id already exists, use updateProduct for updates."); } + changedProductIds.add(product.uuid()); + } public List getProducts() { diff --git a/src/test/java/com/example/BasicTest.java b/src/test/java/com/example/BasicTest.java index e6fe0c2..8a45877 100644 --- a/src/test/java/com/example/BasicTest.java +++ b/src/test/java/com/example/BasicTest.java @@ -273,13 +273,13 @@ void should_removeExistingProduct() { assertThat(warehouse.getProducts()).hasSize(1); // Act - warehouse.remove(milk.id()); + warehouse.remove(milk.uuid()); // Assert assertThat(warehouse.isEmpty()) .as("Warehouse should be empty after the only product is removed.") .isTrue(); - assertThat(warehouse.getProductById(milk.id())) + assertThat(warehouse.getProductById(milk.uuid())) .as("The removed product should no longer be found.") .isEmpty(); } From 6e8353c6d8cda924f19e2b7747f9c0691c59553e Mon Sep 17 00:00:00 2001 From: mattknatt Date: Mon, 20 Oct 2025 15:10:26 +0200 Subject: [PATCH 10/10] Updates atfer feedback from GitHub AI --- src/main/java/com/example/FoodProduct.java | 2 +- src/main/java/com/example/Perishable.java | 4 ++-- src/main/java/com/example/Warehouse.java | 4 +++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/FoodProduct.java b/src/main/java/com/example/FoodProduct.java index 1962a17..c934106 100644 --- a/src/main/java/com/example/FoodProduct.java +++ b/src/main/java/com/example/FoodProduct.java @@ -10,9 +10,9 @@ public class FoodProduct extends Product implements Perishable, Shippable { protected FoodProduct(UUID id, String name, Category category, BigDecimal price, LocalDate expirationDate, BigDecimal weight) { if (price.doubleValue() < 0) throw new IllegalArgumentException("Price cannot be negative."); + if (weight.doubleValue() < 0) throw new IllegalArgumentException("Weight cannot be negative."); super(id, name, category, price); this.expirationDate = expirationDate; - if (weight.doubleValue() < 0) throw new IllegalArgumentException("Weight cannot be negative."); this.weight = weight; } diff --git a/src/main/java/com/example/Perishable.java b/src/main/java/com/example/Perishable.java index d2f1af2..9c80d7c 100644 --- a/src/main/java/com/example/Perishable.java +++ b/src/main/java/com/example/Perishable.java @@ -6,7 +6,7 @@ public interface Perishable { LocalDate expirationDate(); - default boolean isExpired() { - return expirationDate().isBefore(LocalDate.now()); + default boolean isExpired(LocalDate referenceDate) { + return expirationDate().isBefore(referenceDate); } } diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index 404a1cf..4cc283a 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -1,5 +1,6 @@ package com.example; +import java.time.LocalDate; import java.util.stream.Collectors; import java.math.BigDecimal; import java.util.*; @@ -90,9 +91,10 @@ public List getChangedProducts() { } public List expiredProducts() { + LocalDate today = LocalDate.now(); return products.values().stream() .filter(p -> p instanceof Perishable) - .filter(p -> ((Perishable) p).isExpired()) + .filter(p -> ((Perishable) p).isExpired(today)) .map(p -> (Perishable) p) .collect(Collectors.toList());