001package scpc; 002 003import java.io.Serializable; 004import java.math.BigDecimal; 005import java.math.RoundingMode; 006import java.util.ArrayList; 007import java.util.Collection; 008import java.util.Collections; 009import java.util.Comparator; 010import java.util.HashMap; 011import java.util.List; 012import java.util.Map; 013import java.util.Set; 014import javax.script.ScriptException; 015import org.slf4j.Logger; 016import org.slf4j.LoggerFactory; 017import scpc.model.BonusItem; 018import scpc.model.CurrentItem; 019import scpc.model.IChainRule; 020import scpc.model.IItem; 021import scpc.model.ILeafRule; 022import scpc.model.IRule; 023import scpc.model.SingleItem; 024 025/** 026 * Calculator of Shopping cart promotion. 027 * 028 * @author Kent Yeh 029 */ 030public class Calculator { 031 032 private static final Logger logger = LoggerFactory.getLogger(Calculator.class); 033 034 /** 035 * Spread shopping cart into single item for iteration. 036 * <br>展開購物車以便逐一檢視單個品項 037 * 038 * @param <T> Type represented of shopping cart item. 039 * @param cartItems shopping cart items. 040 * @return 041 */ 042 public static <T> List<SingleItem<T>> flat(Set<? extends IItem<T>> cartItems) { 043 List<IItem<T>> items = new ArrayList<>(cartItems); 044 Collections.sort(items, new Comparator<IItem<T>>() { 045 @Override 046 public int compare(IItem o1, IItem o2) { 047 return o1.getSalePrice() < o2.getSalePrice() ? 1 048 : o1.getSalePrice() == o2.getSalePrice() 049 ? o1.getOriginalPrice() < o2.getOriginalPrice() ? 1 050 : o1.getOriginalPrice() == o2.getOriginalPrice() ? 0 : -1 : -1; 051 } 052 }); 053 int idx = 1; 054 List<SingleItem<T>> flats = new ArrayList<>(); 055 for (IItem item : items) { 056 for (int i = 0; i < item.getQuantity(); i++) { 057 SingleItem si = new SingleItem(item, idx++, i + 1); 058 si.setSerialLast(i == item.getQuantity() - 1); 059 flats.add(si); 060 } 061 } 062 return flats; 063 } 064 065 private static <T> List<SingleItem<T>> rearrange(List<SingleItem<T>> items) { 066 SingleItem preSingle = null; 067 IItem preitem = null; 068 int serialNum = 1, sequenceNum = 1; 069 List<SingleItem<T>> flats = new ArrayList<>(); 070 for (int i = 0; i < items.size(); i++) { 071 SingleItem curritem = items.get(i); 072 if (!curritem.isExclusiveLock()) { 073 if (preitem == null || !preitem.equals(curritem.getItem())) { 074 preitem = curritem.getItem(); 075 serialNum = 1; 076 if (preSingle != null) { 077 preSingle.setSerialLast(true); 078 } 079 } 080 curritem = new SingleItem(curritem.getItem(), sequenceNum++, serialNum++); 081 flats.add(curritem); 082 preSingle = curritem; 083 } 084 } 085 if (preSingle != null) { 086 preSingle.setSerialLast(true); 087 } 088 return flats; 089 } 090 091 /** 092 * Extract real cart items from a collection. 093 * <br>取出集合中真正的購物品項 094 * 095 * @param <T> type of real cart item. 096 * @param items those wrapped shopping cart items. 097 * @return real cart items 098 */ 099 public static <T> List<T> purification(Collection<? extends IItem<T>> items) { 100 List<T> res = new ArrayList<>(items.size()); 101 for (IItem<T> item : items) { 102 res.add(item.as()); 103 } 104 return res; 105 } 106 107 /** 108 * Caculate bonus from rules and shopping cart items. 109 * <br>從規則與購物品項計算出優惠 110 * 111 * @param <T> type of real cart item. 112 * @param rules Those rules will be applied to caculation. 113 * @param cartItems Shopping cart items 114 * @return Bonus from rules and items. 115 * @throws ScriptException Error prone when quantity measure from a 116 * {@link ILeafRule} meet an error. 117 */ 118 public static <T> Collection<BonusItem<T>> calcBonus(List<? extends IRule<T>> rules, Set<? extends IItem<T>> cartItems) throws ScriptException { 119 Map<Serializable, BonusItem<T>> bonuses = new HashMap<>(); 120 Collections.sort(rules, new Comparator<IRule<T>>() { 121 122 @Override 123 public int compare(IRule<T> o1, IRule<T> o2) { 124 return o1.getPriority() - o2.getPriority(); 125 } 126 }); 127 List<SingleItem<T>> items = flat(cartItems); 128 for (int i = 0; i < rules.size(); i++) { 129 IRule<T> rule = rules.get(i); 130 logger.debug("[{}] is going to apply.", rule); 131 if (iterate(rule, items, bonuses) && i != rules.size() - 1) { 132 items = rearrange(items); 133 } 134 } 135 return bonuses.values(); 136 } 137 138 private static <T> void unlockItems(Collection<SingleItem<T>> items) { 139 for (SingleItem<T> item : items) { 140 item.setExclusiveLock(false); 141 } 142 items.clear(); 143 } 144 145 private static <T> boolean iterate(IRule<T> rule, List<SingleItem<T>> cartItems, Map<Serializable, BonusItem<T>> bonuses) throws ScriptException { 146 return iterate(rule, cartItems, bonuses, false); 147 } 148 149 private static <T> boolean iterate(IRule<T> rule, List<SingleItem<T>> cartItems, Map<Serializable, BonusItem<T>> bonuses, boolean backOnTriggered) throws ScriptException { 150 List<SingleItem<T>> lockedItems = new ArrayList<>(); 151 boolean hasPromote = false; 152 if (rule.isLeaf()) { 153 ILeafRule<T> lrule = ((ILeafRule<T>) rule); 154 int idx = 0; 155 for (SingleItem<T> item : cartItems) { 156 if (!item.isExclusiveLock() && rule.contains(item)) { 157 BigDecimal op = BigDecimal.valueOf(item.getOriginalPrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN); 158 BigDecimal sp = BigDecimal.valueOf(item.getSalePrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN); 159 logger.debug("\t inspect [{}-{}].", ++idx, item); 160 lockedItems.add(item.setExclusiveLock(true)); 161 rule.containsCountInc().sumOfContainsOriginalPriceInc(op).sumOfContainsSalePriceInc(sp) 162 .sumOfSerialOriginalPriceInc(op).sumOfSerialSalePriceInc(sp); 163 if (rule.isTriggered(item)) { 164 double quantity = lrule.evalQuantity(); 165 if (quantity > 0) { 166 IItem<T> ruleBonus = lrule.getBonus(); 167 Serializable primaryKey = String.format("%d-%s", rule.hashCode(), CurrentItem.isCurrent(ruleBonus) ? item.getIdentity() : ruleBonus.getIdentity()); 168 BonusItem<T> bonus = bonuses.get(primaryKey); 169 if (bonus == null) { 170 IRule<T> topMostRule = rule.getPrevious() == null ? rule : rule.getPrevious(); 171 while (topMostRule.getPrevious() != null) { 172 topMostRule = topMostRule.getPrevious(); 173 } 174 bonus = new BonusItem<>(topMostRule, CurrentItem.isCurrent(ruleBonus) ? lrule.getCurrentAsBonus(item) : lrule.getBonus()); 175 bonuses.put(primaryKey, bonus); 176 logger.debug("init bonus[{}:{}]", primaryKey, bonus); 177 } 178 if (lrule.isLastQuantityOnly()) { 179 bonus.setFracQuantity(quantity); 180 } else { 181 bonus.incFracQuantity(quantity); 182 } 183 lockedItems.clear(); 184 hasPromote = true; 185 if (hasPromote && backOnTriggered) { 186 break; 187 } 188 } 189 } 190 if (item.isSerialLast()) { 191 rule.resetSumOfSerialOriginalPrice(); 192 rule.resetSumOfSerialSalePrice(); 193 } 194 } 195 } 196 } else { 197 int idx = 0; 198 for (SingleItem<T> item : cartItems) { 199 if (!item.isExclusiveLock() && rule.contains(item)) { 200 BigDecimal op = BigDecimal.valueOf(item.getOriginalPrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN); 201 BigDecimal sp = BigDecimal.valueOf(item.getSalePrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN); 202 logger.debug("\t inspect [{}-{}].", ++idx, item); 203 lockedItems.add(item.setExclusiveLock(true)); 204 rule.containsCountInc().sumOfContainsOriginalPriceInc(op).sumOfContainsSalePriceInc(sp) 205 .sumOfSerialOriginalPriceInc(op).sumOfSerialSalePriceInc(sp); 206 if (rule.isTriggered(item)) { 207 if (iterate(((IChainRule<T>) rule).getNext(), cartItems, bonuses, true)) { 208 lockedItems.clear(); 209 hasPromote = true; 210 if (hasPromote && backOnTriggered) { 211 break; 212 } 213 } 214 } 215 if (item.isSerialLast()) { 216 rule.resetSumOfSerialOriginalPrice(); 217 rule.resetSumOfSerialSalePrice(); 218 } 219 } 220 if (hasPromote) { 221 rule.resetSumOfPrice(); 222 } 223 } 224 } 225 unlockItems(lockedItems); 226 if (hasPromote && backOnTriggered) { 227 rule.resetSumOfPrice(); 228 } 229 return hasPromote; 230 } 231}