Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
9 changes: 9 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>25</source>
<target>25</target>
<compilerArgs>--enable-preview</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
</project>
101 changes: 76 additions & 25 deletions src/main/java/com/example/WarehouseAnalyzer.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package com.example;

import com.example.warehouse.*;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.*;
Expand Down Expand Up @@ -138,33 +139,85 @@ public Map<Category, BigDecimal> 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 products in Warehouse whose price deviates from the median by using the
* InterQuartile Range(IQR) method,a robust method that is used to identify outliners when the data is distorted.
* Outliers are defined as any price outside the range: [Q1 - 1.5 * IQR, Q3 + 1.5 * IQR].
* Test expectation: with a mostly tight cluster and two extremes, calling with 1.5 returns the two extremes.
*
* @param standardDeviations threshold in standard deviations (e.g., 2.0)
* @param thresholdFactor threshold factor (e.g., 1.5). The value is used as
* the multiplier in the IQR boundary calculation. (NOTE: 1.5 is the standard factor for the IQR method.)
* @return list of products considered outliers
*/
public List<Product> findPriceOutliers(double standardDeviations) {
public List<Product> findPriceOutliers(double thresholdFactor) {
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);
}
return outliers;
final int n = products.size();
// Edge case: Cannot calculate quartiles reliably with fewer than two items.
if (n < 2) return List.of();

//Retrieve prices(BigDecimal), convert to doubles and sort them.
List<Double> sortedPrices = products.stream()
.map(Product::price).mapToDouble(BigDecimal::doubleValue)
.boxed().sorted().toList();

//Find the median of Q1 and Q3
// L = (n - 1) * p method is used here (0.25 for Q1, 0.75 for Q3).
double q1Index = (n - 1) * 0.25;
double q3Index = (n - 1) * 0.75;

//Quantile value retrieval, using the helper method for linear interpolation.
double q1IndexValue = calculateQuantileValue(sortedPrices, q1Index);
double q3IndexValue = calculateQuantileValue(sortedPrices, q3Index);

//Determine the IQR for final result.
double iqr = q3IndexValue - q1IndexValue;
double lowerOutlier = q1IndexValue - thresholdFactor * iqr;
double upperOutlier = q3IndexValue + thresholdFactor * iqr;

//Use streams to filter the original product-list based on the calculations
return products.stream()
.filter(p -> {
double price = p.price().doubleValue();
// Price is an outlier if it is outside the calculated fences.
return (price < lowerOutlier || price > upperOutlier);
})
.collect(Collectors.toList());
}


/**
* Help-method for the method findPriceOutliers -
* Calculates the quantile value (Q1, Q2, or Q3) by using linear interpolation.
* This is based on my calculated floating-point index (L), using the standard formula L = (n-1) * p.
* @param sortedPrices Sorted list with prices.
* @param qIndex The calculated floating-point index for the quantile (L). Shows where the quantile should be.
* @return The interpolated quantile value.
*/
private static double calculateQuantileValue(List<Double> sortedPrices, double qIndex) {
final int n = sortedPrices.size();

// Calculate the 0-based integer index.

int lowerIndex = (int) Math.floor(qIndex);

// Check 1: If the calculated index falls before the start of the list (lowerIndex < 0),
// we return the lowest price.This handles small data sets where the quantile
// mathematically lands before the first element.
if (lowerIndex < 0) return sortedPrices.getFirst();

// Check 2: If the index falls at or after the end of the list (n-1),
// we return the highest price. This is necessary to prevent an IndexOutOfBoundsException
// when we try to fetch 'lowerIndex + 1'. The value is assumed to be the last element's value.
if (lowerIndex >= n - 1) return sortedPrices.getLast();

//Linear interpolation
// qDecimal represents the weight/fraction of the distance between the two prices.
double qDecimal = qIndex - Math.floor(qIndex);
double lowerPrice = sortedPrices.get(lowerIndex);
double upperPrice = sortedPrices.get(lowerIndex + 1);

// Formula for linear interpolation: Lower Price + (Weight * Distance between Prices)
return lowerPrice + (qDecimal * (upperPrice - lowerPrice));
}

/**
* 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
Expand Down Expand Up @@ -245,7 +298,6 @@ public Map<Product, BigDecimal> calculateExpirationBasedDiscounts() {
* when percentage exceeds 70%.
* - Category diversity: count of distinct categories in the inventory. The tests expect at least 2.
* - Convenience booleans: highValueWarning (percentage > 70%) and minimumDiversity (category count >= 2).
*
* Note: The exact high-value threshold is implementation-defined, but the provided tests create a clear
* separation using very expensive electronics (e.g., 2000) vs. low-priced food items (e.g., 10),
* allowing percentage computation regardless of the chosen cutoff as long as it matches the scenario.
Expand Down Expand Up @@ -330,7 +382,6 @@ public InventoryValidation(double highValuePercentage, int categoryDiversity) {
this.highValueWarning = highValuePercentage > 70.0;
this.minimumDiversity = categoryDiversity >= 2;
}

public double getHighValuePercentage() { return highValuePercentage; }
public int getCategoryDiversity() { return categoryDiversity; }
public boolean isHighValueWarning() { return highValueWarning; }
Expand Down
72 changes: 72 additions & 0 deletions src/main/java/com/example/warehouse/Category.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.example.warehouse;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
* Represents a product category. This class implements the Flyweight design pattern,
* ensuring that only one instance exists for each unique category name
* after normalization (e.g., "food", "Food", and "FOOD" all map to "Food").
*/
public class Category {

private final String name;
//Map to store and retrieve Category instances.
private static final Map<String, Category> categories = new HashMap<>();

/**
* Private constructor for the Flyweight pattern. Instances can only be created
* internally via the Code.of factory method.
*
* @param name The name of the category.
*/
private Category(String name) {
this.name = name;
}

/**
* Factory method to retrieve a Category instance based on the input name.
* The input name is validated and then normalized (first letter capitalized, rest lowercased)
* before checking the cache.
*
* @param name The desired category name (case-insensitive for lookup).
* @return A unique, cached instance of the Category.
* @throws IllegalArgumentException if the provided name is null or blank.
*/
public static Category of(String name) {

if (name == null) throw new IllegalArgumentException("Category name can't be null");
if (name.isBlank()) throw new IllegalArgumentException("Category name can't be blank");

String normalized = name.trim().substring(0, 1).toUpperCase() + name.trim().substring(1).toLowerCase();

return categories.computeIfAbsent(normalized, Category::new);
}

/**
* Retrieves the name of the category.
*
* @return The normalized name of the category (e.g., "Food").
*/

public String getName() {
return name;
}

@Override
public boolean equals(Object o) {
if (!(o instanceof Category category)) return false;
return Objects.equals(name, category.name);
}

@Override
public int hashCode() {
return Objects.hashCode(name);
}

@Override
public String toString() {
return name;
}
}
74 changes: 74 additions & 0 deletions src/main/java/com/example/warehouse/ElectronicsProduct.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.example.warehouse;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.UUID;

/**
* Represents an electronic item in the warehouse.
* An ElectronicsProduct implements Shippable (it has a weight and calculated shipping cost)
* and includes a specific warranty period.
*/
public class ElectronicsProduct extends Product implements Shippable {

private final int warrantyMonths;
private final BigDecimal weight;

/**
* Constructs a new ElectronicsProduct.
* * @param id The unique identifier for the product.
*
* @param name The name of the electronic product.
* @param category The product's category.
* @param price The initial price of the product.
* @param warrantyMonths The length of the product warranty in months.
* @param weight The weight of the product.
* @throws IllegalArgumentException if the warranty months or weight is negative.
*/
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.compareTo(BigDecimal.ZERO) < 0) throw new IllegalArgumentException("Weight cannot be negative.");
this.warrantyMonths = warrantyMonths;
this.weight = weight;
}

/**
* Provides a summary of the electronics product.
* * @return A String containing the product name and its warranty period.
*/
@Override
public String productDetails() {
return String.format("Electronics: %s, Warranty: %s months", name(), warrantyMonths);
}

// --- Implementation of Shippable Interface ---

/**
* Calculates the shipping cost for electronics, applying a surcharge for heavy items.
* Base cost is 79. An extra of 49 is added if the weight is over 5 units(kg).
* * @return The calculated shipping cost, rounded up to two decimals.
*/
@Override
public BigDecimal calculateShippingCost() {

BigDecimal baseShippingCost = BigDecimal.valueOf(79);
BigDecimal extraShippingCost = BigDecimal.valueOf(49);

if (this.weight.compareTo(BigDecimal.valueOf(5)) > 0) {
baseShippingCost = baseShippingCost.add(extraShippingCost);
}
return baseShippingCost.setScale(2, RoundingMode.HALF_UP);
}

/**
* Retrieves the weight of the product as a double.
*
* @return The product's weight.
*/
@Override
public Double weight() {
// Converts the BigDecimal weight to a double for the interface contract.
return this.weight.doubleValue();
}
}
80 changes: 80 additions & 0 deletions src/main/java/com/example/warehouse/FoodProduct.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.example.warehouse;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.util.UUID;

/**
* Represents a food product in the warehouse.
* A FoodProduct implements both Perishable (has an expiration date) and
* Shippable (has a weight and a defined shipping cost calculation).
*/
public class FoodProduct extends Product implements Perishable, Shippable {

private final LocalDate expirationDate;
private final BigDecimal weight;

/**
* Constructs a new FoodProduct.
*
* @param id The unique identifier for the product
* @param name The name of the food product.
* @param category The product's category.
* @param price The initial price of the product.
* @param expirationDate The date on which the product expires.
* @param weight The weight of the product.
* @throws IllegalArgumentException if the provided weight is negative.
*/
public FoodProduct(UUID id, String name, Category category, BigDecimal price, LocalDate expirationDate, BigDecimal weight) {
if (weight.compareTo(BigDecimal.ZERO) < 0) throw new IllegalArgumentException("Weight cannot be negative.");
// Calls the abstract Product constructor.
super(id, name, category, price);

this.expirationDate = expirationDate;
this.weight = weight;
}

/**
* Provides a summary of the food product, forced from the superclass.
* * @return A formatted String containing the product name and its expiration date.
*/
@Override
public String productDetails() {
return String.format("Food: %s, Expires: %s", name(), expirationDate);
}

// ----- Implementation of the interface Shippable ----

/**
* Calculates the shipping cost based on the product's weight.
* Uses weight * 50, rounded to two decimals.
* * @return The calculated shipping cost.
*/
@Override
public BigDecimal calculateShippingCost() {

return weight.multiply(new BigDecimal(50)).setScale(2, RoundingMode.HALF_UP);
}

/**
* Retrieves the weight of the product as a double.
* * @return The product's weight.
*/
@Override
public Double weight() {

return this.weight.doubleValue();
}
// ----Implementation of the Interface Perishable ----

/**
* Retrieves the specific expiration date for this food product.
* * @return The product's expiration date.
*/
@Override
public LocalDate expirationDate() {

return this.expirationDate;
}
}
Loading
Loading