From 67798da33bfed748ad2ff6f2ab986e9c100d56c9 Mon Sep 17 00:00:00 2001 From: "github-classroom[bot]" <66690702+github-classroom[bot]@users.noreply.github.com> Date: Sun, 12 Oct 2025 12:32:05 +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 a11fc976..8a45877e 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 8cebd25ab2676ba4b7b8331409e2f9db1e66aa88 Mon Sep 17 00:00:00 2001 From: Marko Cavric Date: Mon, 20 Oct 2025 15:04:55 +0200 Subject: [PATCH 03/10] =?UTF-8?q?BasicTest=20Godk=C3=A4nt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/Category.java | 60 ++++++++ .../java/com/example/ElectronicsProduct.java | 45 ++++++ src/main/java/com/example/FoodProduct.java | 62 ++++++++ src/main/java/com/example/Perishable.java | 7 + src/main/java/com/example/Product.java | 48 +++++++ src/main/java/com/example/Shippable.java | 10 ++ src/main/java/com/example/Warehouse.java | 130 +++++++++++++++++ .../java/com/example/WarehouseAnalyzer.java | 2 +- src/test/java/com/example/BasicTest.java | 134 +----------------- 9 files changed, 366 insertions(+), 132 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 00000000..3a43cfb9 --- /dev/null +++ b/src/main/java/com/example/Category.java @@ -0,0 +1,60 @@ +package com.example; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public final class Category { + private static final Map CACHE = new ConcurrentHashMap<>(); + private final String name; + + + //private Constructor + private Category(String name){ + this.name = name; + } + + //public static factory category.Of(String name) + public static Category of(String name) { + //validate input - cannot be null + if (name == null) { + throw new IllegalArgumentException("Category name can't be null"); + } + if (name.isBlank()) { + throw new IllegalArgumentException("Category name can't be blank"); + } + //normalize name with inital capital letter fruit -> Fruit + String normalize = name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase(); + + return CACHE.computeIfAbsent(normalize, Category::new); + } + + public String name(){return name;} + + public String getName(){return name;} + + @Override + public String toString(){return name;} + + @Override + public boolean equals(Object o){ + if (this == o) return true; + if (!(o instanceof Category)) return false; + Category category = (Category) o; + return name.equals(category.name); + } + + @Override + public int hashCode() {return name.hashCode();} + + +} +/* +- Category (value object) + - Use a private constructor and a public static factory Category.of(String name). + - Validate input: null => "Category name can't be null"; empty/blank => "Category name can't be blank". + - Normalize name with initial capital letter (e.g., "fruit" -> "Fruit"). + - Cache/flyweight: return the same instance for the same normalized 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..ffedf9c4 --- /dev/null +++ b/src/main/java/com/example/ElectronicsProduct.java @@ -0,0 +1,45 @@ +package com.example; + +import java.math.BigDecimal; +import java.util.UUID; + +public class ElectronicsProduct extends Product implements Shippable{ + + private final int warrantyMonths; + private final BigDecimal weight; + +// - Fields: int warrantyMonths, BigDecimal weight (kg). + + public ElectronicsProduct(UUID id, String name, Category category, + BigDecimal price, int warrantyMonths, BigDecimal weight) { + super(id, name, category, price); + + if (warrantyMonths < 0) { + throw new IllegalArgumentException("Warranty months cannot be negative.");} + + this.warrantyMonths = warrantyMonths; + this.weight = weight; + } + + @Override + public String productDetails() { + //- productDetails() should look like: "Electronics: Laptop, Warranty: 24 months". + return "Electronics: " + name() + ", Warranty: " + warrantyMonths + " months"; + } + + @Override + public Double weight() { + return weight.doubleValue(); + } + + @Override + public BigDecimal calculateShippingCost() { + //- Shipping rule: base 79, add 49 if weight > 5.0 kg. + BigDecimal shippingCost = BigDecimal.valueOf(79); + if (weight.compareTo(BigDecimal.valueOf(5.0)) > 0) { + shippingCost = shippingCost.add(BigDecimal.valueOf(49)); + } + return shippingCost; + } +} + diff --git a/src/main/java/com/example/FoodProduct.java b/src/main/java/com/example/FoodProduct.java new file mode 100644 index 00000000..8389d5a5 --- /dev/null +++ b/src/main/java/com/example/FoodProduct.java @@ -0,0 +1,62 @@ +package com.example; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.UUID; + +public class FoodProduct extends Product implements Perishable, Shippable { + + /* + - FoodProduct (extends Product) + - Implements Perishable and Shippable. + - Fields: LocalDate expirationDate, BigDecimal weight (kg). + - Validations: negative price -> IllegalArgumentException("Price cannot be negative."); negative weight -> + IllegalArgumentException("Weight cannot be negative."). + - productDetails() should look like: "Food: Milk, Expires: 2025-12-24". + - Shipping rule: cost = weight * 50. + */ + private final LocalDate expirationDate; + //private final Double weight; + 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.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Price cannot be negative."); + } + //weight villkor + if (weight.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Weight cannot be negative."); + } + + this.expirationDate = expirationDate; + this.weight = weight; + } + + + //implementerar productDetails + @Override + public String productDetails() { + return "Food: " + name() + ", Expires: " + expirationDate(); + }; + + // ----- Perishable ----- + @Override + public LocalDate expirationDate() {return expirationDate;} + + // ----- Shippable ----- + @Override + public Double weight(){ + return weight.doubleValue();} + + @Override + public BigDecimal calculateShippingCost(){ + //Shipping rule: cost = weight * 50. + //return weight().multiply(BigDecimal.valueOf(50)); + return BigDecimal.valueOf(weight.doubleValue() * 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..bb4699ca --- /dev/null +++ b/src/main/java/com/example/Perishable.java @@ -0,0 +1,7 @@ +package com.example; +import java.time.LocalDate; + +public interface Perishable{ + //varor som kan utgå + LocalDate expirationDate(); +} diff --git a/src/main/java/com/example/Product.java b/src/main/java/com/example/Product.java new file mode 100644 index 00000000..9e57f398 --- /dev/null +++ b/src/main/java/com/example/Product.java @@ -0,0 +1,48 @@ +package com.example; + +import java.math.BigDecimal; +import java.util.UUID; + +abstract class Product { + /* + Beskriver en enskild produkt + */ + + 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 getId() {return id;} + public String getName() {return name;} + public Category getCategory() {return category;} + public BigDecimal getPrice() {return price;} + + public void setPrice(BigDecimal price) {this.price = price;} + + + public abstract String productDetails(); + + //returnerar price + public BigDecimal price() {return price;} + //returnerar name + public String name() {return name;} + //returnerar category + public Category category() {return category;} + //returnerar UUID + public UUID uuid() {return id;} +} + +/* +- Product (abstract base class) + - Keep UUID id, String name, Category category, BigDecimal price. + - Provide getters named uuid(), name(), category(), price() and a setter price(BigDecimal). + - Provide an abstract String productDetails() for polymorphism. + */ \ No newline at end of file diff --git a/src/main/java/com/example/Shippable.java b/src/main/java/com/example/Shippable.java new file mode 100644 index 00000000..756bf9f3 --- /dev/null +++ b/src/main/java/com/example/Shippable.java @@ -0,0 +1,10 @@ +package com.example; + +import java.math.BigDecimal; + +public interface Shippable { + //varor som skickas ut + //BigDecimal weight(); + Double weight(); + BigDecimal calculateShippingCost(); +} diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java new file mode 100644 index 00000000..1c73e816 --- /dev/null +++ b/src/main/java/com/example/Warehouse.java @@ -0,0 +1,130 @@ +package com.example; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +public class Warehouse { + /* + Håller listan över alla produkter + */ + private final List products = new ArrayList<>(); + private final List changedProducts = new ArrayList<>(); + + private static final Map INSTANCES = new HashMap<>(); + + + + //returns the same instance per unique name + public static Warehouse getInstance(String name){ + return INSTANCES.computeIfAbsent(name, k->new Warehouse()); + } + + public void clearProducts(){ + products.clear(); + } + + //lägg till produkter i warehouse + public void addProduct(Product product) { + if (product == null){ + throw new IllegalArgumentException("Product cannot be null."); + } + products.add(product); + } + //GetProducts + + public List getProducts() { + //return Collections.unmodifiableList(products); + return products.stream().toList(); + } + + //GetproductBYID - stream find first + public Optional getProductById(UUID id) { + //Traditionel for loop +// for (Product product : products){ +// if (product.getId().equals(id)){ +// return Optional.of(product); +// } +// } +// } return Optional.empty(); + return products.stream() + .filter(product -> product.getId().equals(id)) + .findFirst(); + } + + public List getChangedProducts() { + return Collections.unmodifiableList(changedProducts); + } + + // - updateProductPrice(UUID, BigDecimal): when not found, + // throw NoSuchElementException("Product not found with id: "). + public void updateProductPrice(UUID uuid, BigDecimal newPrice){ + Product product = products.stream() + .filter(p->p.getId().equals(uuid)) + .findFirst() //optional product + .orElseThrow(()-> new NoSuchElementException("Product not found with id: " + uuid)); + + product.setPrice(newPrice); + + if (!changedProducts.contains(product)){ + changedProducts.add(product); + } + } + + // - remove(UUID): remove the matching product if present with iterator. + public void remove(UUID uuid){ + for (Iterator iterator = products.iterator(); iterator.hasNext();){ + Product product = iterator.next(); + if (product.getId().equals(uuid)){ + iterator.remove(); + break; + } + } + } + + + //shippabeProducts, return list from stored products + public List shippableProducts(){ + List shippables = new ArrayList<>(); + for (Product product : products){ + if (product instanceof Shippable shippable){ + shippables.add(shippable); + } + } + return shippables; + } + + public Map> getProductsGroupedByCategories(){ + if(products.isEmpty()){ + return Collections.emptyMap(); + } + return products.stream() + .collect(Collectors.groupingBy(Product::getCategory)); + //map + //if villkor + //should return an empty map when grouping by category if empty" + } + public List expiredProducts(){ + //- expiredProducts(): return List that are expired. + LocalDate today = LocalDate.now(); + + return products.stream() + //filtrerar perishable produkter + .filter(p -> p instanceof Perishable) + //casha till perishable + .map(p->(Perishable) p) + //filtrerar om expirationDate har passerat + .filter(p->p.expirationDate().isBefore(today)) + //samla i en ny lista + .toList(); + } + + public boolean isEmpty(){return products.isEmpty();} +} + +/* +- Warehouse (singleton per name) + - remove(UUID): remove the matching product if present. + */ \ No newline at end of file diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 1779fc33..d610abd4 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -27,7 +27,7 @@ public WarehouseAnalyzer(Warehouse warehouse) { * @param maxPrice the upper bound (inclusive); must not be null and should be >= minPrice * @return a list of products with minPrice <= price <= maxPrice, in the warehouse's iteration order */ - public List findProductsInPriceRange(BigDecimal minPrice, BigDecimal maxPrice) { + private List findProductsInPriceRange(BigDecimal minPrice, BigDecimal maxPrice) { List result = new ArrayList<>(); for (Product p : warehouse.getProducts()) { BigDecimal price = p.price(); diff --git a/src/test/java/com/example/BasicTest.java b/src/test/java/com/example/BasicTest.java index 8a45877e..a11fc976 100644 --- a/src/test/java/com/example/BasicTest.java +++ b/src/test/java/com/example/BasicTest.java @@ -6,7 +6,6 @@ import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; import java.util.UUID; @@ -119,149 +118,22 @@ void should_beEmpty_when_newlySetUp() { .isTrue(); } - // --- Singleton and Factory Pattern Tests --- - @Nested @DisplayName("Factory and Singleton Behavior") class FactoryTests { - - @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); - } + // ... (omitted for brevity, same as before) } @Nested @DisplayName("Product Management") class ProductManagementTests { - @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); - } + // ... (most tests omitted for brevity, same as before) @Test @DisplayName("🔒 should return an unmodifiable list of products to protect internal state") void should_returnUnmodifiableProductList() { - // 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); + // ... (same as before) } @Test From 10ee08bfe5787d1aba03020f1deeeeb129e1efe2 Mon Sep 17 00:00:00 2001 From: Martin Blomberg Date: Tue, 14 Oct 2025 09:56:29 +0200 Subject: [PATCH 04/10] Adding missing testmethods (cherry picked from commit d1f0a89b23f840bc02a227d7415f499506514890) --- 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 a11fc976..8a45877e 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 ea58cf8342620e554d8274ccf7613888827e07c4 Mon Sep 17 00:00:00 2001 From: Marko Cavric Date: Tue, 21 Oct 2025 11:23:19 +0200 Subject: [PATCH 05/10] =?UTF-8?q?BasicTest=20+=20EdgeCase=20Godk=C3=A4nt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/Warehouse.java | 12 +++- .../java/com/example/WarehouseAnalyzer.java | 61 ++++++++++++------- 2 files changed, 48 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index 1c73e816..f6606569 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -12,16 +12,21 @@ public class Warehouse { */ private final List products = new ArrayList<>(); private final List changedProducts = new ArrayList<>(); - private static final Map INSTANCES = new HashMap<>(); - + private Warehouse() {} //returns the same instance per unique name + //Om det inte finns ett värde för nyckeln name, + // skapa ett nytt med new Warehouse() och lägg till det. Annars returnera det befintliga public static Warehouse getInstance(String name){ return INSTANCES.computeIfAbsent(name, k->new Warehouse()); } + public static Warehouse getInstance(){ + return getInstance("Warehouse"); + } + public void clearProducts(){ products.clear(); } @@ -31,6 +36,9 @@ public void addProduct(Product product) { if (product == null){ throw new IllegalArgumentException("Product cannot be null."); } + if (!getProductById(product.uuid()).equals(Optional.empty())){ + throw new IllegalArgumentException("Product with that id already exists, use updateProduct for updates."); + } products.add(product); } //GetProducts diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index d610abd4..5c6c1bdf 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -27,7 +27,7 @@ public WarehouseAnalyzer(Warehouse warehouse) { * @param maxPrice the upper bound (inclusive); must not be null and should be >= minPrice * @return a list of products with minPrice <= price <= maxPrice, in the warehouse's iteration order */ - private List findProductsInPriceRange(BigDecimal minPrice, BigDecimal maxPrice) { + public List findProductsInPriceRange(BigDecimal minPrice, BigDecimal maxPrice) { List result = new ArrayList<>(); for (Product p : warehouse.getProducts()) { BigDecimal price = p.price(); @@ -138,30 +138,45 @@ 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 price outliers by utilizing IQR algorithm + * 1.5 + * Q2 = median of the dataset. + * Q1 = median of n smallest data points. + * Q3 = median of n highest data points. * - * @param standardDeviations threshold in standard deviations (e.g., 2.0) - * @return list of products considered outliers + * @param iqrMulti + * @return list of outlier products */ - 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); - } + public List findPriceOutliers(double iqrMulti) { + + //1. Sort all your prices + List products = warehouse.getProducts().stream() + .sorted(Comparator.comparing(Product::price)) + .collect(Collectors.toList()); + + //2. Find the 1st quarter Q1, the price at the 25% mark + double Q1 = products.get(products.size() / 4).price().doubleValue(); + + //3. Find Q3 the price at the 75% mark + double Q3 = products.get(3 * products.size() / 4).price().doubleValue(); + + //4. Caluclate IQR = Q3 - Q1 + double IQR = Q3 - Q1; + + //5. Define the normal boundaries. A standard rule is: + // Lower bound: Q1-1.5 * IQR + double lowerBound = Q1 - (iqrMulti * IQR); + // Upper bound: Q3+1.5 * IQR + double upperBound = Q3 + (iqrMulti * IQR); + + //stream igen för outliers + List outliers = products.stream() + .filter(p-> { + double price = p.price().doubleValue(); + return price < lowerBound || price > upperBound; + }) + .toList(); + return outliers; } From ef46afea4f631930cfcea2aa4969d27a02a7a3ec Mon Sep 17 00:00:00 2001 From: Marko Cavric Date: Tue, 21 Oct 2025 12:17:57 +0200 Subject: [PATCH 06/10] =?UTF-8?q?Labb=202=20Godk=C3=A4nt=20-=20St=C3=A4dar?= =?UTF-8?q?=20ful=20kod?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/Category.java | 3 - src/main/java/com/example/Product.java | 26 ++++--- src/main/java/com/example/Warehouse.java | 92 +++++++++--------------- 3 files changed, 47 insertions(+), 74 deletions(-) diff --git a/src/main/java/com/example/Category.java b/src/main/java/com/example/Category.java index 3a43cfb9..7b79212a 100644 --- a/src/main/java/com/example/Category.java +++ b/src/main/java/com/example/Category.java @@ -1,8 +1,5 @@ package com.example; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; diff --git a/src/main/java/com/example/Product.java b/src/main/java/com/example/Product.java index 9e57f398..bf6e717d 100644 --- a/src/main/java/com/example/Product.java +++ b/src/main/java/com/example/Product.java @@ -20,26 +20,24 @@ protected Product(UUID id, String name, Category category, BigDecimal price) { this.price = price; } - public UUID getId() {return id;} - public String getName() {return name;} - public Category getCategory() {return category;} - public BigDecimal getPrice() {return price;} - - public void setPrice(BigDecimal price) {this.price = price;} - - - public abstract String productDetails(); - - //returnerar price - public BigDecimal price() {return price;} + //returnerar UUID + public UUID uuid() {return id;} //returnerar name public String name() {return name;} //returnerar category public Category category() {return category;} - //returnerar UUID - public UUID uuid() {return id;} + //returnerar price + public BigDecimal price() {return price;} + public void setPrice(BigDecimal price) {this.price = price;} + public abstract String productDetails(); + } +// public UUID getId() {return id;} +// public String getName() {return name;} +// public Category getCategory() {return category;} +// public BigDecimal getPrice() {return price;} + /* - Product (abstract base class) - Keep UUID id, String name, Category category, BigDecimal price. diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index f6606569..ff25d126 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -2,7 +2,6 @@ import java.math.BigDecimal; import java.time.LocalDate; -import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; @@ -20,57 +19,40 @@ private Warehouse() {} //Om det inte finns ett värde för nyckeln name, // skapa ett nytt med new Warehouse() och lägg till det. Annars returnera det befintliga public static Warehouse getInstance(String name){ - return INSTANCES.computeIfAbsent(name, k->new Warehouse()); + return INSTANCES.computeIfAbsent(name, n->new Warehouse()); } - public static Warehouse getInstance(){ return getInstance("Warehouse"); } - public void clearProducts(){ - products.clear(); - } - //lägg till produkter i warehouse public void addProduct(Product product) { if (product == null){ - throw new IllegalArgumentException("Product cannot be null."); + throw new IllegalArgumentException("Product cannot be null."); } if (!getProductById(product.uuid()).equals(Optional.empty())){ throw new IllegalArgumentException("Product with that id already exists, use updateProduct for updates."); } products.add(product); } - //GetProducts + //GetProducts public List getProducts() { - //return Collections.unmodifiableList(products); return products.stream().toList(); } //GetproductBYID - stream find first public Optional getProductById(UUID id) { - //Traditionel for loop -// for (Product product : products){ -// if (product.getId().equals(id)){ -// return Optional.of(product); -// } -// } -// } return Optional.empty(); return products.stream() - .filter(product -> product.getId().equals(id)) + .filter(product -> product.uuid().equals(id)) .findFirst(); } - public List getChangedProducts() { - return Collections.unmodifiableList(changedProducts); - } - // - updateProductPrice(UUID, BigDecimal): when not found, // throw NoSuchElementException("Product not found with id: "). public void updateProductPrice(UUID uuid, BigDecimal newPrice){ Product product = products.stream() - .filter(p->p.getId().equals(uuid)) + .filter(p->p.uuid().equals(uuid)) .findFirst() //optional product .orElseThrow(()-> new NoSuchElementException("Product not found with id: " + uuid)); @@ -81,39 +63,6 @@ public void updateProductPrice(UUID uuid, BigDecimal newPrice){ } } - // - remove(UUID): remove the matching product if present with iterator. - public void remove(UUID uuid){ - for (Iterator iterator = products.iterator(); iterator.hasNext();){ - Product product = iterator.next(); - if (product.getId().equals(uuid)){ - iterator.remove(); - break; - } - } - } - - - //shippabeProducts, return list from stored products - public List shippableProducts(){ - List shippables = new ArrayList<>(); - for (Product product : products){ - if (product instanceof Shippable shippable){ - shippables.add(shippable); - } - } - return shippables; - } - - public Map> getProductsGroupedByCategories(){ - if(products.isEmpty()){ - return Collections.emptyMap(); - } - return products.stream() - .collect(Collectors.groupingBy(Product::getCategory)); - //map - //if villkor - //should return an empty map when grouping by category if empty" - } public List expiredProducts(){ //- expiredProducts(): return List that are expired. LocalDate today = LocalDate.now(); @@ -129,7 +78,36 @@ public List expiredProducts(){ .toList(); } - public boolean isEmpty(){return products.isEmpty();} + //shippabeProducts, return list from stored products + public List shippableProducts(){ + return products.stream() + .filter(product -> product instanceof Shippable) + .map(product -> (Shippable) product) + .toList(); + } + + // - remove(UUID): remove the matching product if present + public void remove(UUID uuid){ + products.removeIf(product -> product.uuid().equals(uuid)); + } + + public boolean isEmpty(){ + return products.isEmpty(); + } + + public void clearProducts(){ + products.clear(); + changedProducts.clear(); + } + + public Map> getProductsGroupedByCategories(){ + if(products.isEmpty()){ + return Collections.emptyMap(); + } + return products.stream() + .collect(Collectors.groupingBy(Product::category)); + + } } /* From da66a2efd78bc03431974c06fd8643f451438fee Mon Sep 17 00:00:00 2001 From: Marko Cavric Date: Wed, 22 Oct 2025 11:41:59 +0200 Subject: [PATCH 07/10] =?UTF-8?q?Labb=202=20Godk=C3=A4nt=20-=20Implemented?= =?UTF-8?q?=20changes=20to=20method=20optimizeShippingGroups=20and=20more?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/example/Warehouse.java | 2 +- src/main/java/com/example/WarehouseAnalyzer.java | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index ff25d126..bce8e495 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -30,7 +30,7 @@ public void addProduct(Product product) { if (product == null){ throw new IllegalArgumentException("Product cannot be null."); } - if (!getProductById(product.uuid()).equals(Optional.empty())){ + if (getProductById(product.uuid()).isPresent()) { throw new IllegalArgumentException("Product with that id already exists, use updateProduct for updates."); } products.add(product); diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 5c6c1bdf..8ad47725 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -129,8 +129,9 @@ public Map calculateWeightedAveragePriceByCategory() { if (weightSum > 0) { avg = weightedSum.divide(BigDecimal.valueOf(weightSum), 2, RoundingMode.HALF_UP); } else { - BigDecimal sum = items.stream().map(Product::price).reduce(BigDecimal.ZERO, BigDecimal::add); - avg = sum.divide(BigDecimal.valueOf(items.size()), 2, RoundingMode.HALF_UP); +// BigDecimal sum = items.stream().map(Product::price).reduce(BigDecimal.ZERO, BigDecimal::add); +// avg = sum.divide(BigDecimal.valueOf(items.size()), 2, RoundingMode.HALF_UP); + avg = weightedSum.divide(new BigDecimal(items.size()), 2, RoundingMode.HALF_UP); } result.put(cat, avg); } @@ -191,7 +192,7 @@ public List findPriceOutliers(double iqrMulti) { */ public List optimizeShippingGroups(BigDecimal maxWeightPerGroup) { double maxW = maxWeightPerGroup.doubleValue(); - List items = warehouse.shippableProducts(); + List items = new ArrayList(warehouse.shippableProducts()); // Sort by descending weight (First-Fit Decreasing) items.sort((a, b) -> Double.compare(Objects.requireNonNullElse(b.weight(), 0.0), Objects.requireNonNullElse(a.weight(), 0.0))); List> bins = new ArrayList<>(); @@ -318,8 +319,11 @@ class ShippingGroup { public ShippingGroup(List products) { this.products = new ArrayList<>(products); this.totalWeight = products.stream() +// .map(Shippable::weight) +// .reduce(0.0, Double::sum); .map(Shippable::weight) - .reduce(0.0, Double::sum); + .map(BigDecimal::valueOf) + .reduce(BigDecimal.ZERO, BigDecimal::add).doubleValue(); this.totalShippingCost = products.stream() .map(Shippable::calculateShippingCost) .reduce(BigDecimal.ZERO, BigDecimal::add); From be0eaeee697d9182c2db9929a67a8a387abff507 Mon Sep 17 00:00:00 2001 From: Marko Cavric Date: Fri, 24 Oct 2025 13:33:22 +0200 Subject: [PATCH 08/10] Labb 2 - Implemented changes according to AI Feedback --- .../java/com/example/ElectronicsProduct.java | 7 ++- src/main/java/com/example/FoodProduct.java | 6 +-- src/main/java/com/example/Warehouse.java | 13 ++--- .../java/com/example/WarehouseAnalyzer.java | 48 ++++++++++++------- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/example/ElectronicsProduct.java b/src/main/java/com/example/ElectronicsProduct.java index ffedf9c4..6eeba244 100644 --- a/src/main/java/com/example/ElectronicsProduct.java +++ b/src/main/java/com/example/ElectronicsProduct.java @@ -15,7 +15,12 @@ public ElectronicsProduct(UUID id, String name, Category category, super(id, name, category, price); if (warrantyMonths < 0) { - throw new IllegalArgumentException("Warranty months cannot be negative.");} + throw new IllegalArgumentException("Warranty months cannot be negative."); + } + + if (weight == null || weight.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Weight cannot be negative."); //OR zero + } this.warrantyMonths = warrantyMonths; this.weight = weight; diff --git a/src/main/java/com/example/FoodProduct.java b/src/main/java/com/example/FoodProduct.java index 8389d5a5..4aaf6eb4 100644 --- a/src/main/java/com/example/FoodProduct.java +++ b/src/main/java/com/example/FoodProduct.java @@ -23,12 +23,12 @@ public FoodProduct(UUID id, String name, Category category, BigDecimal price, LocalDate expirationDate, BigDecimal weight) { super(id, name, category, price); - if (price.compareTo(BigDecimal.ZERO) <= 0) { + if (price == null || price.compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("Price cannot be negative."); } //weight villkor - if (weight.compareTo(BigDecimal.ZERO) < 0) { - throw new IllegalArgumentException("Weight cannot be negative."); + if (weight == null || weight.compareTo(BigDecimal.ZERO) <= 0) { + throw new IllegalArgumentException("Weight cannot be negative."); //OR zero. } this.expirationDate = expirationDate; diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index bce8e495..d763a88c 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -10,7 +10,9 @@ public class Warehouse { Håller listan över alla produkter */ private final List products = new ArrayList<>(); - private final List changedProducts = new ArrayList<>(); + //HashSet tillåter inte dubletter - unika objekt + private final Set changedProducts = Collections.synchronizedSet(new HashSet<>()); + private static final Map INSTANCES = new HashMap<>(); private Warehouse() {} @@ -38,7 +40,8 @@ public void addProduct(Product product) { //GetProducts public List getProducts() { - return products.stream().toList(); +// return products.stream().copyOf(); + return List.copyOf(products); } //GetproductBYID - stream find first @@ -57,12 +60,10 @@ public void updateProductPrice(UUID uuid, BigDecimal newPrice){ .orElseThrow(()-> new NoSuchElementException("Product not found with id: " + uuid)); product.setPrice(newPrice); - - if (!changedProducts.contains(product)){ - changedProducts.add(product); - } + changedProducts.add(product); //HashSet förhindrar dupletter, garanterar "uniqueness" } + //use Collections.synchronizedList or validate before adding to ensure uniqueness public List expiredProducts(){ //- expiredProducts(): return List that are expired. LocalDate today = LocalDate.now(); diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 8ad47725..babd4a61 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -5,6 +5,7 @@ import java.math.RoundingMode; import java.time.LocalDate; import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -13,12 +14,13 @@ */ 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]. * Based on tests: products priced exactly at the boundaries must be included; values outside are excluded. @@ -37,7 +39,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 +62,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 +81,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,8 +98,9 @@ public List findProductsAbovePrice(BigDecimal price) { } return result; } - + // Analytics Methods + /** * Computes the average price per category using product weight as the weighting factor when available. * Test expectation: for FoodProduct with weights, use weighted average = sum(price*weight)/sum(weight). @@ -137,33 +140,34 @@ public Map calculateWeightedAveragePriceByCategory() { } return result; } - + /** - *Identifies price outliers by utilizing IQR algorithm + * Identifies price outliers by utilizing IQR algorithm * 1.5 - * Q2 = median of the dataset. - * Q1 = median of n smallest data points. - * Q3 = median of n highest data points. + * Q2 = median of the dataset. + * Q1 = median of n smallest data points. + * Q3 = median of n highest data points. * * @param iqrMulti * @return list of outlier products */ + public List findPriceOutliers(double iqrMulti) { //1. Sort all your prices List products = warehouse.getProducts().stream() .sorted(Comparator.comparing(Product::price)) - .collect(Collectors.toList()); - - //2. Find the 1st quarter Q1, the price at the 25% mark - double Q1 = products.get(products.size() / 4).price().doubleValue(); + .toList(); - //3. Find Q3 the price at the 75% mark - double Q3 = products.get(3 * products.size() / 4).price().doubleValue(); + if (products.size() < 4) + return List.of(); //för små/litet dataset + //2. Find the 1st quarter Q1, the price at the 25% mark - kallar på median metoden under + double Q1 = median(products.subList(0, products.size() / 2)); + //3. Find Q3 the price at the 75% mark - Kallar på median metoden under + double Q3 = median(products.subList((products.size() + 1) / 2, products.size())); //4. Caluclate IQR = Q3 - Q1 double IQR = Q3 - Q1; - //5. Define the normal boundaries. A standard rule is: // Lower bound: Q1-1.5 * IQR double lowerBound = Q1 - (iqrMulti * IQR); @@ -180,6 +184,14 @@ public List findPriceOutliers(double iqrMulti) { return outliers; } + + //Median metod - AI Feedback förslag + private double median (List < Product > list) { + int size = list.size(); + return (size % 2 == 0) + ? (list.get(size / 2 - 1).price().doubleValue() + list.get(size / 2).price().doubleValue()) / 2 + : list.get(size / 2).price().doubleValue(); + } /** * Groups all shippable products into ShippingGroup buckets such that each group's total weight From 92e4bdc3a39e2686e9fa29690508a8dd5c5f8916 Mon Sep 17 00:00:00 2001 From: Marko Cavric Date: Fri, 24 Oct 2025 16:15:09 +0200 Subject: [PATCH 09/10] Labb 2 - More changes AI feedback --- src/main/java/com/example/Warehouse.java | 4 +++- src/main/java/com/example/WarehouseAnalyzer.java | 10 ++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index d763a88c..4a37f578 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -41,7 +41,9 @@ public void addProduct(Product product) { //GetProducts public List getProducts() { // return products.stream().copyOf(); - return List.copyOf(products); +// return List.copyOf(products); + //Third times the charm?... + return Collections.unmodifiableList(products); } //GetproductBYID - stream find first diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index babd4a61..03ab9678 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -132,9 +132,8 @@ public Map calculateWeightedAveragePriceByCategory() { if (weightSum > 0) { avg = weightedSum.divide(BigDecimal.valueOf(weightSum), 2, RoundingMode.HALF_UP); } else { -// BigDecimal sum = items.stream().map(Product::price).reduce(BigDecimal.ZERO, BigDecimal::add); -// avg = sum.divide(BigDecimal.valueOf(items.size()), 2, RoundingMode.HALF_UP); - avg = weightedSum.divide(new BigDecimal(items.size()), 2, RoundingMode.HALF_UP); + BigDecimal sum = items.stream().map(Product::price).reduce(BigDecimal.ZERO, BigDecimal::add); + avg = sum.divide(BigDecimal.valueOf(items.size()), 2, RoundingMode.HALF_UP); } result.put(cat, avg); } @@ -143,7 +142,6 @@ public Map calculateWeightedAveragePriceByCategory() { /** * Identifies price outliers by utilizing IQR algorithm - * 1.5 * Q2 = median of the dataset. * Q1 = median of n smallest data points. * Q3 = median of n highest data points. @@ -169,9 +167,9 @@ public List findPriceOutliers(double iqrMulti) { //4. Caluclate IQR = Q3 - Q1 double IQR = Q3 - Q1; //5. Define the normal boundaries. A standard rule is: - // Lower bound: Q1-1.5 * IQR + // Lower bound: double lowerBound = Q1 - (iqrMulti * IQR); - // Upper bound: Q3+1.5 * IQR + // Upper bound: double upperBound = Q3 + (iqrMulti * IQR); //stream igen för outliers From ffeba534acc3797706073b105d9ae4dbf0d7c25b Mon Sep 17 00:00:00 2001 From: Marko Cavric Date: Sat, 25 Oct 2025 18:03:15 +0200 Subject: [PATCH 10/10] Labb 2 - Final Version --- src/main/java/com/example/Warehouse.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java index 4a37f578..3aa8f257 100644 --- a/src/main/java/com/example/Warehouse.java +++ b/src/main/java/com/example/Warehouse.java @@ -40,9 +40,6 @@ public void addProduct(Product product) { //GetProducts public List getProducts() { -// return products.stream().copyOf(); -// return List.copyOf(products); - //Third times the charm?... return Collections.unmodifiableList(products); }