// Copyright © 2015 HSL <https://www.hsl.fi> // This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses. package fi.hsl.parkandride.core.service; import static fi.hsl.parkandride.core.domain.PricingMethod.CUSTOM; import static fi.hsl.parkandride.core.domain.PricingMethod.PARK_AND_RIDE_247_FREE; import static fi.hsl.parkandride.core.domain.Usage.PARK_AND_RIDE; import java.util.*; import java.util.stream.Collectors; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.mysema.commons.lang.Pair; import fi.hsl.parkandride.core.domain.*; public final class CapacityPricingValidator { private CapacityPricingValidator() {} public static void validateAndNormalize(Facility facility, Collection<Violation> violations) { if (facility.pricingMethod == null) { // Not valid, but also not validated here. return; } else if (facility.pricingMethod == CUSTOM) { validateAndNormalizeCustomPricing(facility.builtCapacity, facility.pricing, facility.unavailableCapacities, violations); } else if (facility.pricingMethod == PARK_AND_RIDE_247_FREE) { validateAndNormalizeUnavailableCapacities(facility.unavailableCapacities, typeUsageMaxForUsage(facility.builtCapacity, PARK_AND_RIDE), violations); } else { throw new IllegalArgumentException("Unsupported PricingMethod: " + facility.pricingMethod); } } public static void validateAndNormalizeCustomPricing(Map<CapacityType, Integer> builtCapacity, List<Pricing> pricing, List<UnavailableCapacity> unavailableCapacities, final Collection<Violation> violations) { if (builtCapacity == null || pricing == null || unavailableCapacities == null) { return; } Map<Pair<CapacityType, Usage>, Integer> typeUsageMax = Maps.newHashMap(); final Set<CapacityType> pricingCapacityTypes = new LinkedHashSet<>(); for (int i=0; i < pricing.size(); i++) { Pricing p = pricing.get(i); if (p != null) { Integer capacity = builtCapacity.get(p.capacityType); if (capacity != null && capacity.intValue() >= 1) { pricingCapacityTypes.add(p.capacityType); } registerTypeUsageMaxCapacity(p, typeUsageMax); validateTotalCapacity(capacity, p, i, violations); validateHourOverlap(pricing, i, violations); } } builtCapacity.keySet().stream().forEach( type -> { if (!pricingCapacityTypes.contains(type)) { violations.add(pricingNotFoundForBuiltCapacity(type)); } }); validateAndNormalizeUnavailableCapacities(unavailableCapacities, typeUsageMax, violations); } private static Map<Pair<CapacityType, Usage>, Integer> typeUsageMaxForUsage(final Map<CapacityType, Integer> builtCapacity, final Usage usage) { return builtCapacity.entrySet().stream().collect(Collectors.toMap( (Map.Entry<CapacityType, Integer> e) -> new Pair<>(e.getKey(), usage), (Map.Entry<CapacityType, Integer> e) -> e.getValue() )); } private static void registerTypeUsageMaxCapacity(Pricing p, Map<Pair<CapacityType, Usage>, Integer> typeUsageMax) { Pair<CapacityType, Usage> typeUsage = new Pair(p.capacityType, p.usage); Integer prevMax = typeUsageMax.get(typeUsage); if (prevMax == null || prevMax.intValue() < p.maxCapacity) { typeUsageMax.put(typeUsage, p.maxCapacity); } } private static void validateTotalCapacity(Integer builtCapacity, Pricing p, int i, Collection<Violation> violations) { if (builtCapacity == null) { violations.add(builtCapacityNotFound(i)); } else if (builtCapacity < p.maxCapacity) { violations.add(pricingCapacityOverflow(i)); } } private static void validateHourOverlap(List<Pricing> pricing, int i, Collection<Violation> violations) { for (int j = i + 1; j < pricing.size(); ++j) { Pricing pi = pricing.get(i); Pricing pj = pricing.get(j); if (pi != null && pj != null && pi.overlaps(pj)) { violations.add(pricingOverlap(j)); } } } private static void validateAndNormalizeUnavailableCapacities(List<UnavailableCapacity> unavailableCapacities, Map<Pair<CapacityType, Usage>, Integer> typeUsageMax, Collection<Violation> violations) { if (unavailableCapacities == null) { return; } List<UnavailableCapacity> normalized = new ArrayList<>(unavailableCapacities.size()); Set<Pair<CapacityType, Usage>> uniqueTypeUsage = Sets.newHashSet(); for (int i=0; i < unavailableCapacities.size(); i++) { UnavailableCapacity uc = unavailableCapacities.get(i); if (uc != null) { Pair<CapacityType, Usage> typeUsage = new Pair(uc.capacityType, uc.usage); if (uniqueTypeUsage.add(typeUsage)) { Integer max = typeUsageMax.get(typeUsage); if (max == null) { continue; } else if (uc.capacity > max.intValue()) { violations.add(unavailableCapacityOverflow(i)); } } else { violations.add(duplicateUnavailableCapacity(i)); } normalized.add(uc); } } if (unavailableCapacities.size() != normalized.size()) { unavailableCapacities.clear(); unavailableCapacities.addAll(normalized); } } private static Violation duplicateUnavailableCapacity(int i) { return new Violation("DuplicateUnavailableCapacity", "unavailableCapacities[" + i + "]", "duplicate capacityType and usage"); } private static Violation unavailableCapacityOverflow(int i) { return new Violation("UnavailableCapacityOverflow", "unavailableCapacities[" + i + "].capacity", "unavailable capacity cannot exceed max capacity of pricing"); } private static Violation pricingNotFoundForBuiltCapacity(CapacityType type) { return new Violation("PricingNotFound", "builtCapacity." + type, "pricing not found"); } private static Violation pricingCapacityOverflow(int i) { return new Violation("PricingCapacityOverflow", "pricing[" + i + "].maxCapacity", "pricing capacity cannot exceed built capacity"); } private static Violation builtCapacityNotFound(int i) { return new Violation("BuiltCapacityNotFound", "pricing[" + i + "].capacityType", "no corresponding built capacity found for pricing capacity"); } private static Violation pricingOverlap(int j) { return new Violation("PricingOverlap", "pricing[" + j + "].time", "hour intervals cannot overlap when usage, capacity type and day type are the same"); } }