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();
}
}