Blame | Last modification | View Log | RSS feed
package com.smartdukaan.cron.offercircular;import java.util.ArrayList;import java.util.LinkedHashSet;import java.util.List;import java.util.Set;import java.util.regex.Matcher;import java.util.regex.Pattern;/*** Parses the "EMI tenure" cell into (months, scheme) pairs.** The scheme marker always FOLLOWS the group of months it applies to, so months are* accumulated and flushed each time a marker is seen:* "3,6 NC 9,12,18,24 LC" -> 3/NCE, 6/NCE, 9/LCE, 12/LCE, 18/LCE, 24/LCE** Some cells name a payment mode rather than a tenure ("Full Swipe", "UPI", "CIB"* alone) and correctly yield nothing.*/public final class TenureParser {/** Tenures the OEMs actually offer; anything else is reported, not stored. */private static final Set<Integer> VALID_MONTHS = new LinkedHashSet<>(java.util.Arrays.asList(3, 6, 9, 12, 18, 24));private static final Pattern TOKEN = Pattern.compile("\\d+|[A-Z]+");private static final Pattern HAS_DIGIT = Pattern.compile("\\d");private static final Pattern MODE_ONLY = Pattern.compile("FULL SWIPE|UPI|CIB");public static final class Tenure {private final int months;private final String scheme;Tenure(int months, String scheme) { this.months = months; this.scheme = scheme; }public int getMonths() { return months; }public String getScheme() { return scheme; }@Override public boolean equals(Object o) {if (!(o instanceof Tenure)) { return false; }Tenure t = (Tenure) o;return months == t.months && scheme.equals(t.scheme);}@Override public int hashCode() { return months * 31 + scheme.hashCode(); }@Override public String toString() { return months + ":" + scheme; }}public static final class Result {private final List<Tenure> tenures = new ArrayList<>();private String warning;public List<Tenure> getTenures() { return tenures; }public String getWarning() { return warning; }}private TenureParser() { }private static String scheme(String token) {switch (token) {case "NCE": case "NC": return "NCE";case "LCE": case "LC": return "LCE";case "CIB": return "CIB";default: return null;}}public static Result parse(String cell) {Result result = new Result();if (cell == null) {return result;}String text = cell.trim();if (text.isEmpty() || "-".equals(text) || "NA".equals(text) || "N/A".equals(text)) {return result;}// PDF line-wrap artefacts: "(L CE)" and "1 8" (meaning 18) are one token split// across a line break and rejoined with a space.text = text.replaceAll("(?i)\\bL\\s+CE\\b", "LCE").replaceAll("(?i)\\bN\\s+CE\\b", "NCE").replaceAll("(?<=\\d)\\s+(?=\\d)", "");String upper = text.toUpperCase();if (!HAS_DIGIT.matcher(upper).find()) {result.warning = MODE_ONLY.matcher(upper).find() ? "mode-only" : "no-digits";return result;}List<Integer> pending = new ArrayList<>();List<Integer> rejected = new ArrayList<>();Set<Tenure> collected = new LinkedHashSet<>();Matcher m = TOKEN.matcher(upper);while (m.find()) {String token = m.group();if (Character.isDigit(token.charAt(0))) {pending.add(Integer.parseInt(token));continue;}String scheme = scheme(token);if (scheme != null) {flush(pending, scheme, collected, rejected);} else if ("M".equals(token)) {flush(pending, "NONE", collected, rejected);}}// trailing months with no marker: CIB if the cell mentions it, else unschemeif (!pending.isEmpty()) {flush(pending, upper.contains("CIB") ? "CIB" : "NONE", collected, rejected);}result.tenures.addAll(collected);result.tenures.sort((a, b) -> a.months != b.months? Integer.compare(a.months, b.months): a.scheme.compareTo(b.scheme));if (!rejected.isEmpty()) {result.warning = "bad-tenure:" + rejected;}return result;}private static void flush(List<Integer> pending, String scheme,Set<Tenure> collected, List<Integer> rejected) {for (Integer months : pending) {if (VALID_MONTHS.contains(months)) {collected.add(new Tenure(months, scheme));} else {rejected.add(months);}}pending.clear();}}