Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/main/java/com/example/Category.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example;

import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public final class Category {
private static final Map<String, Category> CACHE = new ConcurrentHashMap<>();

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.isBlank()) {
throw new IllegalArgumentException("Category name can't be blank");
}

String normalizedName = trimmedName.substring(0, 1).toUpperCase(Locale.ROOT) +
trimmedName.substring(1).toLowerCase(Locale.ROOT);

return CACHE.computeIfAbsent(normalizedName, Category::new);
}

public String getName() {
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();
}
}
41 changes: 41 additions & 0 deletions src/main/java/com/example/ElectronicsProduct.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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.");
}
this.warrantyMonths = warrantyMonths;
this.weight = weight;
}
@Override
public BigDecimal calculateShippingCost() {
BigDecimal baseCost = new BigDecimal("79");
BigDecimal heavyWeightSurcharge = BigDecimal.ZERO;

if (weight.compareTo(new BigDecimal("5.0")) > 0) {
heavyWeightSurcharge = new BigDecimal("49");
}
return baseCost.add(heavyWeightSurcharge).setScale(2, RoundingMode.HALF_UP);

}
@Override
public double weight() {
return weight.doubleValue();
}
@Override
public String productDetails() {
return String.format("Electronics: %s, Warranty: %d months", name(), warrantyMonths);
}

}
42 changes: 42 additions & 0 deletions src/main/java/com/example/FoodProduct.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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 (weight.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Weight cannot be negative.");
}

this.expirationDate = expirationDate;
this.weight = weight;
}
@Override
public LocalDate expirationDate() {
return expirationDate;
}

@Override
public BigDecimal calculateShippingCost() {
return weight.multiply(new BigDecimal("50")).setScale(2, RoundingMode.HALF_UP);
}
@Override
public double weight() {
return weight.doubleValue();
}
@Override
public String productDetails() {
return String.format("Food: %s, Expires: %s", name(), expirationDate());
}


}
11 changes: 11 additions & 0 deletions src/main/java/com/example/Perishable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example;
import java.time.LocalDate;

public interface Perishable {

LocalDate expirationDate();

default boolean isExpired() {
return expirationDate().isBefore(LocalDate.now());
}
}
44 changes: 44 additions & 0 deletions src/main/java/com/example/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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;
setPrice(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) {
setPrice(newPrice);
}

protected void setPrice(BigDecimal newPrice) {
if (newPrice != null && newPrice.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Price cannot be negative.");
}
this.price = newPrice;
}

public abstract String productDetails();
}
11 changes: 11 additions & 0 deletions src/main/java/com/example/Shippable.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.example;

import java.math.BigDecimal;

public interface Shippable {

BigDecimal calculateShippingCost();

double weight();

}
82 changes: 82 additions & 0 deletions src/main/java/com/example/Warehouse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.example;

import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

public class Warehouse {

private static final Map<String, Warehouse> INSTANCES = new ConcurrentHashMap<>();
private final Map<UUID, Product> products;
private final Set<Product> changedProducts;

private Warehouse(String name) {
this.products = new LinkedHashMap<>();
this.changedProducts = new HashSet<>();
}

public static Warehouse getInstance(String name) {
return INSTANCES.computeIfAbsent(name, Warehouse::new);
}

public static Warehouse getInstance() {
return getInstance("DefaultWarehouse");
}

public void addProduct(Product product) {
if (product == null) {
throw new IllegalArgumentException("Product cannot be null.");
}
if (products.containsKey(product.uuid())) {
throw new IllegalArgumentException("Product with that id already exists, use updateProduct for updates.");
}

products.put(product.uuid(), product);
}
public List<Product> getProducts() {
return Collections.unmodifiableList(new ArrayList<>(products.values()));
}
public Optional<Product> getProductById(UUID id) {
return Optional.ofNullable(products.get(id));
}
public void remove(UUID id) {
products.remove(id);
}
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(product);
}
public List<Product> getChangedProducts() {
return List.copyOf(changedProducts);
}

public List<Perishable> expiredProducts() {
return products.values().stream()
.filter(p -> p instanceof Perishable)
.map(p -> (Perishable) p)
.filter(Perishable::isExpired)
.collect(Collectors.toList());
}
public List<Shippable> shippableProducts() {
return products.values().stream()
.filter(p -> p instanceof Shippable)
.map(p -> (Shippable) p)
.collect(Collectors.toList());
}
public void clearProducts() {
products.clear();
changedProducts.clear();
}
public boolean isEmpty() {
return products.isEmpty();
}
public Map<Category, List<Product>> getProductsGroupedByCategories() {
return products.values().stream()
.collect(Collectors.groupingBy(Product::category));
}
}
78 changes: 60 additions & 18 deletions src/main/java/com/example/WarehouseAnalyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -142,27 +143,67 @@ public Map<Category, BigDecimal> 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)
*
* @return list of products considered outliers
*/
public List<Product> findPriceOutliers(double standardDeviations) {
private BigDecimal getQuartile(List<BigDecimal> sortedPrices, double position) {
if (sortedPrices.isEmpty()) {
return BigDecimal.ZERO;
}
int n = sortedPrices.size();
double index = (n - 1) * position;
int lowerIndex = (int) Math.floor(index);
double fraction = index - lowerIndex;

if (fraction == 0) {
return sortedPrices.get(lowerIndex);
}

if (lowerIndex >= n - 1) {
return sortedPrices.get(n - 1);
}

BigDecimal lowerValue = sortedPrices.get(lowerIndex);
BigDecimal upperValue = sortedPrices.get(lowerIndex + 1);

BigDecimal diff = upperValue.subtract(lowerValue);
BigDecimal interpolatedValue = lowerValue.add(diff.multiply(BigDecimal.valueOf(fraction)));

return interpolatedValue.setScale(2, RoundingMode.HALF_UP);
}

/**
* Identifies products whose price is considered a statistical outlier using the
* Interquartile Range (IQR) method (Tukey's Fences).
* * An outlier is defined as any price that falls outside the range:
* [Q1 - (IQR * multiplier), Q3 + (IQR * multiplier)].
* This method is robust against extreme values that can skew mean-based calculations.
* Test expectation: In the provided test scenario, using a multiplier of 1.5
* correctly identifies both the extremely high and extremely low price points
* as outliers.
* * @param iqrMultiplier The multiplier used to define the fences (e.g., 1.5 for standard outlier detection).
* @return A list of products whose prices lie outside the calculated fences.
*/

public List<Product> findPriceOutliers(double iqrMultiplier) {
List<Product> 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<Product> outliers = new ArrayList<>();
for (Product p : products) {
double diff = Math.abs(p.price().doubleValue() - mean);
if (diff > threshold) outliers.add(p);
if (products.isEmpty()) {
return Collections.emptyList();
}
return outliers;
List<BigDecimal> prices = products.stream()
.map(Product::price)
.sorted()
.collect(Collectors.toList());
BigDecimal q1 = getQuartile(prices, 0.25);
BigDecimal q3 = getQuartile(prices, 0.75);
BigDecimal iqr = q3.subtract(q1);
BigDecimal multiplier = BigDecimal.valueOf(iqrMultiplier);
BigDecimal step = iqr.multiply(multiplier);
BigDecimal lowerFence = q1.subtract(step);
BigDecimal upperFence = q3.add(step);
return products.stream()
.filter(p -> p.price().compareTo(lowerFence) < 0 || p.price().compareTo(upperFence) > 0)
.collect(Collectors.toList());
}

/**
Expand Down Expand Up @@ -221,7 +262,7 @@ public Map<Product, BigDecimal> calculateExpirationBasedDiscounts() {
BigDecimal discounted = p.price();
if (p instanceof Perishable per) {
LocalDate exp = per.expirationDate();
long daysBetween = java.time.temporal.ChronoUnit.DAYS.between(today, exp);
long daysBetween = ChronoUnit.DAYS.between(today, exp);
if (daysBetween == 0) {
discounted = p.price().multiply(new BigDecimal("0.50"));
} else if (daysBetween == 1) {
Expand Down Expand Up @@ -368,4 +409,5 @@ public InventoryStatistics(int totalProducts, BigDecimal totalValue, BigDecimal
public int getCategoryCount() { return categoryCount; }
public Product getMostExpensiveProduct() { return mostExpensiveProduct; }
public Product getCheapestProduct() { return cheapestProduct; }

}
Loading
Loading