From ec73b9c7fb0baf6332e4296e748d36e6d020b33e Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:51:43 +0000 Subject: [PATCH 01/16] GitHub Classroom Feedback --- .github/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .github/.keep diff --git a/.github/.keep b/.github/.keep new file mode 100644 index 00000000..e69de29b From c2cec3a5bed060e7e5d586e44a6b4e094f246c25 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Sun, 5 Oct 2025 20:51:44 +0000 Subject: [PATCH 02/16] Setting up GitHub Classroom Feedback From 8988caadad048954ce7373f5811c28d0ff9340e8 Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DenSel@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:59:42 +0200 Subject: [PATCH 03/16] Allt klart enligt: - Category private constructor public static factory Category.of(String name). - Validate input - Normalize name - Cache/flyweight --- src/main/java/com/example/Category.java | 48 +++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/main/java/com/example/Category.java diff --git a/src/main/java/com/example/Category.java b/src/main/java/com/example/Category.java new file mode 100644 index 00000000..932e4724 --- /dev/null +++ b/src/main/java/com/example/Category.java @@ -0,0 +1,48 @@ +package com.example; +import java.util.HashMap; +import java.util.Map; + +public class Category { + private String name; + + // Constructor + private Category(String nameConstructor) { + this.name = nameConstructor; + } + + // Cache + private static final Map categories = new HashMap<>(); + + // Factory + public static Category of(String nameOf){ + if (nameOf == null){ + IO.println("Category name can't be null"); + return null; + } + + if (nameOf.isBlank()){ + IO.println("Category name can't be blank"); + return null; + } + + // Normalize to capital letter + String normalized = normalize(nameOf); + + // Check the cache, create new or return cached instance + if (categories.containsKey(normalized)){ + return categories.get(normalized); + } + else { + Category category = new Category(normalized); + categories.put(normalized, category); + return category; + } + } + + // Method to normalize to capital letter + private static String normalize(String input) { + input = input.trim().toLowerCase(); + input = input.substring(0,1).toUpperCase() + input.substring(1); + return input; + } +} \ No newline at end of file From 05674781cee188ed300398658164126ecd8b1fc1 Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DenSel@users.noreply.github.com> Date: Thu, 16 Oct 2025 22:09:23 +0200 Subject: [PATCH 04/16] Product, FoodProduct, Shippable, Perishable grunder klara --- src/main/java/com/example/FoodProduct.java | 35 +++++++++++++++++++ src/main/java/com/example/Perishable.java | 11 ++++++ src/main/java/com/example/Product.java | 40 ++++++++++++++++++++++ src/main/java/com/example/Shippable.java | 6 ++++ 4 files changed, 92 insertions(+) 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 diff --git a/src/main/java/com/example/FoodProduct.java b/src/main/java/com/example/FoodProduct.java new file mode 100644 index 00000000..6b5e1f0d --- /dev/null +++ b/src/main/java/com/example/FoodProduct.java @@ -0,0 +1,35 @@ +package com.example; +import java.math.BigDecimal; +import java.time.LocalDate; + +public class FoodProduct extends Product implements Perishable, Shippable { + private final LocalDate expirationDate; + private final BigDecimal weight; // kg + + public FoodProduct(String nameInput, Category categoryInput, BigDecimal priceInput, LocalDate expirationDateInput, BigDecimal weightInput) { + super(nameInput, categoryInput, ensurePositive(priceInput, "Price")); + + this.expirationDate = expirationDateInput; + this.weight = ensurePositive(weightInput, "Weight"); + } + + // Make sure price is positive + private static BigDecimal ensurePositive(BigDecimal value, String valueType) { + // Uses built in constant for 0 + if (value.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException(valueType + " cannot be negative."); + } + return value; + } + + @Override + public String productDetails() { + return "Food: " + name() + ", Expires: " + expirationDate + ", Price: " + price() + ", Weight: " + weight + "kg"; + } + + @Override + public BigDecimal calculateShippingCost() { + // Multiply weight with 50 + return weight.multiply(new BigDecimal(50)); + } +} diff --git a/src/main/java/com/example/Perishable.java b/src/main/java/com/example/Perishable.java new file mode 100644 index 00000000..750bc169 --- /dev/null +++ b/src/main/java/com/example/Perishable.java @@ -0,0 +1,11 @@ +package com.example; + +import java.time.LocalDate; + +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..1973328f --- /dev/null +++ b/src/main/java/com/example/Product.java @@ -0,0 +1,40 @@ +package com.example; + +import java.math.BigDecimal; +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(String name, Category category, BigDecimal price) { + this.id = UUID.randomUUID(); + 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; + } + + public void price(BigDecimal price) { + this.price = price; + } + + abstract public 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 00000000..948efc86 --- /dev/null +++ b/src/main/java/com/example/Shippable.java @@ -0,0 +1,6 @@ +package com.example; +import java.math.BigDecimal; + +interface Shippable { + BigDecimal calculateShippingCost(); +} From c2c53161318be141c1de71841a5d59c0a0759f16 Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DenSel@users.noreply.github.com> Date: Wed, 22 Oct 2025 23:01:55 +0200 Subject: [PATCH 05/16] clear --- src/main/java/com/example/Category.java | 16 ++- .../java/com/example/ElectronicsProduct.java | 55 ++++++++ src/main/java/com/example/FoodProduct.java | 21 ++- src/main/java/com/example/Product.java | 6 +- src/main/java/com/example/Shippable.java | 1 + src/main/java/com/example/Warehouse.java | 126 ++++++++++++++++++ .../java/com/example/WarehouseAnalyzer.java | 25 ++-- src/test/java/com/example/BasicTest.java | 2 +- 8 files changed, 223 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/example/ElectronicsProduct.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 index 932e4724..3b98dd66 100644 --- a/src/main/java/com/example/Category.java +++ b/src/main/java/com/example/Category.java @@ -6,23 +6,21 @@ public class Category { private String name; // Constructor - private Category(String nameConstructor) { - this.name = nameConstructor; + private Category(String name) { + this.name = name; } - // Cache + // Create 1 static cache map when initialized, then points to the HashMap reference. private static final Map categories = new HashMap<>(); // Factory public static Category of(String nameOf){ if (nameOf == null){ - IO.println("Category name can't be null"); - return null; + throw new IllegalArgumentException("Category name can't be null"); } if (nameOf.isBlank()){ - IO.println("Category name can't be blank"); - return null; + throw new IllegalArgumentException("Category name can't be blank"); } // Normalize to capital letter @@ -45,4 +43,8 @@ private static String normalize(String input) { input = input.substring(0,1).toUpperCase() + input.substring(1); return input; } + + public String getName(){ + return this.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..29b2a4e0 --- /dev/null +++ b/src/main/java/com/example/ElectronicsProduct.java @@ -0,0 +1,55 @@ +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; // kg + + public ElectronicsProduct(UUID uuid, String nameInput, Category categoryInput, BigDecimal priceInput, int warrantyMonths, BigDecimal weightInput) { + super(uuid, nameInput, categoryInput, ensurePositive(priceInput, "Price")); + + this.warrantyMonths = ensurePositiveInt(warrantyMonths); + this.weight = ensurePositive(weightInput, "Weight"); + } + + // Make sure price, weight is positive + private static BigDecimal ensurePositive(BigDecimal value, String valueType) { + // Uses built in constant for 0 + if (value.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException(valueType + " cannot be negative."); + } + return value; + } + + // Make sure warranty is positive + private static int ensurePositiveInt(int value) { + if (value < 0) { + throw new IllegalArgumentException("Warranty months cannot be negative."); + } + return value; + } + + @Override + public String productDetails() { + return "Electronics: " + name() + ", Warranty: " + warrantyMonths + " months"; + } + + @Override + public BigDecimal calculateShippingCost() { + BigDecimal baseShipping = BigDecimal.valueOf(79.0); + BigDecimal heavyShipping = baseShipping.add(BigDecimal.valueOf(49.0)); + BigDecimal maxWeight = BigDecimal.valueOf(5.0); + // See if weight is larger than 0 + if (weight.compareTo(maxWeight) > 0) { + return heavyShipping; + } + return baseShipping; + } + + @Override + public Double weight() { + return Double.parseDouble(String.valueOf(weight)); + } +} diff --git a/src/main/java/com/example/FoodProduct.java b/src/main/java/com/example/FoodProduct.java index 6b5e1f0d..a13c44de 100644 --- a/src/main/java/com/example/FoodProduct.java +++ b/src/main/java/com/example/FoodProduct.java @@ -1,19 +1,20 @@ 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; // kg - public FoodProduct(String nameInput, Category categoryInput, BigDecimal priceInput, LocalDate expirationDateInput, BigDecimal weightInput) { - super(nameInput, categoryInput, ensurePositive(priceInput, "Price")); + public FoodProduct(UUID uuid, String nameInput, Category categoryInput, BigDecimal priceInput, LocalDate expirationDateInput, BigDecimal weightInput) { + super(uuid, nameInput, categoryInput, ensurePositive(priceInput, "Price")); this.expirationDate = expirationDateInput; this.weight = ensurePositive(weightInput, "Weight"); } - // Make sure price is positive + // Make sure price, weight is positive private static BigDecimal ensurePositive(BigDecimal value, String valueType) { // Uses built in constant for 0 if (value.compareTo(BigDecimal.ZERO) < 0) { @@ -24,12 +25,22 @@ private static BigDecimal ensurePositive(BigDecimal value, String valueType) { @Override public String productDetails() { - return "Food: " + name() + ", Expires: " + expirationDate + ", Price: " + price() + ", Weight: " + weight + "kg"; + return "Food: " + name() + ", Expires: " + expirationDate; } @Override public BigDecimal calculateShippingCost() { // Multiply weight with 50 - return weight.multiply(new BigDecimal(50)); + return weight.multiply(BigDecimal.valueOf(50)); + } + + @Override + public Double weight() { + return Double.parseDouble(String.valueOf(weight)); + } + + @Override + public LocalDate expirationDate() { + return expirationDate; } } diff --git a/src/main/java/com/example/Product.java b/src/main/java/com/example/Product.java index 1973328f..02e5bf97 100644 --- a/src/main/java/com/example/Product.java +++ b/src/main/java/com/example/Product.java @@ -9,8 +9,8 @@ public abstract class Product { private final Category category; private BigDecimal price; - protected Product(String name, Category category, BigDecimal price) { - this.id = UUID.randomUUID(); + protected Product(UUID uuid, String name, Category category, BigDecimal price) { + this.id = uuid; this.name = name; this.category = category; this.price = price; @@ -32,7 +32,7 @@ public BigDecimal price() { return price; } - public void price(BigDecimal price) { + public void setPrice(BigDecimal price) { this.price = price; } diff --git a/src/main/java/com/example/Shippable.java b/src/main/java/com/example/Shippable.java index 948efc86..c80c11ce 100644 --- a/src/main/java/com/example/Shippable.java +++ b/src/main/java/com/example/Shippable.java @@ -3,4 +3,5 @@ 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..1944610d --- /dev/null +++ b/src/main/java/com/example/Warehouse.java @@ -0,0 +1,126 @@ +package com.example; +import java.math.BigDecimal; +import java.util.*; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; + +public class Warehouse { + // List to save what products are in the warehouse + private final Set products; + // Set to make sure no duplicates + private final Set changedProducts; + private final String name; + + // Cache to save warehouses(singletons) by key: name + private static final Map instances = new HashMap<>(); + + // Constructor + private Warehouse() { + // HashSet faster add, remove, contains (I've read) and no order needed + this.name = "Default"; + this.products = new HashSet<>(); + this.changedProducts = new HashSet<>(); + } + + // Constructor + private Warehouse(String name) { + this.name = name; + // HashSet faster add, remove, contains (I've read) and no order needed + this.products = new HashSet<>(); + this.changedProducts = new HashSet<>(); + } + + + /****************************************** + *************** METHODS *************** + *******************************************/ + + public static Warehouse getInstance() { + return getInstance("default"); + } + + public static Warehouse getInstance(String name) { + if(!instances.containsKey(name)) { + instances.put(name, new Warehouse(name)); + } + return instances.get(name); + } + + // Get all products + public List getProducts() { + return Collections.unmodifiableList(new ArrayList<>(products)); + } + + // Get single product by id + public Optional getProductById(UUID uuid) { + return products.stream() + // Find matching ID + .filter(item -> item.uuid().equals(uuid)) + // Return Optional + .findFirst(); + } + + public void updateProductPrice(UUID uuid, BigDecimal newPrice) { + Product item = products.stream() + .filter(p -> p.uuid().equals(uuid)) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("Product not found with id: " + uuid)); + + item.setPrice(newPrice); + changedProducts.add(uuid); + } + + public Set getChangedProducts() { + return Collections.unmodifiableSet(changedProducts); + } + + public void addProduct(Product item) { + if (item == null) { + throw new IllegalArgumentException("Product cannot mvn clean install -Ube null."); + } + else if (products.stream().anyMatch(p -> p.uuid().equals(item.uuid()))) { + throw new IllegalArgumentException("Product with that id already exists, use updateProduct for updates."); + } + products.add(item); + } + + public void clearProducts() { + products.clear(); + changedProducts.clear(); + if (instances != null){ + instances.clear(); + } + + } + + public boolean isEmpty() { + return products.isEmpty(); + } + + public List expiredProducts() { + return products.stream() + .filter(item -> item instanceof Perishable) // Find every item implementing Perishable + .map(item -> (Perishable) item) // Turn into Perishable instead of Product + .filter(Perishable::isExpired) // Find every item that has expired + .collect(toList()); // Finally return the list + } + + public List shippableProducts() { + return products.stream() + .filter(item -> item instanceof Shippable) + .map(item -> (Shippable) item) + .collect(toList()); + } + + public void remove(UUID uuid) { + products.removeIf(item -> item.uuid().equals(uuid)); + changedProducts.remove(uuid); + } + + public Map> getProductsGroupedByCategories() { + return products.stream() + .collect(groupingBy(Product::category)); + } +} + diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 1779fc33..2d37dded 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. @@ -60,7 +60,7 @@ public List findProductsExpiringWithinDays(int days) { } 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,11 +136,10 @@ 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. - * Test expectation: with a mostly tight cluster and two extremes, calling with 2.0 returns the two extremes. + * 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) * @return list of products considered outliers @@ -164,7 +163,7 @@ public List findPriceOutliers(double standardDeviations) { } return outliers; } - + /** * 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 @@ -201,7 +200,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. @@ -237,7 +236,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 +260,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: diff --git a/src/test/java/com/example/BasicTest.java b/src/test/java/com/example/BasicTest.java index 8a45877e..fdc81438 100644 --- a/src/test/java/com/example/BasicTest.java +++ b/src/test/java/com/example/BasicTest.java @@ -289,7 +289,7 @@ void should_removeExistingProduct() { void should_throwException_when_addingNullProduct() { assertThatThrownBy(() -> warehouse.addProduct(null)) .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Product cannot be null."); + .hasMessage("Product cannot mvn clean install -Ube null."); } @Test From 44c40c5e98d78725b69c7458c672efacc759254f Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DenSel@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:31:30 +0200 Subject: [PATCH 06/16] EdgeCaseTest clear --- src/main/java/com/example/Main.java | 14 +++ .../java/com/example/WarehouseAnalyzer.java | 92 ++++++++++++++++--- src/test/java/com/example/EdgeCaseTest.java | 2 +- 3 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/example/Main.java diff --git a/src/main/java/com/example/Main.java b/src/main/java/com/example/Main.java new file mode 100644 index 00000000..dbd753cc --- /dev/null +++ b/src/main/java/com/example/Main.java @@ -0,0 +1,14 @@ +package com.example; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +public class Main { + + public static void main(String[] args){ + } +} diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 2d37dded..90fd268b 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -144,26 +144,90 @@ public Map calculateWeightedAveragePriceByCategory() { * @param standardDeviations threshold in standard deviations (e.g., 2.0) * @return list of products considered outliers */ + + public List findPriceOutliers(double standardDeviations) { List products = warehouse.getProducts(); - int n = products.size(); + List sortedByPrice = products.stream() + .map(p -> p.price().doubleValue()) + .sorted() + .toList(); + + // Amount of elements in list + int n = sortedByPrice.size(); + // Return empty list if there's no elements 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); + // Get index of the median (left index if even list size) + int q2Index = (n - 1) / 2; + double q2Value = 0.0; + int q1Index = 0; + double q1Value = 0.0; + int q3Index = 0; + double q3Value = 0.0; + double iqr = 0.0; + + // If even size list + if (n % 2 == 0) { + q1Index = q2Index / 2; + q3Index = (q2Index + n) / 2; + q1Value = calculateMedian(sortedByPrice, q1Index); + q3Value = calculateMedian(sortedByPrice, q3Index); } - return outliers; + // If odd size list + else { + q1Index = (q2Index-1) / 2; + q3Index = (q2Index + n) / 2; + q1Value = calculateMedian(sortedByPrice, q1Index); + q3Value = calculateMedian(sortedByPrice, q3Index); + } + + // Create outer values to find outliers + iqr = q3Value - q1Value; + // Have to make it "effectively final" .. + double lowOutline = q1Value - standardDeviations * iqr; + double highOutline = q3Value + standardDeviations * iqr; + + // Return the outliers + return products.stream() + .filter(p -> p.price().doubleValue() < lowOutline || p.price().doubleValue() > highOutline) + .collect(Collectors.toList()); } + public static double calculateMedian (List sortedList, int startIndex) { + int nextIndex = startIndex+1; + double firstValue = sortedList.get(startIndex); + double secondValue = sortedList.get(nextIndex); + + if (sortedList.size() % 2 == 0) { + return (firstValue+secondValue)/2; + } + else { + return (firstValue); + } + } + + + +// 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; +// List outliers = new ArrayList<>(); +// for (Product p : products) { +// double diff = Math.abs(p.price().doubleValue() - mean); +// if (diff > threshold) outliers.add(p); +// } +// return outliers; +// } + /** * 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 diff --git a/src/test/java/com/example/EdgeCaseTest.java b/src/test/java/com/example/EdgeCaseTest.java index fb4f9358..80e1ef44 100644 --- a/src/test/java/com/example/EdgeCaseTest.java +++ b/src/test/java/com/example/EdgeCaseTest.java @@ -188,7 +188,7 @@ 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 Interquartile Range (IQR). * Arrange: mostly normal-priced items around 15, plus very cheap and very expensive outliers. * Act: analyzer.findPriceOutliers(2.0). * Assert: returns exactly the two outliers ("Expensive" and "Cheap"). From 90f474e0434d0a1885d1b166df0f994737d70313 Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DenSel@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:35:22 +0200 Subject: [PATCH 07/16] EdgeCaseTest clear Push --- src/main/java/com/example/Main.java | 14 -------------- src/main/java/com/example/WarehouseAnalyzer.java | 11 +++++------ 2 files changed, 5 insertions(+), 20 deletions(-) delete mode 100644 src/main/java/com/example/Main.java diff --git a/src/main/java/com/example/Main.java b/src/main/java/com/example/Main.java deleted file mode 100644 index dbd753cc..00000000 --- a/src/main/java/com/example/Main.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example; - -import java.math.BigDecimal; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; -import java.util.stream.Stream; - -public class Main { - - public static void main(String[] args){ - } -} diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 90fd268b..02a17563 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -159,12 +159,11 @@ public List findPriceOutliers(double standardDeviations) { if (n == 0) return List.of(); // Get index of the median (left index if even list size) int q2Index = (n - 1) / 2; - double q2Value = 0.0; - int q1Index = 0; - double q1Value = 0.0; - int q3Index = 0; - double q3Value = 0.0; - double iqr = 0.0; + int q1Index; + double q1Value; + int q3Index; + double q3Value; + double iqr; // If even size list if (n % 2 == 0) { From 8b58a5db095d47d40bd069f591ace264a81a06f3 Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DenSel@users.noreply.github.com> Date: Sun, 26 Oct 2025 00:34:36 +0200 Subject: [PATCH 08/16] EdgeCaseTest clear Push --- 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 1944610d..33de95c1 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -52,7 +52,7 @@ public List getProducts() { return Collections.unmodifiableList(new ArrayList<>(products)); } - // Get single product by id + // Get a single product by id public Optional getProductById(UUID uuid) { return products.stream() // Find matching ID From aea9e3c3f3977ac0704653b5c9ee071b3975d618 Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DenSel@users.noreply.github.com> Date: Sun, 26 Oct 2025 00:47:34 +0200 Subject: [PATCH 09/16] Test --- 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 33de95c1..dddee65b 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -17,7 +17,7 @@ public class Warehouse { // Constructor private Warehouse() { - // HashSet faster add, remove, contains (I've read) and no order needed + // HashSet with faster add, remove, contains (I've read) and no order needed this.name = "Default"; this.products = new HashSet<>(); this.changedProducts = new HashSet<>(); From 35b80bc7cba86d09b32ca57d33338c15ca0208a1 Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DennSel@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:55:11 +0100 Subject: [PATCH 10/16] EdgeCaseTest clear Push --- src/main/java/com/example/Warehouse.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index 33de95c1..ec75eb8f 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -108,9 +108,9 @@ public List expiredProducts() { public List shippableProducts() { return products.stream() - .filter(item -> item instanceof Shippable) - .map(item -> (Shippable) item) - .collect(toList()); + .filter(item -> item instanceof Shippable) // Find every item implementing Shippable + .map(item -> (Shippable) item) // Turn Product into Shippable + .collect(toList()); // Finally return the list } public void remove(UUID uuid) { From 8d84b7599c347ace59a6d7ae59273f14b8fa4390 Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DennSel@users.noreply.github.com> Date: Sun, 26 Oct 2025 21:17:56 +0100 Subject: [PATCH 11/16] EdgeCaseTest clear Push Hoppas jag lagat min 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 3f0cfe4b..7937fbbb 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -110,7 +110,7 @@ public List shippableProducts() { return products.stream() .filter(item -> item instanceof Shippable) // Find every item implementing Shippable .map(item -> (Shippable) item) // Turn Product into Shippable - .collect(toList()); // Finally return the list + .collect(toList()); // Finally returns the list } public void remove(UUID uuid) { From fdd8bc650560be03c6ebcc41022ad50d53045af1 Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DennSel@users.noreply.github.com> Date: Sun, 26 Oct 2025 21:47:00 +0100 Subject: [PATCH 12/16] Updated IQR test --- .../java/com/example/WarehouseAnalyzer.java | 44 ++++++------------- src/test/java/com/example/EdgeCaseTest.java | 16 ++++--- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 02a17563..30d72a6d 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -137,16 +137,21 @@ public Map calculateWeightedAveragePriceByCategory() { return result; } + // Del av text lånad från Kathify /** - * 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. + * Finds products with prices that are unusually high or low + * using the IQR method + * Below Q1 - (q1Value - multiplier * iqr) or above Q3 + (q3Value + multiplier * iqr) + * are considered outliers + * In tests with mostly similar prices and two extreme values, + * a threshold of 2.0 should detect both extremes * - * @param standardDeviations threshold in standard deviations (e.g., 2.0) - * @return list of products considered outliers + * @param multiplier multiplier for IQR (e.g., 1.5 or 2.0) + * @return list of outlier products */ - - public List findPriceOutliers(double standardDeviations) { + // This could definitely be improved, simplified.. + public List findPriceOutliers(double multiplier) { List products = warehouse.getProducts(); List sortedByPrice = products.stream() .map(p -> p.price().doubleValue()) @@ -182,9 +187,9 @@ public List findPriceOutliers(double standardDeviations) { // Create outer values to find outliers iqr = q3Value - q1Value; - // Have to make it "effectively final" .. - double lowOutline = q1Value - standardDeviations * iqr; - double highOutline = q3Value + standardDeviations * iqr; + // Have to make it "effectively final" + double lowOutline = q1Value - multiplier * iqr; + double highOutline = q3Value + multiplier * iqr; // Return the outliers return products.stream() @@ -206,27 +211,6 @@ public static double calculateMedian (List sortedList, int startIndex) { } - -// 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; -// List outliers = new ArrayList<>(); -// for (Product p : products) { -// double diff = Math.abs(p.price().doubleValue() - mean); -// if (diff > threshold) outliers.add(p); -// } -// return outliers; -// } - /** * 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 diff --git a/src/test/java/com/example/EdgeCaseTest.java b/src/test/java/com/example/EdgeCaseTest.java index 80e1ef44..c16afada 100644 --- a/src/test/java/com/example/EdgeCaseTest.java +++ b/src/test/java/com/example/EdgeCaseTest.java @@ -185,14 +185,18 @@ void should_calculateWeightedAveragePrice_byCategory() { .isEqualByComparingTo(new BigDecimal("11.43")); } - @Test - @DisplayName("📊 should identify products with abnormal pricing (outliers)") + // Del av text lånad från Kathify /** - * Detects price outliers using Interquartile Range (IQR). - * Arrange: mostly normal-priced items around 15, plus very cheap and very expensive outliers. - * Act: analyzer.findPriceOutliers(2.0). - * Assert: returns exactly the two outliers ("Expensive" and "Cheap"). + * Finds products with prices that are unusually high or low + * using the IQR method + * Below Q1 - (q1Value - multiplier * iqr) or above Q3 + (q3Value + multiplier * iqr) + * are considered outliers + * In tests with mostly similar prices and two extreme values, + * a threshold of 2.0 should detect both extremes */ + @Test + @DisplayName("📊 should identify products with abnormal pricing (outliers)") + void should_identifyPriceOutliers_usingStatistics() { // Arrange - Most products around 10-20, with outliers IntStream.rangeClosed(1, 10).forEach(i -> From 6bb5521ac9a0ebe3e4acc0bbcee3febd04591f2d Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DennSel@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:14:59 +0100 Subject: [PATCH 13/16] Updated IQR test Update Warehouse and Category --- src/main/java/com/example/Category.java | 1 + src/main/java/com/example/Warehouse.java | 4 ++-- .../java/com/example/WarehouseAnalyzer.java | 18 +++++++++--------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/Category.java b/src/main/java/com/example/Category.java index 3b98dd66..76d21254 100644 --- a/src/main/java/com/example/Category.java +++ b/src/main/java/com/example/Category.java @@ -40,6 +40,7 @@ public static Category of(String nameOf){ // Method to normalize to capital letter private static String normalize(String input) { input = input.trim().toLowerCase(); + if (input.isEmpty()) throw new IllegalArgumentException("Category name must not be blank"); input = input.substring(0,1).toUpperCase() + input.substring(1); return input; } diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index 7937fbbb..dc98117a 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -119,8 +119,8 @@ public void remove(UUID uuid) { } public Map> getProductsGroupedByCategories() { - return products.stream() - .collect(groupingBy(Product::category)); + return Collections.unmodifiableMap(products.stream() + .collect(groupingBy(Product::category))); } } diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 30d72a6d..848d2027 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -159,11 +159,11 @@ public List findPriceOutliers(double multiplier) { .toList(); // Amount of elements in list - int n = sortedByPrice.size(); + int elements = sortedByPrice.size(); // Return empty list if there's no elements - if (n == 0) return List.of(); + if (elements == 0) return List.of(); // Get index of the median (left index if even list size) - int q2Index = (n - 1) / 2; + int q2Index = (elements - 1) / 2; int q1Index; double q1Value; int q3Index; @@ -171,16 +171,16 @@ public List findPriceOutliers(double multiplier) { double iqr; // If even size list - if (n % 2 == 0) { + if (elements % 2 == 0) { q1Index = q2Index / 2; - q3Index = (q2Index + n) / 2; + q3Index = (q2Index + elements) / 2; q1Value = calculateMedian(sortedByPrice, q1Index); q3Value = calculateMedian(sortedByPrice, q3Index); } // If odd size list else { q1Index = (q2Index-1) / 2; - q3Index = (q2Index + n) / 2; + q3Index = (q2Index + elements) / 2; q1Value = calculateMedian(sortedByPrice, q1Index); q3Value = calculateMedian(sortedByPrice, q3Index); } @@ -203,14 +203,14 @@ public static double calculateMedian (List sortedList, int startIndex) { double secondValue = sortedList.get(nextIndex); if (sortedList.size() % 2 == 0) { - return (firstValue+secondValue)/2; + return (firstValue); } else { - return (firstValue); + + return (firstValue+secondValue)/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 From c7b06f70294f3f01ab30660adb387c94c8fc47df Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DennSel@users.noreply.github.com> Date: Mon, 27 Oct 2025 12:17:22 +0100 Subject: [PATCH 14/16] IQR rewritten with interpolating Final version --- .../java/com/example/WarehouseAnalyzer.java | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 848d2027..ec27f5e2 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -162,31 +162,17 @@ public List findPriceOutliers(double multiplier) { int elements = sortedByPrice.size(); // Return empty list if there's no elements if (elements == 0) return List.of(); - // Get index of the median (left index if even list size) - int q2Index = (elements - 1) / 2; - int q1Index; - double q1Value; - int q3Index; - double q3Value; - double iqr; - // If even size list - if (elements % 2 == 0) { - q1Index = q2Index / 2; - q3Index = (q2Index + elements) / 2; - q1Value = calculateMedian(sortedByPrice, q1Index); - q3Value = calculateMedian(sortedByPrice, q3Index); - } - // If odd size list - else { - q1Index = (q2Index-1) / 2; - q3Index = (q2Index + elements) / 2; - q1Value = calculateMedian(sortedByPrice, q1Index); - q3Value = calculateMedian(sortedByPrice, q3Index); - } + // Get indexes + double q1Index = (elements+1)*0.25; + double q3Index = (elements+1)*0.75; + + // Get quartile values + double q1Value = calculateQuartiles(sortedByPrice, q1Index); + double q3Value = calculateQuartiles(sortedByPrice, q3Index); // Create outer values to find outliers - iqr = q3Value - q1Value; + double iqr = q3Value - q1Value; // Have to make it "effectively final" double lowOutline = q1Value - multiplier * iqr; double highOutline = q3Value + multiplier * iqr; @@ -197,17 +183,22 @@ public List findPriceOutliers(double multiplier) { .collect(Collectors.toList()); } - public static double calculateMedian (List sortedList, int startIndex) { - int nextIndex = startIndex+1; - double firstValue = sortedList.get(startIndex); - double secondValue = sortedList.get(nextIndex); + public static double calculateQuartiles (List sortedList, double index) { + // Floor of index + int indexFloor = (int) Math.floor(index); - if (sortedList.size() % 2 == 0) { - return (firstValue); + // If whole number + if(index == indexFloor) { + return sortedList.get(indexFloor); } else { + // Fraction to use in interpolation + double fraction = index - indexFloor; + double lower = sortedList.get(indexFloor); + double upper = sortedList.get(indexFloor+1); - return (firstValue+secondValue)/2; + // return interpolation (had to look it up, understand it but still a bit muddy) + return lower + (fraction * (upper - lower)); } } From 210c3087c0463e4b358af93603f2444ebaa603b2 Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DennSel@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:41:18 +0100 Subject: [PATCH 15/16] Fixed:clearProducts() Breaks Singleton Pattern --- src/main/java/com/example/Shippable.java | 6 +++++- src/main/java/com/example/Warehouse.java | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/Shippable.java b/src/main/java/com/example/Shippable.java index c80c11ce..3d404faa 100644 --- a/src/main/java/com/example/Shippable.java +++ b/src/main/java/com/example/Shippable.java @@ -3,5 +3,9 @@ interface Shippable { BigDecimal calculateShippingCost(); - Double weight(); + + // Return 0.0 if no weight override + default Double weight(){ + return 0.0; + } } diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index dc98117a..524c045d 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -88,10 +88,6 @@ else if (products.stream().anyMatch(p -> p.uuid().equals(item.uuid()))) { public void clearProducts() { products.clear(); changedProducts.clear(); - if (instances != null){ - instances.clear(); - } - } public boolean isEmpty() { From f447992d127dc00703c449bed098e6153e50c18c Mon Sep 17 00:00:00 2001 From: Dennis Selden <111012436+DennSel@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:54:35 +0100 Subject: [PATCH 16/16] Might have fixed this AI feedback: clearProducts() Breaks Singleton Pattern Shipping Group Optimization Handling Already implemented that AI wants me to implement: Weighted Average Category Handling --- src/test/java/com/example/EdgeCaseTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/example/EdgeCaseTest.java b/src/test/java/com/example/EdgeCaseTest.java index c16afada..e4924b1d 100644 --- a/src/test/java/com/example/EdgeCaseTest.java +++ b/src/test/java/com/example/EdgeCaseTest.java @@ -167,7 +167,7 @@ class AdvancedAnalyticsTests { * Assert: Dairy category has weighted average 11.43. */ void should_calculateWeightedAveragePrice_byCategory() { - // Arrange - Products with different weights in same category + // Arrange - Products with different weights in the same category Category dairy = Category.of("Dairy"); warehouse.addProduct(new FoodProduct(UUID.randomUUID(), "Milk", dairy, new BigDecimal("10.00"), LocalDate.now().plusDays(5), new BigDecimal("2.0"))); // Weight: 2kg