Blame | Last modification | View Log | RSS feed
package com.smartdukaan.cron.offercircular;import java.math.BigDecimal;import java.util.ArrayList;import java.util.Arrays;import java.util.LinkedHashMap;import java.util.LinkedHashSet;import java.util.List;import java.util.Map;import java.util.Set;import java.util.regex.Matcher;import java.util.regex.Pattern;/*** Parses the "Offer Description" cell into a value per payment mode.** One circular row routinely carries two different values, and even two different* calculation types:* "10% cashback up to 2500 on CC EMI & Rs 1500 on CC Full Swipe"* -> CC_EMI = PERCENT 10% capped at 2500* CC_FULL_SWIPE = FLAT 1500* which is exactly why the amount belongs on (offer, mode) and never on the offer.*/public final class BenefitParser {public static final String CALC_FLAT = "FLAT";public static final String CALC_PERCENT = "PERCENT";/** Order matters: the compound forms must be tested before the simple ones. */private static final Object[][] MODE_PATTERNS = {{"CC\\s*/\\s*DC\\s*FULL\\s*SWIPE", new String[]{"CC_FULL_SWIPE", "DC_FULL_SWIPE"}},{"CC\\s*/\\s*DC\\s*EMI", new String[]{"CC_EMI", "DC_EMI"}},{"DC\\s*EMI", new String[]{"DC_EMI"}},{"CC\\s*EMI", new String[]{"CC_EMI"}},{"CCEMI", new String[]{"CC_EMI"}},{"DC\\s*FULL\\s*SWIPE", new String[]{"DC_FULL_SWIPE"}},{"CC\\s*FULL\\s*SWIPE", new String[]{"CC_FULL_SWIPE"}},{"CC\\s*REG", new String[]{"CC_FULL_SWIPE"}},{"FULL\\s*SWIPE", new String[]{"CC_FULL_SWIPE"}},{"\\bFS\\b", new String[]{"CC_FULL_SWIPE"}},{"\\bUPI\\b", new String[]{"UPI"}},{"CARDLESS", new String[]{"CARDLESS_EMI"}},};private static final List<Pattern> MODE_REGEX = new ArrayList<>();static {for (Object[] row : MODE_PATTERNS) {MODE_REGEX.add(Pattern.compile((String) row[0], Pattern.CASE_INSENSITIVE));}}private static final Pattern PERCENT = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*%");private static final Pattern CAP =Pattern.compile("(?:up\\s*to|upto)\\s*(?:rs\\.?\\s*)?(\\d[\\d,]*)", Pattern.CASE_INSENSITIVE);private static final Pattern FLAT =Pattern.compile("(?:rs\\.?\\s*)?(\\d[\\d,]{2,})", Pattern.CASE_INSENSITIVE);private static final Pattern EMI_ONLY =Pattern.compile("only\\s*nc\\s*emi", Pattern.CASE_INSENSITIVE);public static final class Benefit {private final String calcType;private final BigDecimal flatAmount;private final BigDecimal percent;private final BigDecimal maxAmount;Benefit(String calcType, BigDecimal flatAmount, BigDecimal percent, BigDecimal maxAmount) {this.calcType = calcType;this.flatAmount = flatAmount;this.percent = percent;this.maxAmount = maxAmount;}public String getCalcType() { return calcType; }public BigDecimal getFlatAmount() { return flatAmount; }public BigDecimal getPercent() { return percent; }public BigDecimal getMaxAmount() { return maxAmount; }@Override public String toString() {return CALC_PERCENT.equals(calcType)? percent + "%" + (maxAmount == null ? "" : " cap " + maxAmount): String.valueOf(flatAmount);}}public static final class Result {/** payment mode -> value; insertion-ordered */private final Map<String, Benefit> benefits = new LinkedHashMap<>();private String warning;public Map<String, Benefit> getBenefits() { return benefits; }public String getWarning() { return warning; }}private BenefitParser() { }private static List<String> findModes(String text) {Set<String> ordered = new LinkedHashSet<>();for (int i = 0; i < MODE_REGEX.size(); i++) {if (MODE_REGEX.get(i).matcher(text).find()) {ordered.addAll(Arrays.asList((String[]) MODE_PATTERNS[i][1]));}}return new ArrayList<>(ordered);}private static BigDecimal num(String raw) {return new BigDecimal(raw.replace(",", ""));}private static Benefit findValue(String text) {Matcher pct = PERCENT.matcher(text);if (pct.find()) {Matcher cap = CAP.matcher(text);return new Benefit(CALC_PERCENT, null, new BigDecimal(pct.group(1)),cap.find() ? num(cap.group(1)) : null);}Matcher flat = FLAT.matcher(text);if (flat.find()) {return new Benefit(CALC_FLAT, num(flat.group(1)), null, null);}return null;}public static Result parse(String cell) {Result result = new Result();if (cell == null) {result.warning = "blank";return result;}String text = cell.trim();if (text.isEmpty() || "-".equals(text) || "NA".equals(text) || "N/A".equals(text)) {result.warning = "blank";return result;}if (EMI_ONLY.matcher(text).find()) {result.warning = "emi-only-no-cashback";return result;}// '&' separates the per-mode clauses: "Rs.4000 on CC EMI & Rs.3000 CC Full Swipe"List<Benefit> values = new ArrayList<>();List<List<String>> modesPerSegment = new ArrayList<>();Benefit carried = null;for (String segment : text.split("\\s*&\\s*")) {if (segment.trim().isEmpty()) {continue;}Benefit value = findValue(segment);if (value == null) {value = carried; // "…& UPI" inherits the preceding amount} else {carried = value;}if (value == null) {continue;}List<String> modes = findModes(segment);values.add(value);modesPerSegment.add(modes.isEmpty() ? null : modes);}if (values.isEmpty()) {result.warning = "no-value-found";return result;}// A clause naming no mode of its own applies to whatever modes the whole cell// mentions and no other clause has claimed; ANY when the cell names none.List<String> allModes = findModes(text);if (allModes.isEmpty()) {allModes = new ArrayList<>(Arrays.asList("ANY"));}Set<String> claimed = new LinkedHashSet<>();for (List<String> modes : modesPerSegment) {if (modes != null) {claimed.addAll(modes);}}for (int i = 0; i < values.size(); i++) {List<String> targets = modesPerSegment.get(i);if (targets == null) {targets = new ArrayList<>();for (String mode : allModes) {if (!claimed.contains(mode)) {targets.add(mode);}}if (targets.isEmpty()) {targets = Arrays.asList("ANY");}}for (String mode : targets) {// first clause wins for a given moderesult.benefits.putIfAbsent(mode, values.get(i));}}return result;}}