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.getRegularPrice() < o2.getRegularPrice() ? 1 050 : o1.getRegularPrice() == o2.getRegularPrice() ? 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++); 058 si.setSerialLast(i == item.getQuantity() - 1); 059 flats.add(si); 060 } 061 } 062 return flats; 063 } 064 065 /** 066 * Extract real cart items from a collection. 067 * <br>取出集合中真正的購物品項 068 * 069 * @param <T> type of real cart item. 070 * @param items those wrapped shopping cart items. 071 * @return real cart items 072 */ 073 public static <T> List<T> purification(Collection<? extends IItem<T>> items) { 074 List<T> res = new ArrayList<>(items.size()); 075 for (IItem<T> item : items) { 076 res.add(item.as()); 077 } 078 return res; 079 } 080 081 /** 082 * Caculate bonus from rules and shopping cart items. 083 * <br>從規則與購物品項計算出優惠 084 * 085 * @param <T> type of real cart item. 086 * @param rules Those rules will be applied to caculation. 087 * @param cartItems Shopping cart items 088 * @return Bonus from rules and items. 089 * @throws ScriptException Error prone when quantity measure from a 090 * {@link ILeafRule} meet an error. 091 */ 092 public static <T> Collection<BonusItem<T>> calcBonus(List<? extends IRule<T>> rules, Set<? extends IItem<T>> cartItems) throws ScriptException { 093 Map<Serializable, BonusItem<T>> bonuses = new HashMap<>(); 094 Collections.sort(rules, new Comparator<IRule<T>>() { 095 096 @Override 097 public int compare(IRule<T> o1, IRule<T> o2) { 098 return o1.getPriority() - o2.getPriority(); 099 } 100 }); 101 List<SingleItem<T>> items = flat(cartItems); 102 for (int i = 0; i < rules.size(); i++) { 103 IRule<T> rule = rules.get(i); 104 logger.debug("[{}] is going to apply.", rule); 105 if (iterate(rule, items, bonuses) && i != rules.size() - 1) { 106 } 107 } 108 return bonuses.values(); 109 } 110 111 private static <T> void unlockItems(Collection<SingleItem<T>> items) { 112 for (SingleItem<T> item : items) { 113 item.setExclusiveLock(false); 114 } 115 items.clear(); 116 } 117 118 private static <T> boolean iterate(IRule<T> rule, List<SingleItem<T>> cartItems, Map<Serializable, BonusItem<T>> bonuses) throws ScriptException { 119 return iterate(rule, cartItems, bonuses, false); 120 } 121 122 private static <T> boolean iterate(IRule<T> rule, List<SingleItem<T>> cartItems, Map<Serializable, BonusItem<T>> bonuses, boolean backOnTriggered) throws ScriptException { 123 List<SingleItem<T>> lockedItems = new ArrayList<>(); 124 boolean hasPromote = false; 125 if (rule.isLeaf()) { 126 ILeafRule<T> lrule = ((ILeafRule<T>) rule); 127 int idx = 0; 128 IItem preitem = null; 129 for (SingleItem<T> item : cartItems) { 130 if (!item.isExclusiveLock() && rule.contains(item)) { 131 BigDecimal op = BigDecimal.valueOf(item.getRegularPrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN); 132 BigDecimal sp = BigDecimal.valueOf(item.getSalePrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN); 133 logger.debug("\t inspect [{}-{}].", ++idx, item); 134 lockedItems.add(item.setExclusiveLock(true)); 135 rule.containsCountInc().serialNumInc(item.getItem().equals(preitem)).sumOfContainsRegularPriceInc(op).sumOfContainsSalePriceInc(sp) 136 .sumOfSerialRegularPriceInc(op).sumOfSerialSalePriceInc(sp); 137 if (rule.isTriggered(item)) { 138 double quantity = lrule.evalQuantity(); 139 if (quantity > 0) { 140 IItem<T> ruleBonus = lrule.getBonus(); 141 Serializable primaryKey = String.format("%d-%s", rule.hashCode(), CurrentItem.isCurrent(ruleBonus) ? item.getIdentity() : ruleBonus.getIdentity()); 142 BonusItem<T> bonus = bonuses.get(primaryKey); 143 if (bonus == null) { 144 IRule<T> topMostRule = rule.getPrevious() == null ? rule : rule.getPrevious(); 145 while (topMostRule.getPrevious() != null) { 146 topMostRule = topMostRule.getPrevious(); 147 } 148 bonus = new BonusItem<>(topMostRule, CurrentItem.isCurrent(ruleBonus) ? lrule.getCurrentAsBonus(item) : lrule.getBonus()); 149 bonuses.put(primaryKey, bonus); 150 logger.debug("init bonus[{}:{}]", primaryKey, bonus); 151 } 152 if (lrule.isLastQuantityOnly()) { 153 bonus.setFracQuantity(quantity); 154 } else { 155 bonus.incFracQuantity(quantity); 156 } 157 lockedItems.clear(); 158 hasPromote = true; 159 if (hasPromote && backOnTriggered) { 160 break; 161 } 162 } 163 } 164 if (item.isSerialLast()) { 165 rule.resetSumOfSerialRegularPrice(); 166 rule.resetSumOfSerialSalePrice(); 167 preitem = null; 168 } else { 169 preitem = item.getItem(); 170 } 171 } 172 } 173 } else { 174 int idx = 0; 175 for (SingleItem<T> item : cartItems) { 176 if (!item.isExclusiveLock() && rule.contains(item)) { 177 BigDecimal op = BigDecimal.valueOf(item.getRegularPrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN); 178 BigDecimal sp = BigDecimal.valueOf(item.getSalePrice()).setScale(rule.getPriceScale(), RoundingMode.HALF_EVEN); 179 logger.debug("\t inspect [{}-{}].", ++idx, item); 180 lockedItems.add(item.setExclusiveLock(true)); 181 rule.containsCountInc().sumOfContainsRegularPriceInc(op).sumOfContainsSalePriceInc(sp) 182 .sumOfSerialRegularPriceInc(op).sumOfSerialSalePriceInc(sp); 183 if (rule.isTriggered(item)) { 184 if (iterate(((IChainRule<T>) rule).getNext(), cartItems, bonuses, true)) { 185 lockedItems.clear(); 186 hasPromote = true; 187 if (hasPromote && backOnTriggered) { 188 break; 189 } 190 } 191 } 192 if (item.isSerialLast()) { 193 rule.resetSumOfSerialRegularPrice(); 194 rule.resetSumOfSerialSalePrice(); 195 } 196 } 197 if (hasPromote) { 198 rule.resetSumOfPrice(); 199 } 200 } 201 } 202 unlockItems(lockedItems); 203 if (hasPromote && backOnTriggered) { 204 rule.resetSumOfPrice(); 205 } 206 return hasPromote; 207 } 208}