zul/src/main/java/org/zkoss/zul/impl/NumberInputElement.java

Summary

Maintainability
D
2 days
Test Coverage
/* NumberInputElement.java

    Purpose:
        
    Description:
        
    History:
        Fri May  4 11:39:46     2007, Created by tomyeh

Copyright (C) 2007 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
    This program is distributed under LGPL Version 2.1 in the hope that
    it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zul.impl;

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

import org.zkoss.json.JSONValue;
import org.zkoss.lang.JVMs;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.math.RoundingModes;
import org.zkoss.util.Locales;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.sys.ObjectPropertyAccess;
import org.zkoss.zk.ui.sys.PropertyAccess;

/**
 * A skeletal implementation for number-type input box.
 *
 * @author tomyeh
 */
public abstract class NumberInputElement extends FormatInputElement {
    /** The rounding mode. */
    private int _rounding = BigDecimal.ROUND_HALF_EVEN;
    private Locale _locale;

    /** Sets the rounding mode.
     * Note: You cannot change the rounding mode unless you are
     * using Java 6 or later.
     *
     * @param mode the rounding mode. Allowed value:
     * {@link BigDecimal#ROUND_CEILING}, {@link BigDecimal#ROUND_DOWN},
     * {@link BigDecimal#ROUND_FLOOR}, {@link BigDecimal#ROUND_HALF_DOWN},
     * {@link BigDecimal#ROUND_HALF_UP}, {@link BigDecimal#ROUND_HALF_EVEN},
     * {@link BigDecimal#ROUND_UNNECESSARY} and {@link BigDecimal#ROUND_UP}
     *
     * @exception UnsupportedOperationException if Java 5 or below
     */
    public void setRoundingMode(int mode) {
        if (_rounding != mode) {
            if (!JVMs.isJava6())
                throw new UnsupportedOperationException("Java 6 or above is required");
            _rounding = mode;
            smartUpdate("rounding", mode);
        }
    }

    /** Sets the rounding mode by the name.
     * Note: You cannot change the rounding mode unless you are
     * using Java 6 or later.
     *
     * @param name the rounding mode's name. Allowed value:
    <dl>
    <dt>CEILING</dt>
    <dd>Rounding mode to round towards positive infinity.</dd>
    <dt>DOWN</dt>
    <dd>Rounding mode to round towards zero.</dd>
    <dt>FLOOR</dt>
    <dd>Rounding mode to round towards negative infinity.</dd>
    <dt>HALF_DOWN</dt>
    <dd>Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.</dd>
    <dt>HALF_EVEN</dt>
    <dd>Rounding mode to round towards the "nearest neighbor" unless both neighbors are equidistant, in which case, round towards the even neighbor.</dd>
    <dt>HALF_UP</dt>
    <dd>Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.</dd>
    <dt>UNNECESSARY</dt>
    <dd>Rounding mode to assert that the requested operation has an exact result, hence no rounding is necessary.</dd>
    <dt>UP</dt>
    <dd>Rounding mode to round away from zero.</dd>
    </dl>
     * @exception UnsupportedOperationException if Java 5 or below
     * @see RoundingModes
     */
    public void setRoundingMode(String name) {
        setRoundingMode(RoundingModes.valueOf(name));
    }

    /** Returns the rounding mode.
     * <p>Default: {@link BigDecimal#ROUND_HALF_EVEN}.
     */
    public int getRoundingMode() {
        return _rounding;
    }

    /** Returns the locale associated with this number input element,
     * or null if {@link Locales#getCurrent} is preferred.
     * @since 5.0.8
     */
    public Locale getLocale() {
        return _locale;
    }

    /** Sets the locale used to identify the symbols of this number input element.
     * <p>Default: null (i.e., {@link Locales#getCurrent}, the current locale
     * is assumed)
     * <p> If the format of {@link #getFormat()} is null, the format is assumed from
     * {@link #getDefaultFormat()}. (since 5.0.9)
     * @since 5.0.8
     */
    public void setLocale(Locale locale) {
        if (!Objects.equals(_locale, locale)) {
            _locale = locale;

            if (getFormat() == null)
                setFormat(getDefaultFormat());

            invalidate();
        }
    }

    /** Sets the locale used to identify the symbols of this number input element.
     * <p>Default: null (i.e., {@link Locales#getCurrent}, the current locale
     * is assumed)
     * @since 5.0.8
     */
    public void setLocale(String locale) {
        setLocale(locale != null && locale.length() > 0 ? Locales.getLocale(locale) : null);
    }

    /** Returns the real symbols according to the current locale.
     * @since 5.0.8
     */
    private String getRealSymbols() {
        if (_locale != null || isLocaleFormat()) {
            Locale usedLocale = getDefaultLocale();
            String localeName = usedLocale.toString();
            if (org.zkoss.zk.ui.impl.Utils.markClientInfoPerDesktop(getDesktop(),
                    "org.zkoss.zul.impl.NumberInputElement" + localeName)) {
                final DecimalFormatSymbols symbols = new DecimalFormatSymbols(usedLocale);
                Map<String, String> map = new HashMap<String, String>();
                map.put("GROUPING", String.valueOf(symbols.getGroupingSeparator()));
                map.put("DECIMAL", String.valueOf(symbols.getDecimalSeparator()));
                map.put("PERCENT", String.valueOf(symbols.getPercent()));
                map.put("PER_MILL", String.valueOf(symbols.getPerMill()));
                map.put("MINUS", String.valueOf(symbols.getMinusSign()));
                return JSONValue.toJSONString(new Object[] { localeName, map });
            } else
                return JSONValue.toJSONString(new Object[] { localeName, null });
        }
        return null;
    }

    /**
     * Returns if the "locale:" in {@link #getFormat()} is presented.
     * @since 9.5.1
     */
    protected boolean isLocaleFormat() {
        String format = getFormat();
        return format != null && format.startsWith("locale:");
    }

    /** Returns the default locale, either {@link #getLocale} or
     * {@link Locales#getCurrent} (never null).
     * It is useful when you wan to get a locale for this input.
     * @since 5.0.10
     */
    protected Locale getDefaultLocale() {
        if (isLocaleFormat())
            return Locales.getLocale(getFormat().substring(7), '-');
        return _locale != null ? _locale : Locales.getCurrent();
    }

    //super//
    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
        super.renderProperties(renderer);

        if (_rounding != BigDecimal.ROUND_HALF_EVEN)
            renderer.render("_rounding", _rounding);
        if (_locale != null)
            renderer.render("localizedSymbols", getRealSymbols());
    }

    /**
     * Return a default format for the number input element when the locale is specified.
     * <p> Default: <code>##,##0.##</code>, you can overwrite this by specifying
     * the following setting in zk.xml
     * <pre>{@code
     *    <library-property>
     *        <name>org.zkoss.zul.numberFormat</name>
     *        <value>##,##0.##</value>
     *    </library-property>
     * }</pre>
     * @since 5.0.9
     * @see #setLocale(Locale)
     */
    protected String getDefaultFormat() {
        return Library.getProperty("org.zkoss.zul.numberFormat", "##,##0.##");
    }

    //utilities//
    /** Formats a number (Integer, BigDecimal...) into a string.
     * If null, an empty string is returned.
     *
     * <p>A utility to assist the handling of numeric data.
     *
     * @see #toNumberOnly
     * @param defaultFormat used if {@link #getFormat} returns null.
     * If defaultFormat and {@link #getFormat} are both null,
     * the system's default format is used.
     */
    protected String formatNumber(Object value, String defaultFormat) {
        if (value == null)
            return "";

        final DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(getDefaultLocale());
        if (_rounding != BigDecimal.ROUND_HALF_EVEN)
            df.setRoundingMode(RoundingMode.valueOf(_rounding));

        String fmt = getFormat();
        if (fmt == null)
            fmt = defaultFormat;
        if (fmt != null && !isLocaleFormat()) {
            try {
                df.applyPattern(fmt); //Bug ZK-1518: should try apply pattern first
            } catch (IllegalArgumentException e) {
                if (_locale != null)
                    df.applyLocalizedPattern(fmt); //Bug ZK-1227: apply localized pattern for decimalbox
            }
        }
        return df.format(value);
    }

    /** Filters out non digit characters, such comma and whitespace,
     * from the specified value.
     * It is designed to let user enter data in more free style.
     * They may or may not enter data in the specified format.
     *
     * @return a two element array. The first element is the string to
     * parse with, say, Double.parseDouble. The second element is
     * an integer to indicate how many digits the result shall be scaled.
     * For example, if the second element is 2. Then, the result shall be
     * divided with 10 ^ 2.
     *
     * @see #formatNumber
     */
    protected Object[] toNumberOnly(String val) {
        if (val == null)
            return new Object[] { null, null };

        final DecimalFormatSymbols symbols = new DecimalFormatSymbols(getDefaultLocale());
        final char GROUPING = symbols.getGroupingSeparator(), DECIMAL = symbols.getDecimalSeparator(),
                PERCENT = symbols.getPercent(), PER_MILL = symbols.getPerMill(), //1/1000
                //not support yet: INFINITY = symbols.getInfinity(), NAN = symbols.getNaN(),
                MINUS = symbols.getMinusSign();
        final String fmt = getFormat();
        StringBuffer sb = null;
        int divscale = 0; //the second element
        boolean minus = false;
        for (int j = 0, len = val.length(); j < len; ++j) {
            final char cc = val.charAt(j);

            boolean ignore = false;
            //We handle percent and (nnn) specially
            if (cc == PERCENT) {
                divscale += 2;
                ignore = true;
            } else if (cc == PER_MILL) {
                divscale += 3;
                ignore = true;
            } else if (cc == '(') {
                minus = true;
                ignore = true;
            } else if (cc == '+') {
                ignore = true;
            }

            //We don't add if cc shall be ignored (not alphanum but in fmt)
            if (!ignore)
                ignore = (cc < '0' || cc > '9') && cc != DECIMAL && cc != MINUS && cc != '+'
                        && (Character.isWhitespace(cc) || cc == GROUPING || cc == ')'
                                || (fmt != null && fmt.indexOf(cc) >= 0));
            if (ignore) {
                if (sb == null)
                    sb = new StringBuffer(len).append(val.substring(0, j));
            } else {
                final char c2 = cc == MINUS ? '-' : cc == DECIMAL ? '.' : cc;
                if (cc != c2) {
                    if (sb == null)
                        sb = new StringBuffer(len).append(val.substring(0, j));
                    sb.append(c2);
                } else if (sb != null) {
                    sb.append(c2);
                }
            }
        }
        if (minus) {
            if (sb == null)
                sb = new StringBuffer(val.length() + 1).append(val);
            if (sb.length() > 0) {
                if (sb.charAt(0) == '-') {
                    sb.deleteCharAt(0);
                } else {
                    sb.insert(0, '-');
                }
            }
        }

        //handle '%'
        if (fmt != null && divscale > 0) {
            l_out: for (int j = 0, k, len = fmt.length(); (k = fmt.indexOf('\'', j)) >= 0;) {
                while (++k < len) {
                    final char cc = fmt.charAt(k);
                    if (cc == '%')
                        divscale -= 2;
                    else if (cc == '‰')
                        divscale -= 3;
                    else if (cc == '\'') {
                        ++k;
                        break;
                    }
                    if (divscale <= 0) {
                        divscale = 0;
                        break l_out;
                    }
                }
                j = k;
            }
        }

        return new Object[] { (sb != null ? sb.toString() : val), new Integer(divscale) };
    }

    //--ComponentCtrl--//
    private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(2);

    static {
        _properties.put("locale", new ObjectPropertyAccess() {
            public void setValue(Component cmp, Object locale) {
                if (locale instanceof String)
                    ((NumberInputElement) cmp).setLocale((String) locale);
                else if (locale instanceof Locale)
                    ((NumberInputElement) cmp).setLocale((Locale) locale);
            }

            public String getValue(Component cmp) {
                return ((NumberInputElement) cmp).getLocale().toString();
            }
        });
    }

    public PropertyAccess getPropertyAccess(String prop) {
        PropertyAccess pa = _properties.get(prop);
        if (pa != null)
            return pa;
        return super.getPropertyAccess(prop);
    }
}