/* * @(#)JavaNumberFormatter.java * * Copyright (c) 2009-2010 The authors and contributors of JHotDraw. * * You may not use, copy or modify this file, except in compliance with the * accompanying license terms. */ package org.jhotdraw.text; import edu.umd.cs.findbugs.annotations.Nullable; import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.text.ParseException; import java.util.Locale; import javax.swing.JFormattedTextField.AbstractFormatterFactory; import javax.swing.text.DefaultFormatter; import javax.swing.text.DefaultFormatterFactory; /** * {@code ScaledNumberFormatter} is used to format numbers written in the * Java programming syntax. * * @author Werner Randelshofer * @version $Id$ */ public class JavaNumberFormatter extends DefaultFormatter { private static final long serialVersionUID = 1L; /** * Specifies whether the formatter allows null values. */ private boolean allowsNullValue = false; @SuppressWarnings("rawtypes") private Comparable min; @SuppressWarnings("rawtypes") private Comparable max; @Nullable private String unit; private DecimalFormat decimalFormat; private DecimalFormat scientificFormat; private double multiplier = 1; private int minIntDigits; private int maxIntDigits; private int minFractionDigits; private int maxFractionDigits; private int minNegativeExponent = -3; private int minPositiveExponent = 8; private boolean usesScientificNotation = true; /** * Creates a <code>NumberFormatter</code> with the a default * <code>NumberFormat</code> instance obtained from * <code>NumberFormat.getNumberInstance()</code>. */ public JavaNumberFormatter() { super(); initFormats(); } /** * Creates a NumberFormatter with the specified Format instance. */ public JavaNumberFormatter(double min, double max, double multiplier) { this(min, max, multiplier, false, null); } /** * Creates a NumberFormatter with the specified Format instance. */ public JavaNumberFormatter(double min, double max, double multiplier, boolean allowsNullValue) { this(min, max, multiplier, allowsNullValue, null); } /** * Creates a NumberFormatter with the specified Format instance. */ public JavaNumberFormatter(double min, double max, double multiplier, boolean allowsNullValue, @Nullable String unit) { super(); initFormats(); setMinimum(min); setMaximum(max); setMultiplier(multiplier); setAllowsNullValue(allowsNullValue); setOverwriteMode(false); setUnit(unit); } private void initFormats() { DecimalFormatSymbols s = new DecimalFormatSymbols(Locale.ENGLISH); decimalFormat = new DecimalFormat("#################0.#################", s); scientificFormat = new DecimalFormat("0.0################E0", s); } /** * Sets the minimum permissible value. If the <code>valueClass</code> has * not been specified, and <code>minimum</code> is non null, the * <code>valueClass</code> will be set to that of the class of * <code>minimum</code>. * * @param minimum Minimum legal value that can be input * @see #setValueClass */ @SuppressWarnings("rawtypes") public void setMinimum(Comparable minimum) { if (getValueClass() == null && minimum != null) { setValueClass(minimum.getClass()); } min = minimum; } /** * Returns the minimum permissible value. * * @return Minimum legal value that can be input */ @SuppressWarnings("rawtypes") public Comparable getMinimum() { return min; } /** * Sets the maximum permissible value. If the <code>valueClass</code> has * not been specified, and <code>max</code> is non null, the * <code>valueClass</code> will be set to that of the class of * <code>max</code>. * * @param max Maximum legal value that can be input * @see #setValueClass */ @SuppressWarnings("rawtypes") public void setMaximum(Comparable max) { if (getValueClass() == null && max != null) { setValueClass(max.getClass()); } this.max = max; } /** * Returns the maximum permissible value. * * @return Maximum legal value that can be input */ @SuppressWarnings("rawtypes") public Comparable getMaximum() { return max; } /** * Sets the multiplier for use in percent, per mille, and similar formats. */ public void setMultiplier(double newValue) { multiplier = newValue; } /** * Gets the multiplier for use in percent, per mille, and similar formats. */ public double getMultiplier() { return multiplier; } /** * Allows/Disallows null values. * * @param newValue */ public void setAllowsNullValue(boolean newValue) { allowsNullValue = newValue; } /** * Returns true if null values are allowed. */ public boolean getAllowsNullValue() { return allowsNullValue; } /** * Specifies whether ".0" is appended to double and float * values. By default this is true. * * @param newValue */ public void setMinimumFractionDigits(int newValue) { minFractionDigits = newValue; decimalFormat.setMinimumFractionDigits(newValue); } /** * Returns true if null values are allowed. */ public int getMinimumFractionDigits() { return minFractionDigits; } /** * Returns a String representation of the Object <code>value</code>. * This invokes <code>format</code> on the current <code>Format</code>. * * @throws ParseException if there is an error in the conversion * @param value Value to convert * @return String representation of value */ @Override public String valueToString(Object value) throws ParseException { if (value == null && allowsNullValue) { return ""; } StringBuilder buf = new StringBuilder(); if (value instanceof Double) { double v = ((Double) value).doubleValue(); v = v * multiplier; String str; BigDecimal big = new BigDecimal(v); int exponent = big.scale() >= 0 ? big.precision() - big.scale() : -big.scale(); if (!usesScientificNotation || exponent > minNegativeExponent && exponent < minPositiveExponent) { str = decimalFormat.format(v); } else { str = scientificFormat.format(v); } buf.append(str); } else if (value instanceof Float) { float v = ((Float) value).floatValue(); v = (float) (v * multiplier); String str;// = Float.toString(v); BigDecimal big = new BigDecimal(v); int exponent = big.scale() >= 0 ? big.precision() - big.scale() : -big.scale(); if (!usesScientificNotation || exponent > minNegativeExponent && exponent < minPositiveExponent) { str = decimalFormat.format(v); } else { str = scientificFormat.format(v); } buf.append(str); } else if (value instanceof Long) { long v = ((Long) value).longValue(); v = (long) (v * multiplier); buf.append(Long.toString(v)); } else if (value instanceof Integer) { int v = ((Integer) value).intValue(); v = (int) (v * multiplier); buf.append(Integer.toString(v)); } else if (value instanceof Byte) { byte v = ((Byte) value).byteValue(); v = (byte) (v * multiplier); buf.append(Byte.toString(v)); } else if (value instanceof Short) { short v = ((Short) value).shortValue(); v = (short) (v * multiplier); buf.append(Short.toString(v)); } if (buf.length() != 0) { if (unit != null) { buf.append(unit); } return buf.toString(); } throw new ParseException("Value is of unsupported class " + value, 0); } /** * Returns the <code>Object</code> representation of the * <code>String</code> <code>text</code>. * * @param text <code>String</code> to convert * @return <code>Object</code> representation of text * @throws ParseException if there is an error in the conversion */ @Override public Object stringToValue(String text) throws ParseException { if ((text == null || text.length() == 0) && getAllowsNullValue()) { return null; } // Remove unit from text if (unit != null) { int p = text.lastIndexOf(unit); if (p != -1) { text = text.substring(0, p); } } Class<?> valueClass = getValueClass(); Object value; if (valueClass != null) { try { if (valueClass == Integer.class) { int v = Integer.parseInt(text); v = (int) (v / multiplier); value = v; } else if (valueClass == Long.class) { long v = Long.parseLong(text); v = (long) (v / multiplier); value = v; } else if (valueClass == Float.class) { float v = Float.parseFloat(text); v = (float) (v / multiplier); value = new Float(v); } else if (valueClass == Double.class) { double v = Double.parseDouble(text); v = (v / multiplier); value = new Double(v); } else if (valueClass == Byte.class) { byte v = Byte.parseByte(text); v = (byte) (v / multiplier); value = v; } else if (valueClass == Short.class) { short v = Short.parseShort(text); v = (short) (v / multiplier); value = v; } else { throw new ParseException("Unsupported value class " + valueClass, 0); } } catch (NumberFormatException e) { throw new ParseException(e.getMessage(), 0); } } else { throw new ParseException("Unsupported value class " + valueClass, 0); } try { if (!isValidValue(value, true)) { throw new ParseException("Value not within min/max range", 0); } } catch (ClassCastException cce) { throw new ParseException("Class cast exception comparing values: " + cce, 0); } return value; } /** * Returns true if <code>value</code> is between the min/max. * * @param wantsCCE If false, and a ClassCastException is thrown in * comparing the values, the exception is consumed and * false is returned. */ @SuppressWarnings("unchecked") boolean isValidValue(Object value, boolean wantsCCE) { try { if (min != null && min.compareTo((Number)value) > 0) { return false; } } catch (ClassCastException cce) { if (wantsCCE) { throw cce; } return false; } try { if (max != null && max.compareTo((Number)value) < 0) { return false; } } catch (ClassCastException cce) { if (wantsCCE) { throw cce; } return false; } return true; } /** If non-null the unit string is appended to the value. */ public void setUnit(@Nullable String value) { unit = value; } /** If non-null the unit string is appended to the value. */ @Nullable public String getUnit() { return unit; } /** * Gets the minimum number of digits allowed in the integer portion of a * number. */ public int getMinimumIntegerDigits() { return minIntDigits; } /** * Sets the minimum number of digits allowed in the integer portion of a * number. */ public void setMinimumIntegerDigits(int newValue) { decimalFormat.setMinimumIntegerDigits(newValue); scientificFormat.setMinimumIntegerDigits(newValue); this.minIntDigits = newValue; } /** * Gets the maximum number of digits allowed in the integer portion of a * number. */ public int getMaximumIntegerDigits() { return maxIntDigits; } /** * Sets the maximum number of digits allowed in the integer portion of a * number. */ public void setMaximumIntegerDigits(int newValue) { decimalFormat.setMaximumIntegerDigits(newValue); scientificFormat.setMaximumIntegerDigits(newValue); this.maxIntDigits = newValue; } /** * Gets the maximum number of digits allowed in the fraction portion of a * number. */ public int getMaximumFractionDigits() { return maxFractionDigits; } /** * Sets the maximum number of digits allowed in the fraction portion of a * number. */ public void setMaximumFractionDigits(int newValue) { decimalFormat.setMaximumFractionDigits(newValue); scientificFormat.setMaximumFractionDigits(newValue); this.maxFractionDigits = newValue; } /** * Gets the minimum negative exponent value for scientific notation. */ public int getMinimumNegativeExponent() { return minNegativeExponent; } /** * Sets the minimum negative exponent value for scientific notation. */ public void setMinimumNegativeExponent(int newValue) { this.minNegativeExponent = newValue; } /** * Gets the minimum positive exponent value for scientific notation. */ public int getMinimumPositiveExponent() { return minPositiveExponent; } /** * Sets the minimum positive exponent value for scientific notation. */ public void setMinimumPositiveExponent(int newValue) { this.minPositiveExponent = newValue; } /** * Returns true if scientific notation is used. */ public boolean isUsesScientificNotation() { return usesScientificNotation; } /** * Sets whether scientific notation is used. */ public void setUsesScientificNotation(boolean newValue) { this.usesScientificNotation = newValue; } /** * Convenience method for creating a formatter factory with a * {@code ScalableNumberFormatter} and a Java-style DecimalFormat. * Doesn't allow null values and doesn't append ".0" to double and float values. */ public static AbstractFormatterFactory createFormatterFactory(double min, double max, double multiplier) { return createFormatterFactory(min, max, multiplier, false, null); } /** * Convenience method for creating a formatter factory with a * {@code ScalableNumberFormatter} and a Java-style DecimalFormat. */ public static AbstractFormatterFactory createFormatterFactory(double min, double max, double multiplier, boolean allowsNullValue) { JavaNumberFormatter formatter = new JavaNumberFormatter(min, max, multiplier, allowsNullValue, null); return new DefaultFormatterFactory(formatter); } /** * Convenience method for creating a formatter factory with a * {@code ScalableNumberFormatter} and a Java-style DecimalFormat. */ public static AbstractFormatterFactory createFormatterFactory(double min, double max, double multiplier, boolean allowsNullValue, @Nullable String unit) { JavaNumberFormatter formatter = new JavaNumberFormatter(min, max, multiplier, allowsNullValue, unit); return new DefaultFormatterFactory(formatter); } }