org.talend.dataprep.transformation.actions.math.ChangeNumberFormat.java Source code

Java tutorial

Introduction

Here is the source code for org.talend.dataprep.transformation.actions.math.ChangeNumberFormat.java

Source

// ============================================================================
// Copyright (C) 2006-2016 Talend Inc. - www.talend.com
//
// This source code is available under agreement available at
// https://github.com/Talend/data-prep/blob/master/LICENSE
//
// You should have received a copy of the agreement
// along with this program; if not, write to Talend SA
// 9 rue Pages 92150 Suresnes, France
//
// ============================================================================

package org.talend.dataprep.transformation.actions.math;

import static org.talend.daikon.number.BigDecimalParser.*;
import static org.talend.dataprep.parameters.ParameterType.STRING;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.*;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.talend.daikon.number.BigDecimalFormatter;
import org.talend.daikon.number.BigDecimalParser;
import org.talend.dataprep.api.action.Action;
import org.talend.dataprep.api.dataset.ColumnMetadata;
import org.talend.dataprep.api.dataset.row.DataSetRow;
import org.talend.dataprep.api.type.Type;
import org.talend.dataprep.i18n.ActionsBundle;
import org.talend.dataprep.parameters.Parameter;
import org.talend.dataprep.parameters.SelectParameter;
import org.talend.dataprep.transformation.actions.category.ActionCategory;
import org.talend.dataprep.transformation.actions.common.AbstractActionMetadata;
import org.talend.dataprep.transformation.actions.common.ColumnAction;
import org.talend.dataprep.transformation.api.action.context.ActionContext;
import org.talend.dataprep.util.NumericHelper;

/**
 * Change the pattern on a 'number' column.
 */
@Action(AbstractActionMetadata.ACTION_BEAN_PREFIX + ChangeNumberFormat.ACTION_NAME)
public class ChangeNumberFormat extends AbstractActionMetadata implements ColumnAction {

    /** Action name. */
    public static final String ACTION_NAME = "change_number_format"; //$NON-NLS-1$

    /** Parameter to define original decimal & grouping separators. */
    public static final String FROM_SEPARATORS = "from_separators";

    /**
     * The pattern shown to the user as a list. An item in this list is the value 'custom', which allow the user to
     * manually enter his pattern.
     */
    public static final String TARGET_PATTERN = "target_pattern"; //$NON-NLS-1$

    /**
     * Keys used in the values of different parameters:
     */
    public static final String CUSTOM = "custom";

    public static final String US_SEPARATORS = "us_separators";

    public static final String EU_SEPARATORS = "eu_separators";

    public static final String CH_SEPARATORS = "ch_separators";

    public static final String US_PATTERN = "us_pattern";

    public static final String EU_PATTERN = "eu_pattern";

    public static final String CH_PATTERN = "ch_pattern";

    public static final String SCIENTIFIC = "scientific";

    /**
     * Constants to build parameters name by concat:
     */
    public static final String FROM = "from";

    public static final String TARGET = "target";

    public static final String GROUPING = "_grouping";

    public static final String DECIMAL = "_decimal";

    public static final String SEPARATOR = "_separator";

    private static final Logger LOGGER = LoggerFactory.getLogger(ChangeNumberFormat.class);

    /**
     * Key to store the compiled format in action context.
     */
    private static final String COMPILED_TARGET_FORMAT = "compiled_number_format";

    private static final String UNKNOWN_SEPARATORS = "unknown_separators";

    private static final DecimalFormat CH_DECIMAL_PATTERN = new DecimalFormat("#,##0.##",
            DecimalFormatSymbols.getInstance(new Locale("FR", "CH")));

    @Override
    public String getName() {
        return ACTION_NAME;
    }

    @Override
    public String getCategory() {
        return ActionCategory.NUMBERS.getDisplayName();
    }

    @Override
    public boolean acceptField(ColumnMetadata column) {
        Type columnType = Type.get(column.getType());
        return Type.NUMERIC.isAssignableFrom(columnType);
    }

    @Override
    public List<Parameter> getParameters() {
        final List<Parameter> parameters = super.getParameters();

        // @formatter:off
        parameters.add(SelectParameter.Builder.builder().name(FROM_SEPARATORS)
                .item(UNKNOWN_SEPARATORS, UNKNOWN_SEPARATORS).item(US_SEPARATORS, US_SEPARATORS)
                .item(EU_SEPARATORS, EU_SEPARATORS).item(CH_SEPARATORS, CH_SEPARATORS)
                .item(CUSTOM, buildDecimalSeparatorParameter(FROM), buildGroupingSeparatorParameter(FROM))
                .defaultValue(UNKNOWN_SEPARATORS).build());
        // @formatter:on

        // @formatter:off
        parameters.add(SelectParameter.Builder.builder().name(TARGET_PATTERN).item(US_PATTERN, US_PATTERN)
                .item(EU_PATTERN, EU_PATTERN).item(CH_PATTERN, CH_PATTERN).item(SCIENTIFIC, SCIENTIFIC)
                .item(CUSTOM, CUSTOM,
                        new Parameter(TARGET_PATTERN + "_" + CUSTOM, STRING, US_DECIMAL_PATTERN.toPattern()),
                        buildDecimalSeparatorParameter(TARGET), buildGroupingSeparatorParameter(TARGET))
                .defaultValue(US_PATTERN).build());
        // @formatter:on

        return ActionsBundle.attachToAction(parameters, this);
    }

    private Parameter buildDecimalSeparatorParameter(String prefix) {
        final String name = prefix + DECIMAL + SEPARATOR;
        return SelectParameter.Builder.builder() //
                .name(name) //
                .item(".") //
                .item(",") //
                .item(CUSTOM, CUSTOM, new Parameter(name + "_" + CUSTOM, STRING, ".")) //
                .defaultValue(".") //
                .build();
    }

    private Parameter buildGroupingSeparatorParameter(String prefix) {
        final String name = prefix + GROUPING + SEPARATOR;
        return SelectParameter.Builder.builder() //
                .name(name) //
                .item(",", "comma") //
                .item(" ", "space") //
                .item(".", "dot") //
                .item("'", "quote") //
                .item("", "none") //
                .item(CUSTOM, CUSTOM, new Parameter(name + "_" + CUSTOM, STRING, ",")) //
                .canBeBlank(true) //
                .defaultValue(",") //
                .build();
    }

    private String getCustomizableParam(String pName, Map<String, String> parameters) {
        if (!parameters.containsKey(pName)) {
            return "";
        } else if (parameters.get(pName).equals(CUSTOM)) {
            return parameters.get(pName + "_" + CUSTOM);
        } else {
            return parameters.get(pName);
        }
    }

    /**
     * @param parameters the action parameters.
     * @return the pattern to use according to the given parameters.
     */
    private NumberFormat getFormat(Map<String, String> parameters) {
        switch (parameters.get(TARGET_PATTERN)) {
        case CUSTOM:
            return getCustomFormat(parameters);
        case US_PATTERN:
            return US_DECIMAL_PATTERN;
        case EU_PATTERN:
            return EU_DECIMAL_PATTERN;
        case CH_PATTERN:
            return CH_DECIMAL_PATTERN;
        case SCIENTIFIC:
            return US_SCIENTIFIC_DECIMAL_PATTERN;
        default:
            throw new IllegalArgumentException("Pattern is empty");
        }
    }

    /**
     * Return the custom format out of the parameters.
     *
     * @param parameters the action parameters.
     * @return the custom format out of the parameters.
     */
    private NumberFormat getCustomFormat(Map<String, String> parameters) {
        final DecimalFormat decimalFormat = new DecimalFormat(parameters.get(TARGET_PATTERN + "_" + CUSTOM));

        DecimalFormatSymbols decimalFormatSymbols = decimalFormat.getDecimalFormatSymbols();

        String decimalSeparator = getCustomizableParam(TARGET + DECIMAL + SEPARATOR, parameters);
        if (!StringUtils.isEmpty(decimalSeparator)) {
            decimalFormatSymbols.setDecimalSeparator(decimalSeparator.charAt(0));
        }

        String groupingSeparator = getCustomizableParam(TARGET + GROUPING + SEPARATOR, parameters);
        if (StringUtils.isEmpty(groupingSeparator) || groupingSeparator.equals(decimalSeparator)) {
            decimalFormat.setGroupingUsed(false);
        } else {
            decimalFormatSymbols.setGroupingSeparator(groupingSeparator.charAt(0));
        }

        decimalFormat.setDecimalFormatSymbols(decimalFormatSymbols);

        return decimalFormat;
    }

    @Override
    public void compile(ActionContext actionContext) {
        super.compile(actionContext);
        if (actionContext.getActionStatus() == ActionContext.ActionStatus.OK) {
            try {
                actionContext.get(COMPILED_TARGET_FORMAT, p -> getFormat(actionContext.getParameters()));
            } catch (IllegalArgumentException e) {
                LOGGER.warn("Unsupported number format", e);
                actionContext.setActionStatus(ActionContext.ActionStatus.CANCELED);
            }
        }
    }

    /**
     * @see ColumnAction#applyOnColumn(DataSetRow, ActionContext)
     */
    @Override
    public void applyOnColumn(DataSetRow row, ActionContext context) {
        final String columnId = context.getColumnId();
        final DecimalFormat decimalTargetFormat = context.get(COMPILED_TARGET_FORMAT);

        final ColumnMetadata columnMetadata = context.getRowMetadata().getById(columnId);
        columnMetadata.setType(Type.DOUBLE.toString());
        columnMetadata.setTypeForced(true);
        columnMetadata.setDomain("");
        columnMetadata.setDomainLabel("");
        columnMetadata.setDomainForced(true);

        final String value = row.get(columnId);
        final String mode = context.getParameters().get(FROM_SEPARATORS);
        if (StringUtils.isBlank(value) || (!NumericHelper.isBigDecimal(value) && !CUSTOM.equals(mode))) {
            LOGGER.debug("Unable to parse {} value as Number, it is blank or not numeric", value);
            return;
        }

        final BigDecimal bd;
        switch (mode) {
        case EU_SEPARATORS:
            bd = BigDecimalParser.toBigDecimal(value, ',', '.');
            break;
        case CH_SEPARATORS:
            bd = BigDecimalParser.toBigDecimal(value, '.', '\'');
            break;
        case CUSTOM:
            try {
                bd = parseCustomNumber(context, value);
                break;
            } catch (Exception e) {
                // User specified custom separators that doesn't validate value
                LOGGER.debug("Unable to use custom separators to parse value '{}'", value);
                return;
            }
        case US_SEPARATORS:
            bd = BigDecimalParser.toBigDecimal(value, '.', ',');
            break;
        case UNKNOWN_SEPARATORS:
        default:
            bd = BigDecimalParser.toBigDecimal(value);
            break;
        }

        String newValue = BigDecimalFormatter.format(bd, decimalTargetFormat);
        row.set(columnId, newValue);
    }

    /**
     * Parse the the given number with a custom separator.
     *
     * @param context the action context.
     * @param number the number to parse.
     * @return the given number parsed as BigDecimal using the action context custome separator.
     */
    private BigDecimal parseCustomNumber(ActionContext context, String number) {
        BigDecimal bd;
        String decSep = getCustomizableParam(FROM + DECIMAL + SEPARATOR, context.getParameters());
        String groupSep = getCustomizableParam(FROM + GROUPING + SEPARATOR, context.getParameters());

        if (StringUtils.isEmpty(decSep)) {
            decSep = ".";
        }
        if (StringUtils.isEmpty(groupSep)) {
            groupSep = ",";
        }

        bd = BigDecimalParser.toBigDecimal(number, decSep.charAt(0), groupSep.charAt(0));
        return bd;
    }

    @Override
    public Set<Behavior> getBehavior() {
        return EnumSet.of(Behavior.VALUES_COLUMN);
    }

}