Hazem-Ben-Khalfallah
4/1/2016 - 11:27 AM

parse String number with different decimal and grouping separator

parse String number with different decimal and grouping separator

package com.influans.wonderland.babland.onboarding.performer;

import com.influans.wonderland.babland.dto.CsvMappingItem;
import com.influans.wonderland.babland.dto.CsvMappingList;
import com.influans.wonderland.babland.dto.ImportableDto;
import com.influans.wonderland.babland.entity.MappingRuleEntity;
import com.influans.wonderland.babland.entity.enums.RuleEnum;
import com.influans.wonderland.babland.enums.CatalogProductFieldsEnum;
import com.influans.wonderland.babland.onboarding.Context;
import com.influans.wonderland.babland.onboarding.ElementWrapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import java.util.Map;

@Component
public abstract class AbstractPricePerformer<T extends ImportableDto> implements Performer<T> {

    private final int MULTIPLY_PRICE_VALUE =100;
    @Override
    @SuppressWarnings("unchecked")
    public void apply(Context<T> ctx, ElementWrapper<T> elementWrapper) {
        final String price = getPrice(elementWrapper.getElement());
        if (!StringUtils.isEmpty(price)) {
            RuleEnum ruleEnum = null;
            if (ctx.hasFlowParam(Context.CSV_MAPPED_COLUMNS)) {
                final CsvMappingList csvMappingList = ctx.getFlowParam(Context.CSV_MAPPED_COLUMNS, CsvMappingList.class);
                final CsvMappingItem csvMappingItem = csvMappingList.getMappingItem(CatalogProductFieldsEnum.PRICE.name());
                if (csvMappingItem != null) {
                    ruleEnum = csvMappingItem.getRuleDto().getRuleEnum();
                }
            } else if (ctx.hasFlowParam(Context.CSV_MAPPING)) {
                final Map<String, MappingRuleEntity> mappingRuleMap = ctx.getFlowParam(Context.CSV_MAPPING, Map.class);
                final MappingRuleEntity priceRule = mappingRuleMap.get(CatalogProductFieldsEnum.PRICE.name());
                if (priceRule != null) {
                    ruleEnum = priceRule.getRuleEnum();
                }
            }

            if (RuleEnum.MULTIPLY.equals(ruleEnum)) {
                final Double value = parsePrice(price);
                if (value != null) {
                    final Long valueInCents = convertToCents(value);
                    if (valueInCents != null) {
                        setPrice(elementWrapper.getElement(), "" + valueInCents);
                    }
                }
            }
        }
    }

    protected abstract String getPrice(T element);

    protected abstract void setPrice(T element, String price);

    /**
     * Checks text validity and return the price as a double
     *
     * @param price text to be parsed as a price
     * @return price value as float or null
     */
    public Double parsePrice(String price) {
        final StringBuilder sb = new StringBuilder(price);

        // invalid number: both separators used multiple times
        if (StringUtils.countMatches(price, ".") > 1 && StringUtils.countMatches(price, ",") > 1) {
            return null;
        }

        int decimalMarkPosition;
        // detect possible decimal mark
        if (sb.lastIndexOf(".") > 0 || sb.lastIndexOf(",") > 0) {
            if (sb.lastIndexOf(".") < sb.lastIndexOf(",")) {
                decimalMarkPosition = sb.lastIndexOf(",");
            } else {
                decimalMarkPosition = sb.lastIndexOf(".");
            }

            if (decimalMarkPosition > 0) {
                // replace grouping separator by default one
                for (int i = 0; i < decimalMarkPosition; i++) {
                    if (!Character.isDigit(sb.charAt(i))) {
                        sb.replace(i, i + 1, " ");
                    }
                }

                if (isDecimalMark(price, decimalMarkPosition)) {
                    sb.replace(decimalMarkPosition, decimalMarkPosition + 1, "."); //replace decimal mark by default one
                } else {
                    sb.replace(decimalMarkPosition, decimalMarkPosition + 1, " "); //the last mark is a grouping separator
                }
            }

        }
        return parseAsDouble(sb.toString(), '.', ' ');
    }

    /**
     * Check if the char at the given position is a decimal mark or a grouping mark.
     *
     * @param numberText          text that will be treated as number
     * @param decimalMarkPosition decimal mark position
     * @return true if the char at given index is a decimal mark
     */
    private boolean isDecimalMark(String numberText, int decimalMarkPosition) {
        if (numberText.length() - (decimalMarkPosition + 1) <= 2) {
            return true;
        }

        final StringBuilder sb = new StringBuilder(numberText);
        final char decimalMark = sb.charAt(decimalMarkPosition);
        int count = 0;
        for (int i = 0; i < decimalMarkPosition; i++) {
            if (sb.charAt(i) == decimalMark) {
                count++;
            }
        }

        // the mark has not been used for grouping
        return count == 0 && decimalMarkPosition + 1 > 3;

    }

    /**
     * Parses text from the beginning of the given string to produce a float.
     *
     * @param numberText        A String that will be parsed.
     * @param decimalSeparator  Sets the character used for decimal sign. Different for French, etc.
     * @param groupingSeparator Sets the character used for grouping sign. Different for French, etc.
     * @return parsed float or null if numberText is not a valid number
     */
    private Double parseAsDouble(String numberText, char decimalSeparator, char groupingSeparator) {
        try {
            final DecimalFormat df = new DecimalFormat();
            final DecimalFormatSymbols symbols = new DecimalFormatSymbols();
            symbols.setDecimalSeparator(decimalSeparator);
            symbols.setGroupingSeparator(groupingSeparator);
            df.setDecimalFormatSymbols(symbols);
            final Number number = df.parse(numberText);
            return number.doubleValue();
        } catch (Exception e) {
            return null;
        }
    }

    /**
     * convert a currency number to cents
     *
     * @param number currency in USD/EURO
     * @return the currency value in cents
     */
    public Long convertToCents(double number) {
        final DecimalFormat df = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
        df.setMaximumFractionDigits(340); //340 = DecimalFormat.DOUBLE_FRACTION_DIGITS
        final String valueAsText = df.format(number);
        // price has more that 2 digits after decimal separator
        if (valueAsText.lastIndexOf(".") > 0 && valueAsText.length() - valueAsText.lastIndexOf(".") - 1 > 2)
            return null;
        final BigDecimal value1 = new BigDecimal(valueAsText);
        final BigDecimal value2 = new BigDecimal(MULTIPLY_PRICE_VALUE);
        final BigDecimal convertedPrice = value1.multiply(value2);
        return convertedPrice.longValue();
    }


}