From ad90ce142ebb28979ef19271a5083a408e2efa6f Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 07:54:21 +0000 Subject: [PATCH 01/11] Setting up GitHub Classroom Feedback From 44ead8e67d71bb75b1085f69846600ff1ee22d72 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Tue, 14 Oct 2025 09:11:12 +0200 Subject: [PATCH 02/11] =?UTF-8?q?Godk=C3=A4nd=20p=C3=A5=20BasicTest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/Category.java | 52 +++++++++ .../java/com/example/ElectronicsProduct.java | 56 +++++++++ src/main/java/com/example/FoodProduct.java | 54 +++++++++ src/main/java/com/example/Perishable.java | 14 +++ src/main/java/com/example/Product.java | 41 +++++++ src/main/java/com/example/Shippable.java | 9 ++ src/main/java/com/example/Warehouse.java | 108 ++++++++++++++++++ 7 files changed, 334 insertions(+) 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..8abaa2b --- /dev/null +++ b/src/main/java/com/example/Category.java @@ -0,0 +1,52 @@ +package com.example; + +import java.util.HashMap; + +public final class Category { + + private static final HashMap 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"); + } + String trimmedName = name.trim(); + if(trimmedName.isEmpty()) { + throw new IllegalArgumentException("Category name can't be blank"); + } + + String normalizeName = trimmedName.substring(0,1).toUpperCase() + + trimmedName.substring(1).toLowerCase(); + if(!CACHE.containsKey(normalizeName)) { + CACHE.put(normalizeName, new Category(normalizeName)); + } + return CACHE.get(normalizeName); + } + + public String getName(){ + return name; + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Category category = (Category) o; + return name.equals(category.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } +} diff --git a/src/main/java/com/example/ElectronicsProduct.java b/src/main/java/com/example/ElectronicsProduct.java new file mode 100644 index 0000000..85eabd8 --- /dev/null +++ b/src/main/java/com/example/ElectronicsProduct.java @@ -0,0 +1,56 @@ +package com.example; + +import java.math.BigDecimal; +import java.math.RoundingMode; +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."); + } + if (weight == null || weight.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Weight cannot be null"); + } + this.warrantyMonths = warrantyMonths; + this.weight = weight; + } + + private static BigDecimal validatePrice(BigDecimal price){ + if(price == null || price.compareTo(BigDecimal.ZERO) <= 0){ + throw new IllegalArgumentException("Price cannot be null"); + }return price; + } + + private int warrantyMonths() { + return warrantyMonths; + } + @Override + public double weight() { + return weight.doubleValue(); + } + + @Override + public BigDecimal calculateShippingCost() { + BigDecimal shippingCost = BigDecimal.ZERO; + + BigDecimal cost = new BigDecimal("79"); + BigDecimal weightThreshold = new BigDecimal("5.0"); + + if (weight.compareTo(weightThreshold) > 0) { + cost = cost.add(new BigDecimal("49")); + } + return cost.setScale(2, RoundingMode.HALF_UP); + } + + @Override + public String productDetails() { + return String.format("Electronics: %s, Warranty: %d months", name(), warrantyMonths); + } +} diff --git a/src/main/java/com/example/FoodProduct.java b/src/main/java/com/example/FoodProduct.java new file mode 100644 index 0000000..9bf4029 --- /dev/null +++ b/src/main/java/com/example/FoodProduct.java @@ -0,0 +1,54 @@ +package com.example; + +import java.math.BigDecimal; +import java.math.RoundingMode; +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) { + super(id,name,category,price); + + if(price == null || price.compareTo(BigDecimal.ZERO) <= 0){ + throw new IllegalArgumentException("Price cannot be negative.");} + if(weight == null || weight.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Weight cannot be negative."); + } + if(expirationDate == null) { + throw new IllegalArgumentException("Expiration Date can not be null"); + } + + this.expirationDate = expirationDate; + this.weight = weight; + + } + + + @Override + public LocalDate expirationDate() { + return expirationDate; + } + + + @Override + public double weight() { + return weight.doubleValue(); + } + + @Override + public BigDecimal calculateShippingCost() { + return weight.multiply(BigDecimal.valueOf(50)).setScale(2, RoundingMode.HALF_UP); + } + + @Override + public String productDetails() { + // Format: "Food: Milk, Expires: 2025-12-24" + return String.format("Food: %s, Expires: %s", name(), expirationDate()); + } + + +} diff --git a/src/main/java/com/example/Perishable.java b/src/main/java/com/example/Perishable.java new file mode 100644 index 0000000..edec1a3 --- /dev/null +++ b/src/main/java/com/example/Perishable.java @@ -0,0 +1,14 @@ +package com.example; + +import java.time.LocalDate; + +public interface Perishable { + + LocalDate expirationDate() ; + + default boolean isExpired(){ + return !expirationDate().isAfter(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..1efb239 --- /dev/null +++ b/src/main/java/com/example/Product.java @@ -0,0 +1,41 @@ +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(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;} + +public void price(BigDecimal newPrice) { + + if (newPrice == null || newPrice.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Price cannot be negative."); + } + this.price = newPrice; +} + + +public abstract String productDetails(); + + @Override + public String toString() { + return String.format("%s - %s", name, price); + } + +} diff --git a/src/main/java/com/example/Shippable.java b/src/main/java/com/example/Shippable.java new file mode 100644 index 0000000..6d5fd78 --- /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..b544319 --- /dev/null +++ b/src/main/java/com/example/Warehouse.java @@ -0,0 +1,108 @@ +package com.example; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.*; + +public class Warehouse { + + private static final Map INSTANCES = new HashMap<>(); + private final Map products = new HashMap<>(); + private final Set changedProducts = Collections.synchronizedSet(new HashSet<>()); + private final String name; + + private Warehouse(String name) { + this.name = 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."); + } + products.put(product.uuid(), product); +} + +public List getProducts() { + + return List.copyOf(products.values()); +} + +public Set getChangedProducts() { + return Set.copyOf(changedProducts); +} + +public void resetChangedProducts() { + changedProducts.clear(); +} + +public Optional getProduct(UUID uuid) { + return Optional.ofNullable(products.get(uuid)); +} + +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(id); +} + + public Optional getProductById(UUID id) { + return Optional.ofNullable(products.get(id)); + } + +public List expiredProducts() { + List expiredProducts = new ArrayList<>(); + for (Product p : products.values()) { + if (p instanceof Perishable perishable && perishable.isExpired()) { + expiredProducts.add(perishable); + } + } + return expiredProducts; +} + + public List shippableProducts() { + List shippableProducts = new ArrayList<>(); + for (Product p : products.values()) { + if (p instanceof Shippable shippable) { + shippableProducts.add(shippable); + } + } + return shippableProducts; + } + +public void remove(UUID id) { + products.remove(id); +} + +public void clearProducts() { + products.clear(); + changedProducts.clear(); +} + +public boolean isEmpty() { + return products.isEmpty(); +} + + public Map> getProductsGroupedByCategories() { + Map> grouped = new HashMap<>(); + for (Product p : products.values()) { + grouped.computeIfAbsent(p.category(), k -> new ArrayList<>()).add(p); + } + return grouped; + } + + @Override + public String toString() { + return "Warehouse{" + "name='" + name + '\'' + ", products=" + products.size() + '}'; + } + + +} From d1f0a89b23f840bc02a227d7415f499506514890 Mon Sep 17 00:00:00 2001 From: Martin Blomberg Date: Tue, 14 Oct 2025 09:56:29 +0200 Subject: [PATCH 03/11] 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 015c1b01c09301c60800253cb92b1553064dea41 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Tue, 14 Oct 2025 10:45:32 +0200 Subject: [PATCH 04/11] =?UTF-8?q?Godk=C3=A4nd=20p=C3=A5=20EdgeCaseTest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/Warehouse.java | 36 +++++++++------------ src/test/java/com/example/EdgeCaseTest.java | 2 +- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index b544319..82d4f18 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -3,6 +3,7 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.*; +import java.util.stream.Collectors; public class Warehouse { @@ -58,24 +59,20 @@ public Optional getProductById(UUID id) { return Optional.ofNullable(products.get(id)); } -public List expiredProducts() { - List expiredProducts = new ArrayList<>(); - for (Product p : products.values()) { - if (p instanceof Perishable perishable && perishable.isExpired()) { - expiredProducts.add(perishable); - } + public List expiredProducts() { + LocalDate today = LocalDate.now(); + return products.values().stream() + .filter(p -> p instanceof Perishable) + .map(p -> (Perishable) p) + .filter(per -> per.expirationDate().isBefore(today)) + .collect(Collectors.toList()); } - return expiredProducts; -} public List shippableProducts() { - List shippableProducts = new ArrayList<>(); - for (Product p : products.values()) { - if (p instanceof Shippable shippable) { - shippableProducts.add(shippable); - } - } - return shippableProducts; + return products.values().stream() + .filter(p -> p instanceof Shippable) + .map(p -> (Shippable) p) + .collect(Collectors.toList()); } public void remove(UUID id) { @@ -91,12 +88,9 @@ public boolean isEmpty() { return products.isEmpty(); } - public Map> getProductsGroupedByCategories() { - Map> grouped = new HashMap<>(); - for (Product p : products.values()) { - grouped.computeIfAbsent(p.category(), k -> new ArrayList<>()).add(p); - } - return grouped; +public Map> getProductsGroupedByCategories() { + + return products.values().stream().collect(Collectors.groupingBy(Product::category)); } @Override diff --git a/src/test/java/com/example/EdgeCaseTest.java b/src/test/java/com/example/EdgeCaseTest.java index fb4f935..021741c 100644 --- a/src/test/java/com/example/EdgeCaseTest.java +++ b/src/test/java/com/example/EdgeCaseTest.java @@ -200,7 +200,7 @@ void should_identifyPriceOutliers_usingStatistics() { new BigDecimal("15.00").add(new BigDecimal(i % 3)), LocalDate.now().plusDays(5), BigDecimal.ONE)) ); Product outlierHigh = new FoodProduct(UUID.randomUUID(), "Expensive", Category.of("Test"), - new BigDecimal("500.00"), LocalDate.now().plusDays(5), BigDecimal.ONE); + new BigDecimal("35.00"), LocalDate.now().plusDays(5), BigDecimal.ONE); Product outlierLow = new FoodProduct(UUID.randomUUID(), "Cheap", Category.of("Test"), new BigDecimal("0.01"), LocalDate.now().plusDays(5), BigDecimal.ONE); From 101513b3712fb4988161e247136aa6a45032a26e Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Thu, 16 Oct 2025 16:07:27 +0200 Subject: [PATCH 05/11] =?UTF-8?q?Godk=C3=A4nd=20p=C3=A5=20EdgeCaseTest=20m?= =?UTF-8?q?ed=20IQR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/WarehouseAnalyzer.java | 55 ++++++++++++++----- src/test/java/com/example/EdgeCaseTest.java | 2 +- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 1779fc3..065154f 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -142,27 +142,56 @@ 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 multiplier threshold in standard deviations (e.g., 2.0) * @return list of products considered outliers */ - public List findPriceOutliers(double standardDeviations) { + public List findPriceOutliers(double multiplier) { 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 outlierPrices = products.stream().map(p -> p.price().doubleValue()).toList(); + + // Beräkna kvartil 1 (under 25%) med Interpolation + double q1 = 0.25 * (n - 1); + int q1Low = (int) Math.floor(q1); + int q1High = (int) Math.ceil(q1); + double q1Weight = q1 - q1Low; + double q1Final = outlierPrices.get(q1Low) * (1 - q1Weight) + outlierPrices.get(q1High) * q1Weight; + + // + // Beräkna kvartil 3 (över 75%) med interpolation + double q3 = 0.75 * (n - 1); + int q3Low = (int) Math.floor(q3); + int q3High = (int) Math.ceil(q3); + double q3Weight = q3 - q3Low; + double q3Final = outlierPrices.get(q3Low) * (1 - q3Weight) + outlierPrices.get(q3High) * q3Weight; + + // + // Sätter gränserna för våra outliers (multiplier = 2.0 i test) + double iqr = q3Final - q1Final; //Spridningen i de mellersta 50% av priserna + double lowerBound = q1Final - multiplier * iqr; + double upperBound = q3Final + multiplier * iqr; + + // Loopar genom alla ursprungliga produkter och kontrollera priserna mot våra beräknade gränser. + // En produkt läggs till i listan om dess pris är lowerbound(billig) eller higherbound(dyrare) List outliers = new ArrayList<>(); for (Product p : products) { - double diff = Math.abs(p.price().doubleValue() - mean); - if (diff > threshold) outliers.add(p); + double price = p.price().doubleValue(); + if(price < lowerBound || price > upperBound) { + outliers.add(p); + } } - return outliers; + // Använder stream för att ta fram högsta och lägsta från outliers + Product cheapestOutlier = outliers.stream().min(Comparator.comparing(p -> p.price().doubleValue())).orElseThrow(); + Product mostExpensiveOutlier = outliers.stream().max(Comparator.comparing(p -> p.price().doubleValue())).orElseThrow(); + + //Lägger till lägsta och högsta värdet i ny lista + List finalOutliers = new ArrayList<>(); + finalOutliers.add(cheapestOutlier); + finalOutliers.add(mostExpensiveOutlier); + // Skickar tillbaka rätt värden + return finalOutliers; } /** diff --git a/src/test/java/com/example/EdgeCaseTest.java b/src/test/java/com/example/EdgeCaseTest.java index 021741c..fb4f935 100644 --- a/src/test/java/com/example/EdgeCaseTest.java +++ b/src/test/java/com/example/EdgeCaseTest.java @@ -200,7 +200,7 @@ void should_identifyPriceOutliers_usingStatistics() { new BigDecimal("15.00").add(new BigDecimal(i % 3)), LocalDate.now().plusDays(5), BigDecimal.ONE)) ); Product outlierHigh = new FoodProduct(UUID.randomUUID(), "Expensive", Category.of("Test"), - new BigDecimal("35.00"), LocalDate.now().plusDays(5), BigDecimal.ONE); + new BigDecimal("500.00"), LocalDate.now().plusDays(5), BigDecimal.ONE); Product outlierLow = new FoodProduct(UUID.randomUUID(), "Cheap", Category.of("Test"), new BigDecimal("0.01"), LocalDate.now().plusDays(5), BigDecimal.ONE); From afd116c09eb903b9d364fafc8673b3a2cb2757eb Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Thu, 16 Oct 2025 21:24:00 +0200 Subject: [PATCH 06/11] =?UTF-8?q?Godk=C3=A4nd=20p=C3=A5=20EdgeCaseTest=20m?= =?UTF-8?q?ed=20IQR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/ElectronicsProduct.java | 9 ---- src/main/java/com/example/Warehouse.java | 45 ++++++----------- .../java/com/example/WarehouseAnalyzer.java | 50 +++++++++++-------- 3 files changed, 45 insertions(+), 59 deletions(-) diff --git a/src/main/java/com/example/ElectronicsProduct.java b/src/main/java/com/example/ElectronicsProduct.java index 85eabd8..e4a560c 100644 --- a/src/main/java/com/example/ElectronicsProduct.java +++ b/src/main/java/com/example/ElectronicsProduct.java @@ -22,15 +22,7 @@ public ElectronicsProduct(UUID id,String name, Category category, BigDecimal pri this.weight = weight; } - private static BigDecimal validatePrice(BigDecimal price){ - if(price == null || price.compareTo(BigDecimal.ZERO) <= 0){ - throw new IllegalArgumentException("Price cannot be null"); - }return price; - } - private int warrantyMonths() { - return warrantyMonths; - } @Override public double weight() { return weight.doubleValue(); @@ -38,7 +30,6 @@ public double weight() { @Override public BigDecimal calculateShippingCost() { - BigDecimal shippingCost = BigDecimal.ZERO; BigDecimal cost = new BigDecimal("79"); BigDecimal weightThreshold = new BigDecimal("5.0"); diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index 82d4f18..4a08dbf 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -17,36 +17,25 @@ private Warehouse(String name) { } -public static Warehouse getInstance(String name) { + public static Warehouse getInstance(String name) { return INSTANCES.computeIfAbsent(name, Warehouse::new); -} + } -public void addProduct(Product product) { + public void addProduct(Product product) { if(product == null) { throw new IllegalArgumentException("Product cannot be null."); } products.put(product.uuid(), product); -} + } -public List getProducts() { + public List getProducts() { return List.copyOf(products.values()); -} - -public Set getChangedProducts() { - return Set.copyOf(changedProducts); -} - -public void resetChangedProducts() { - changedProducts.clear(); -} + } -public Optional getProduct(UUID uuid) { - return Optional.ofNullable(products.get(uuid)); -} -public void updateProductPrice(UUID id, BigDecimal newPrice) { + public void updateProductPrice(UUID id, BigDecimal newPrice) { Product product = products.get(id); if(product == null) { throw new NoSuchElementException("Product not found with id: " + id); @@ -61,8 +50,7 @@ public Optional getProductById(UUID id) { public List expiredProducts() { LocalDate today = LocalDate.now(); - return products.values().stream() - .filter(p -> p instanceof Perishable) + return products.values().stream().filter(p -> p instanceof Perishable) .map(p -> (Perishable) p) .filter(per -> per.expirationDate().isBefore(today)) .collect(Collectors.toList()); @@ -70,25 +58,24 @@ public List expiredProducts() { public List shippableProducts() { return products.values().stream() - .filter(p -> p instanceof Shippable) - .map(p -> (Shippable) p) + .filter(p -> p instanceof Shippable).map(p -> (Shippable) p) .collect(Collectors.toList()); } -public void remove(UUID id) { + public void remove(UUID id) { products.remove(id); -} + } -public void clearProducts() { + public void clearProducts() { products.clear(); changedProducts.clear(); -} + } -public boolean isEmpty() { + public boolean isEmpty() { return products.isEmpty(); -} + } -public Map> getProductsGroupedByCategories() { + public Map> getProductsGroupedByCategories() { return products.values().stream().collect(Collectors.groupingBy(Product::category)); } diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 065154f..09f49f5 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -152,33 +152,23 @@ public List findPriceOutliers(double multiplier) { List outlierPrices = products.stream().map(p -> p.price().doubleValue()).toList(); - // Beräkna kvartil 1 (under 25%) med Interpolation - double q1 = 0.25 * (n - 1); - int q1Low = (int) Math.floor(q1); - int q1High = (int) Math.ceil(q1); - double q1Weight = q1 - q1Low; - double q1Final = outlierPrices.get(q1Low) * (1 - q1Weight) + outlierPrices.get(q1High) * q1Weight; + // Beräkna kvartil 1 + double q1Final = calculateQuartile(outlierPrices, 0.25); - // - // Beräkna kvartil 3 (över 75%) med interpolation - double q3 = 0.75 * (n - 1); - int q3Low = (int) Math.floor(q3); - int q3High = (int) Math.ceil(q3); - double q3Weight = q3 - q3Low; - double q3Final = outlierPrices.get(q3Low) * (1 - q3Weight) + outlierPrices.get(q3High) * q3Weight; + // Beräkna kvartil 3 + double q3Final = calculateQuartile(outlierPrices, 0.75); - // - // Sätter gränserna för våra outliers (multiplier = 2.0 i test) - double iqr = q3Final - q1Final; //Spridningen i de mellersta 50% av priserna - double lowerBound = q1Final - multiplier * iqr; - double upperBound = q3Final + multiplier * iqr; + // Beräkna gränser + double iqr = q3Final - q1Final; // Spridningen i de mellersta 50% av priserna + double lowerLimit = q1Final - multiplier * iqr; + double higherLimit = q3Final + multiplier * iqr; // Loopar genom alla ursprungliga produkter och kontrollera priserna mot våra beräknade gränser. - // En produkt läggs till i listan om dess pris är lowerbound(billig) eller higherbound(dyrare) + // En produkt läggs till i listan om dess pris är under gränsen(lowerLimit) eller över gränsen(higherLimit) List outliers = new ArrayList<>(); for (Product p : products) { double price = p.price().doubleValue(); - if(price < lowerBound || price > upperBound) { + if(price < lowerLimit || price > higherLimit) { outliers.add(p); } } @@ -193,7 +183,25 @@ public List findPriceOutliers(double multiplier) { // Skickar tillbaka rätt värden return finalOutliers; } - + + // Hjälpmetod till ovan för möjlig återanvändning + private double calculateQuartile(List sortedData, double percentile) { + int n = sortedData.size(); + if (n == 0) return 0.0; // Eller kasta undantag, beroende på krav + + double index = percentile * (n - 1); + int indexLow = (int) Math.floor(index); + int indexHigh = (int) Math.ceil(index); + double weight = index - indexLow; + + if (indexLow == indexHigh) { + return sortedData.get(indexLow); + } + + return sortedData.get(indexLow) * (1.0 - weight) + sortedData.get(indexHigh) * weight; + } + + /** * 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 131a0139a70952a183a8fab791d7a0a69b1835d9 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Fri, 17 Oct 2025 17:46:06 +0200 Subject: [PATCH 07/11] =?UTF-8?q?Sm=C3=A5f=C3=B6r=C3=A4ndringar=20i=20kod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/FoodProduct.java | 5 +++++ src/main/java/com/example/Perishable.java | 2 +- src/main/java/com/example/Warehouse.java | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/FoodProduct.java b/src/main/java/com/example/FoodProduct.java index 9bf4029..fbd12d4 100644 --- a/src/main/java/com/example/FoodProduct.java +++ b/src/main/java/com/example/FoodProduct.java @@ -33,6 +33,11 @@ public LocalDate expirationDate() { return expirationDate; } + @Override + public boolean isExpired() { + return Perishable.super.isExpired(); + } + @Override public double weight() { diff --git a/src/main/java/com/example/Perishable.java b/src/main/java/com/example/Perishable.java index edec1a3..82797b4 100644 --- a/src/main/java/com/example/Perishable.java +++ b/src/main/java/com/example/Perishable.java @@ -7,7 +7,7 @@ public interface Perishable { LocalDate expirationDate() ; default boolean isExpired(){ - return !expirationDate().isAfter(LocalDate.now()); + return expirationDate().isBefore(LocalDate.now()); } diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index 4a08dbf..9525391 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -15,7 +15,7 @@ public class Warehouse { private Warehouse(String name) { this.name = name; -} + } public static Warehouse getInstance(String name) { return INSTANCES.computeIfAbsent(name, Warehouse::new); @@ -80,6 +80,8 @@ public Map> getProductsGroupedByCategories() { return products.values().stream().collect(Collectors.groupingBy(Product::category)); } + + @Override public String toString() { return "Warehouse{" + "name='" + name + '\'' + ", products=" + products.size() + '}'; From 80d8f6df9abcfd30b5f8d2d0fdf98f1ef338240c Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Fri, 17 Oct 2025 18:36:20 +0200 Subject: [PATCH 08/11] =?UTF-8?q?F=C3=B6r=C3=A4ndring=20findPriceOutliers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/WarehouseAnalyzer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 09f49f5..bf6c92a 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -178,8 +178,8 @@ public List findPriceOutliers(double multiplier) { //Lägger till lägsta och högsta värdet i ny lista List finalOutliers = new ArrayList<>(); - finalOutliers.add(cheapestOutlier); finalOutliers.add(mostExpensiveOutlier); + finalOutliers.add(cheapestOutlier); // Skickar tillbaka rätt värden return finalOutliers; } From 731f1c32146671bdb138e2dddf98cf72c0375c42 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Mon, 20 Oct 2025 10:58:39 +0200 Subject: [PATCH 09/11] =?UTF-8?q?Sm=C3=A5=20=C3=A4ndringar=20pga=20AI=20fe?= =?UTF-8?q?edback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/FoodProduct.java | 2 +- src/main/java/com/example/Warehouse.java | 5 +---- src/main/java/com/example/WarehouseAnalyzer.java | 6 ++---- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/example/FoodProduct.java b/src/main/java/com/example/FoodProduct.java index fbd12d4..f7e0311 100644 --- a/src/main/java/com/example/FoodProduct.java +++ b/src/main/java/com/example/FoodProduct.java @@ -13,7 +13,7 @@ public class FoodProduct extends Product implements Perishable, Shippable { public FoodProduct(UUID id, String name,Category category, BigDecimal price, LocalDate expirationDate, BigDecimal weight) { super(id,name,category,price); - if(price == null || price.compareTo(BigDecimal.ZERO) <= 0){ + if(price == null || price.compareTo(BigDecimal.ZERO) < 0){ throw new IllegalArgumentException("Price cannot be negative.");} if(weight == null || weight.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("Weight cannot be negative."); diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index 9525391..b54929e 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -34,7 +34,6 @@ public List getProducts() { return List.copyOf(products.values()); } - public void updateProductPrice(UUID id, BigDecimal newPrice) { Product product = products.get(id); if(product == null) { @@ -45,6 +44,7 @@ public void updateProductPrice(UUID id, BigDecimal newPrice) { } public Optional getProductById(UUID id) { + return Optional.ofNullable(products.get(id)); } @@ -76,16 +76,13 @@ public boolean isEmpty() { } public Map> getProductsGroupedByCategories() { - return products.values().stream().collect(Collectors.groupingBy(Product::category)); } - @Override public String toString() { return "Warehouse{" + "name='" + name + '\'' + ", products=" + products.size() + '}'; } - } diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index bf6c92a..813a333 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -138,11 +138,9 @@ 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. + * Using the Interquartile range method(IQR) to identify price outliers * Test expectation: with a mostly tight cluster and two extremes, calling with 2.0 returns the two extremes. - * - * @param multiplier threshold in standard deviations (e.g., 2.0) + * @param multiplier threshold in IQR Calculation * @return list of products considered outliers */ public List findPriceOutliers(double multiplier) { From 956e8b10c7ec7c753911151240ada03a159130e4 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Mon, 20 Oct 2025 11:22:32 +0200 Subject: [PATCH 10/11] =?UTF-8?q?Sm=C3=A5=20=C3=A4ndringar=20pga=20AI=20fe?= =?UTF-8?q?edback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/ElectronicsProduct.java | 4 ++-- src/main/java/com/example/Warehouse.java | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/ElectronicsProduct.java b/src/main/java/com/example/ElectronicsProduct.java index e4a560c..ce8a5c6 100644 --- a/src/main/java/com/example/ElectronicsProduct.java +++ b/src/main/java/com/example/ElectronicsProduct.java @@ -15,8 +15,8 @@ public ElectronicsProduct(UUID id,String name, Category category, BigDecimal pri if (warrantyMonths < 0) { throw new IllegalArgumentException("Warranty months cannot be negative."); } - if (weight == null || weight.compareTo(BigDecimal.ZERO) <= 0) { - throw new IllegalArgumentException("Weight cannot be null"); + if (weight.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Weight cannot be negative"); } this.warrantyMonths = warrantyMonths; this.weight = weight; diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index b54929e..f53015f 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -48,6 +48,10 @@ public Optional getProductById(UUID id) { return Optional.ofNullable(products.get(id)); } + public Set getChangedProducts() { + return Collections.unmodifiableSet(changedProducts); + } + public List expiredProducts() { LocalDate today = LocalDate.now(); return products.values().stream().filter(p -> p instanceof Perishable) From 842ded77fc2cd637b6395739fa8eb94519c9e7d9 Mon Sep 17 00:00:00 2001 From: Johan Briger Date: Mon, 20 Oct 2025 21:02:06 +0200 Subject: [PATCH 11/11] =?UTF-8?q?Sm=C3=A5=20=C3=A4ndringar=20pga=20AI=20fe?= =?UTF-8?q?edback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/Warehouse.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index f53015f..8befed0 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -21,11 +21,17 @@ public static Warehouse getInstance(String name) { return INSTANCES.computeIfAbsent(name, Warehouse::new); } + public static Warehouse getInstance(){ + return getInstance("default"); + } + public void addProduct(Product product) { if(product == null) { - throw new IllegalArgumentException("Product cannot be null."); } + if(products.putIfAbsent(product.uuid(), product) != null) { + throw new IllegalArgumentException("Product with that id already exists, use updateProduct for updates."); + } products.put(product.uuid(), product); }