diff --git a/.github/.keep b/.github/.keep new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/example/Category.java b/src/main/java/com/example/Category.java new file mode 100644 index 00000000..6b9632f7 --- /dev/null +++ b/src/main/java/com/example/Category.java @@ -0,0 +1,30 @@ +package com.example; + +import java.util.HashMap; + +public class Category { + public String name; + static HashMap categories = new HashMap<>(); + + private Category(String categoryName) { + name = categoryName; + name = name.substring(0, 1).toUpperCase() + + name.substring(1).toLowerCase(); + } + + public static Category of(String name) { + if (name == null) { + throw new IllegalArgumentException("Category name can't be null"); + } else if (name.isBlank()) + throw new IllegalArgumentException("Category name can't be blank"); + if (categories.containsKey(name)) + return categories.get(name); + categories.put(name, new Category(name)); + return categories.get(name); + } + + public String getName() { + return name; + } +} + diff --git a/src/main/java/com/example/ElectronicsProduct.java b/src/main/java/com/example/ElectronicsProduct.java new file mode 100644 index 00000000..17d36747 --- /dev/null +++ b/src/main/java/com/example/ElectronicsProduct.java @@ -0,0 +1,40 @@ +package com.example; + +import java.math.BigDecimal; +import java.util.UUID; + +public class ElectronicsProduct extends Product implements Shippable { + int warrantyMonths; + BigDecimal weight; + BigDecimal basePrice = BigDecimal.valueOf(79); + BigDecimal addPrice = BigDecimal.valueOf(49); + + public ElectronicsProduct(UUID id, String name, Category category, BigDecimal price, int warranty, BigDecimal weight) { + super(id, name, category, price); + this.warrantyMonths = warranty; + if (warrantyMonths < 0) + throw new IllegalArgumentException("Warranty months cannot be negative."); + this.weight = weight; + } + + public int getWarrantyMonths() { + return warrantyMonths; + } + + @Override + public String productDetails() { + return "Electronics: " + name + ", Warranty: " + warrantyMonths + " months"; + } + + @Override + public double weight() { + return weight.doubleValue(); + } + + @Override + public BigDecimal calculateShippingCost() { + return (weight.compareTo(BigDecimal.valueOf(5.0)) >= 0) + ? basePrice.add(addPrice) + : basePrice; + } +} diff --git a/src/main/java/com/example/FoodProduct.java b/src/main/java/com/example/FoodProduct.java new file mode 100644 index 00000000..c50b8f3a --- /dev/null +++ b/src/main/java/com/example/FoodProduct.java @@ -0,0 +1,38 @@ +package com.example; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.UUID; + +public class FoodProduct extends Product implements Perishable, Shippable { + public LocalDate expirationDate; + public BigDecimal weight; + + public FoodProduct(UUID id, String name, Category category, BigDecimal price, LocalDate expirationDate, BigDecimal weight) { + super(id, name, category, price); + this.expirationDate = expirationDate; + if (weight.compareTo(BigDecimal.ZERO) < 0) + throw new IllegalArgumentException("Weight cannot be negative."); + this.weight = weight; + } + + @Override + public LocalDate expirationDate() { + return expirationDate; + } + + @Override + public String productDetails() { + return "Food: " + name + ", Expires: " + expirationDate; + } + + @Override + public double weight() { + return weight.doubleValue(); + } + + @Override + public BigDecimal calculateShippingCost() { + return weight.multiply(BigDecimal.valueOf(50)); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/Perishable.java b/src/main/java/com/example/Perishable.java new file mode 100644 index 00000000..adeba099 --- /dev/null +++ b/src/main/java/com/example/Perishable.java @@ -0,0 +1,10 @@ +package com.example; + +import java.time.LocalDate; + +interface Perishable { + LocalDate expirationDate(); + default boolean isExpired() { + return LocalDate.now().isAfter(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..ba422d27 --- /dev/null +++ b/src/main/java/com/example/Product.java @@ -0,0 +1,42 @@ +package com.example; + +import java.math.BigDecimal; +import java.util.UUID; + +public abstract class Product { + UUID uuid; + String name; + Category category; + BigDecimal price; + + public Product(UUID uuid, String name, Category category, BigDecimal price) { + this.uuid = uuid; + this.name = name; + this.category = category; + if(price.compareTo(BigDecimal.ZERO) < 0) + throw new IllegalArgumentException("Price cannot be negative."); + this.price = price; + } + + public BigDecimal price() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public UUID uuid() { + return uuid; + } + + public String name() { + return name; + } + + public Category category() { + return category; + } + + public abstract String productDetails(); +} diff --git a/src/main/java/com/example/Shippable.java b/src/main/java/com/example/Shippable.java new file mode 100644 index 00000000..6b1b2e49 --- /dev/null +++ b/src/main/java/com/example/Shippable.java @@ -0,0 +1,8 @@ +package com.example; + +import java.math.BigDecimal; + +public interface Shippable { + BigDecimal calculateShippingCost(); + double weight(); +} diff --git a/src/main/java/com/example/Warehouse.java b/src/main/java/com/example/Warehouse.java new file mode 100644 index 00000000..33e6a417 --- /dev/null +++ b/src/main/java/com/example/Warehouse.java @@ -0,0 +1,114 @@ +package com.example; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.*; + +public class Warehouse { + static HashMap warehouses = new HashMap<>(); + HashMap products; + List shippables; + List perishables; + HashMap, HashMap> changedProducts; + + private Warehouse() { + this.products = new HashMap<>(); + this.shippables = new ArrayList<>(); + this.perishables = new ArrayList<>(); + this.changedProducts = new HashMap<>(); + } + + public List getProducts(){ + return List.copyOf(products.values()); + } + + public List shippableProducts() { + return shippables; + } + + public void clearProducts(){ + products.clear(); + Category.categories.clear(); + shippables.clear(); + perishables.clear(); + } + + public boolean isEmpty(){ + return products.isEmpty(); + } + + public void addProduct(Product product){ + if (product == null) + throw new IllegalArgumentException("Product cannot be null."); + if (getProductById(product.uuid).isPresent()) + throw new IllegalArgumentException("Product with that id already exists, use updateProduct for updates."); + products.put(product.uuid, product); + if(product instanceof Shippable) + shippables.add((Shippable) product); + if(product instanceof Perishable) + perishables.add((Perishable) product); + } + + public void remove(UUID id){ + products.remove(id); + } + + public Optional getProductById(UUID id){ + if (products.isEmpty()) + return Optional.empty(); + return (Optional.ofNullable(products.get(id))); + } + + public static synchronized Warehouse getInstance(String string) { + if (!warehouses.containsKey(string)) { + warehouses.put(string, new Warehouse()); + } + return warehouses.get(string); + } + + public static synchronized Warehouse getInstance() { + if(!warehouses.containsKey("default")) + warehouses.put("default", new Warehouse()); + return warehouses.get("default"); + } + + public Map> getProductsGroupedByCategories() { + if(products == null || products.isEmpty()){ + return Collections.emptyMap(); + } + HashMap> result = new HashMap<>(); + var tempMap = Category.categories.entrySet(); + var productsList = new ArrayList<>(products.values()); + for(var category : tempMap){ + List tempList = new ArrayList<>(); + productsList.stream() + .filter(element -> element.category.equals(category.getValue())) + .forEach(tempList::add); + result.put(category.getValue(), tempList); + } + return result; + } + + public void updateProductPrice(UUID id, BigDecimal newPrice){ + var product = getProductById(id); + if(product.isPresent()) { + var tempMap = new HashMap(); + tempMap.put(LocalDateTime.now(), product.get().price); + changedProducts.put(product, tempMap); + product.get().setPrice(newPrice); + } + else + throw new NoSuchElementException("Product not found with id:" + id); + } + + public Map, HashMap> getChangedProducts(){ + return changedProducts; + } + + public List expiredProducts() { + return perishables.stream() + .filter(Perishable::isExpired) + .toList(); + } +} + diff --git a/src/main/java/com/example/WarehouseAnalyzer.java b/src/main/java/com/example/WarehouseAnalyzer.java index 1779fc33..54349ac5 100644 --- a/src/main/java/com/example/WarehouseAnalyzer.java +++ b/src/main/java/com/example/WarehouseAnalyzer.java @@ -145,26 +145,46 @@ public Map calculateWeightedAveragePriceByCategory() { * @param standardDeviations threshold in standard deviations (e.g., 2.0) * @return list of products considered outliers */ + + public List findPriceOutliers(double standardDeviations) { - List products = warehouse.getProducts(); - int n = products.size(); - 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() + List productList = warehouse.getProducts(); + List products = productList.stream() .map(Product::price) - .mapToDouble(bd -> Math.pow(bd.doubleValue() - mean, 2)) - .sum() / n; - double std = Math.sqrt(variance); - double threshold = standardDeviations * std; + .sorted() + .toList(); + + var Q1 = getPercentile(products, 25); + var Q3 = getPercentile(products, 75); + + var IQR = Q3.doubleValue()-Q1.doubleValue(); + var lowRange = Q1.doubleValue()-standardDeviations*IQR; + var highRange = Q3.doubleValue()+standardDeviations*IQR; + List outliers = new ArrayList<>(); - for (Product p : products) { - double diff = Math.abs(p.price().doubleValue() - mean); - if (diff > threshold) outliers.add(p); + for(Product p : productList){ + if((p.price.doubleValue()>highRange) || (p.price.doubleValue()> T getPercentile(Collection input, double percentile) { + if (input == null || input.isEmpty()) { + throw new IllegalArgumentException("The input dataset cannot be null or empty."); + } + if (percentile < 0 || percentile > 100) { + throw new IllegalArgumentException("Percentile must be between 0 and 100 inclusive."); + } + List sortedList = input.stream() + .sorted() + .collect(Collectors.toList()); + + int rank = percentile == 0 ? 1 : (int) Math.ceil(percentile / 100.0 * input.size()); + return sortedList.get(rank - 1); + } + /** * 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