VariantSummary.java
package com.rbctcsworld.ecommerce.product;
import java.math.BigDecimal;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
/**
* What a catalog card says about an item with variants: "8 options, 2 colours, sizes S-XL, $19.99-$21.99".
* Colours in the order they were added; sizes in clothing order (XXS..XXXL), then numbers, then the rest.
*/
public record VariantSummary(String group, int options, List<String> colors, List<String> sizes,
BigDecimal minPrice, BigDecimal maxPrice, int stock) {
static final List<String> SIZE_ORDER = List.of("XXS", "XS", "S", "M", "L", "XL", "XXL", "XXXL");
static VariantSummary of(String group, List<Product> variants) {
List<String> colors = variants.stream().map(Product::getColor).filter(Objects::nonNull).distinct().toList();
List<String> sizes = variants.stream().map(Product::getSize).filter(Objects::nonNull).distinct()
.sorted(SIZES).toList();
BigDecimal min = variants.stream().map(Product::getPrice).min(Comparator.naturalOrder()).orElse(BigDecimal.ZERO);
BigDecimal max = variants.stream().map(Product::getPrice).max(Comparator.naturalOrder()).orElse(BigDecimal.ZERO);
int stock = variants.stream().mapToInt(Product::getStock).sum();
return new VariantSummary(group, variants.size(), colors, sizes, min, max, stock);
}
/** Clothing sizes first in their natural order, then numeric sizes by value, then anything else A-Z. */
static final Comparator<String> SIZES = Comparator
.comparingInt((String s) -> rank(s))
.thenComparingDouble(VariantSummary::number)
.thenComparing(s -> s.toLowerCase(Locale.ROOT));
private static int rank(String s) {
int i = SIZE_ORDER.indexOf(s.trim().toUpperCase(Locale.ROOT));
if (i >= 0) return i;
return Double.isNaN(number(s)) ? 200 : 100;
}
private static double number(String s) {
java.util.regex.Matcher m = java.util.regex.Pattern.compile("^\\s*(\\d+(?:\\.\\d+)?)").matcher(s);
return m.find() ? Double.parseDouble(m.group(1)) : Double.NaN;
}
}