/* * Copyright (c) 2004, JSR-108 group (http://www.jcp.org/en/jsr/detail?id=108) * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * - Neither the name of the JSR-108 nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT * SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package javax.units; // J2SE direct dependencies import java.text.FieldPosition; import java.text.Format; import java.text.ParseException; import java.text.ParsePosition; import java.util.HashMap; /** *

This is the abstract base class for all unit formats.

*

This class provides the interface for formatting and parsing * units.

*

For all {@link SI} units, the 20 SI prefixes used to form decimal * multiples and sub-multiples of SI units are recognized. * {@link NonSI} units are directly recognized. For example:

 *        Unit.valueOf("m°C") == SI.MILLI(SI.CELSIUS)
 *        Unit.valueOf("kW")  == SI.KILO(SI.WATT)
 *        Unit.valueOf("ft")  == SI.METER.multiply(0.3048)

* * @author Jean-Marie Dautelle */ public abstract class UnitFormat extends Format { //////////////////////////////////////////////////////////////////////////// // UNIT DATABASE OPERATIONS. // /** * Holds the system-wide label-unit mapping. */ private static final HashMap LABEL_UNIT = new HashMap(); /** * Holds the system-wide unit-label mapping. */ private static final HashMap UNIT_LABEL = new HashMap(); /** * Holds the system-wide alias-unit mapping. */ private static final HashMap ALIAS_UNIT = new HashMap(); /** * Attaches a system-wide label to the specified unit. This method overrides * the previous unit's label (e.g. label from unit database) as units may * only have one label (but multiple aliases). For example: *

     *     UnitFormat.label(DAY.multiply(365), "year");
     *     Unit FOOT = UnitFormat.label(METER.multiply(0.3048), "ft");
     * 
* * @param unit the unit being associated to the specified label. * @param label the new label for the specified unit or null * to detache the previous label (if any). * @return the specified unit. * @throws IllegalArgumentException if the specified label is a known symbol * or if the specified label is already attached to a different * unit (must be detached first). * @see #labelFor * @see #alias */ public static Unit label(Unit unit, String label) { // Checks label argument. if (label != null) { if (Unit.searchSymbol(label) != null) { throw new IllegalArgumentException( "Label: " + label + " is a known symbol"); } else { Unit u = (Unit) LABEL_UNIT.get(label); if ((u != null) && (u != unit)) { throw new IllegalArgumentException( "Label: " + label + " is attached to a different unit" + " (must be detached first)"); } } } // Updates unit database. synchronized (UnitFormat.class) { // Removes previous unit mapping. String prevLabel = (String) UNIT_LABEL.remove(unit); LABEL_UNIT.remove(prevLabel); // Removes previous label mapping. Unit prevUnit = (Unit) LABEL_UNIT.remove(label); UNIT_LABEL.remove(prevUnit); // Maps unit and label together. LABEL_UNIT.put(label, unit); UNIT_LABEL.put(unit, label); } return unit; } /** * Attaches a system-wide alias to the specified unit. Multiple aliases may * be attached to the same unit. Aliases are used during parsing to * recognize different variants of the same unit. For example: *

     *     UnitFormat.alias(METER.multiply(0.3048), "foot");
     *     UnitFormat.alias(METER.multiply(0.3048), "feet");
     *     UnitFormat.alias(METER, "meter");
     *     UnitFormat.alias(METER, "metre");
     * 
* * @param unit the unit being aliased. * @param alias the alias being attached to the specified unit. * @return the specified unit. * @see #unitFor */ public static Unit alias(Unit unit, String alias) { synchronized (UnitFormat.class) { ALIAS_UNIT.put(alias, unit); } return unit; } // //////////////////////////////////////////////////////////////////////////// /** * Returns the default unit format for the current default locale. * For example: * centimétre cube par kilogramme fois Ampére carré * (French locale). * * @return the unit format for the current locale. */ public final static UnitFormat getInstance() { return getStandardInstance(); } /** * Returns the standard unit format. This format is not locale sensitive * (international) and uses UTF Latin-1 Supplement * (range 0080-00FF) supported by the majority of fonts. * For example: cm³·A²/kg * * @return the standard unit format (format used by {@link Unit#toString}). */ public final static UnitFormat getStandardInstance() { return Standard.INSTANCE; } /** * Returns the ASCII unit format. This format uses characters range * 0000-007F exclusively. * For example: cm^3 kg^-1 A^2 * * @return the ASCII unit format. */ public final static UnitFormat getAsciiInstance() { return Ascii.INSTANCE; } /** * Returns the HTML unit format. This format makes use of HTML tags to * represent product units. * For example: cm<sup>3</sup>&#183;kg<sup>-1 * </sup>&#183;A<sup>2</sup> * (cm3·kg-1·A2) * * @return the HTML unit format. */ public final static UnitFormat getHtmlInstance() { return Html.INSTANCE; } /** * Determines if the specified character may be part of a unit * identifier. Any letter or symbol which cannot be mistaken for * a separator is allowed. * * @param ch the character to be tested. * @return true if the character may be part of a unit * identifier; false otherwise. */ public static boolean isUnitIdentifierPart(char ch) { return (ch > '"') && ((ch <= '%')||(ch > '?')) && (ch != '^') && (ch != '¹') && (ch != '²') && (ch != '³') && (ch != '·'); } /** * Formats a unit and appends the resulting text to a given string * buffer. * * @param obj the unit to format. * @param toAppendTo where the text is to be appended * @param pos a FieldPosition identifying a field * in the formatted text (not used). * @return the string buffer passed in as toAppendTo, * with formatted text appended. * @throws NullPointerException if toAppendTo is * null * @throws IllegalArgumentException if this format cannot format the given * object (e.g. not a Unit instance). */ public abstract StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos); /** * Parses text from a string to produce an object. * * @param source a String, part of which should be parsed. * @param pos a ParsePosition object with index and error * index information. * @return an Object parsed from the string. In case of * error, returns null. * @see #parseUnit */ public final Object parseObject(String source, ParsePosition pos) { try { int start = pos.getIndex(); Unit unit = parseUnit(source.substring(start)); pos.setIndex(source.length()); // Parsing uses all characters up to // the end of the string. return unit; } catch (ParseException e) { pos.setErrorIndex(e.getErrorOffset()); return null; } } /** * Parses text from a character sequence to produce a unit. *

The expected form of the character sequence is: * {<name>{<power>{<root>}}} *

For examples: *

*

HTML tags are ignored (e.g. <sup>...</sup>).

*

Escape sequences are considered as separators (e.g. &#183;).

*

"/" (if not placed between two numbers) indicates ALL the following * unit exponents will be multiplied by -1; multiple * instances of "/" in a line will result in successive * implicit multiplications by -1 * of all the exponents that follow (e.g. "s/s/s" * is equivalent to "s").

* * @param source the characters sequence to be parsed. * @return a Unit parsed from the string. * @throws ParseException if a parsing error occurs. */ public Unit parseUnit(CharSequence source) throws ParseException { // This method uses local variables only and therefore is thread-safe. int state = DEFAULT; CharSequence name = null; CharSequence power = null; CharSequence root = null; boolean isInverse = false; boolean isSlash = false; boolean requestFlush = false; int start = 0; // Start index for the relevant state. int bracketLevel = 0; // For nested [] Unit result = Unit.ONE; for (int i=0; i <= source.length(); i++) { // Adds an extra iteration to flush all buffers when done. // Otherwise buffers would be flushed only when a new unit is // encountered and the last unit would be ignored. char c = (i < source.length()) ? source.charAt(i) : ' '; switch (state) { case DEFAULT: if (c == '[') { start = i; state = SYSTEM_NAME; requestFlush = true; // Flush previous unit (if any) } else if (isUnitIdentifierPart(c)) { start = i; state = NAME; requestFlush = true; // Flush previous unit (if any) } else if (isExponent(c)) { start = i; state = EXPONENT; } else if (c == '<') { state = MARKUP; } else if (c == '&') { state = ESCAPE; } break; case SYSTEM_NAME: if (c == '[') { bracketLevel++; } else if ( (c == ']') && (bracketLevel == 0)) { name = source.subSequence(start, i+1); state = DEFAULT; } else if (c == ']') { bracketLevel--; } break; case NAME: if (!isUnitIdentifierPart(c)) { name = source.subSequence(start, i); if (isExponent(c)) { start = i; state = EXPONENT; } else if (c == '<') { state = MARKUP; } else if (c == '&') { state = ESCAPE; } else { state = DEFAULT; } } break; case EXPONENT: if (!isExponent(c)) { if (power == null) { // First exponent is power. power = source.subSequence(start, i); } else { // Second is root. isSlash = false; // Reset potential slash (ratio) root = source.subSequence(start, i); } if (c == '[') { start = i; state = SYSTEM_NAME; requestFlush = true; // Flush previous unit (if any) } else if (isUnitIdentifierPart(c)) { start = i; state = NAME; requestFlush = true; // Flush previous unit (if any) } else if (c == '<') { state = MARKUP; } else if (c == '&') { state = ESCAPE; } else { state = DEFAULT; } } break; case MARKUP: if (c == '>') { state = DEFAULT; } break; case ESCAPE: if (c == ';') { state = DEFAULT; } break; default: throw new InternalError("state " + state + " unknown"); } // Check for slash. if ( (c == '/') && (state == DEFAULT)) { isSlash = true; } // Flushes buffers when done. if (i == source.length()) { if (state == NAME) { name = source.subSequence(start, source.length()); } else if (state == EXPONENT) { if (power == null) { power = source.subSequence(start, source.length()); } else { root = source.subSequence(start, source.length()); } } requestFlush = true; } // Flushes previous unit to the result (multiply). if (requestFlush) { if (name != null) { Unit unit = unitFor(name); if (unit != null) { int powValue = parseExponent(power); int rootValue = parseExponent(root); powValue = isInverse ? -powValue : powValue; result = result.multiply( unit.pow(powValue).root(rootValue)); } else { // Label not recognized. throw new ParseException( "Label: " + name + " not recognized", i); } } isInverse = (isSlash) ? (!isInverse) : isInverse; // Resets all. requestFlush = false; name = null; power = null; root = null; isSlash = false; } } return result; } private static boolean isExponent(char c) { return Character.isDigit(c) || (c == '-') || (c == '¹') || (c == '²') || (c == '³'); } private static int parseExponent(CharSequence chars) { if (chars != null) { boolean isNegative = false; int exp = 0; for (int i=0; i < chars.length(); i++) { char c = chars.charAt(i); if (c == '-') { isNegative = true; continue; } else if (c == '+') { continue; } else if (c == '¹') { c = '1'; } else if (c == '²') { c = '2'; } else if (c == '³') { c = '3'; } exp = exp * 10 + (c - '0'); } return isNegative ? - exp : exp; } else { return 1; } } private static final int DEFAULT = 0; private static final int SYSTEM_NAME = 1; private static final int NAME = 2; private static final int EXPONENT = 3; private static final int MARKUP = 4; private static final int ESCAPE = 5; /** * Returns the label for the specified unit. The default behavior of * this method (which may be overridden) is first to search the label * database. * * @param unit the unit to format. * @return the database label, the unit's symbol, some other representation * of the specified unit (e.g. [K+273.15], [m*0.01]) or * null for units with custom converters or * {@link ProductUnit} with no label. * @see #unitFor */ public String labelFor(Unit unit) { // Label database. synchronized (UnitFormat.class) { String label = (String) UNIT_LABEL.get(unit); if (label != null) { return label; } } // Symbol. if (unit._symbol != null) { return unit._symbol; } // Transformed unit. if (unit instanceof TransformedUnit) { TransformedUnit tfmUnit = (TransformedUnit) unit; Unit systemUnit = tfmUnit.getSystemUnit(); Converter cvtr = tfmUnit.getConverterTo(systemUnit); if (cvtr instanceof AddConverter) { return "[" + systemUnit + "+" + ((AddConverter)cvtr).getOffset() + "]"; } else if (cvtr instanceof MultiplyConverter) { return "[" + systemUnit + "*" + ((MultiplyConverter)cvtr).derivative(0) + "]"; } else { // Custom converters. return null; } } return null; } /** * Returns the unit identified by the specified label. The default behavior * of this method (which may be overridden) is first to search the label * database, then the alias database and finally the symbols collection. * * @param label the label, alias, or symbol identifying the unit. * @return the unit identified by the specified label or * null if the identification fails. * @see #labelFor */ public Unit unitFor(CharSequence label) { synchronized (UnitFormat.class) { // Label database. Unit unit = (Unit) LABEL_UNIT.get(label); if (unit != null) { return unit; } // Alias database. unit = (Unit) ALIAS_UNIT.get(label); if (unit != null) { return unit; } } // Symbol. Unit unit = Unit.searchSymbol(label); if (unit != null) { return unit; } return null; } /** * This inner class represents the standard unit format. */ private static final class Standard extends UnitFormat { /** * Holds the default instance. */ private static final Standard INSTANCE = new Standard(); // Implements abstract method. public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { if (obj instanceof Unit) { String label = labelFor((Unit)obj); if (label != null) { toAppendTo.append(label); return toAppendTo; // Done. } } if (obj instanceof ProductUnit) { ProductUnit unit = (ProductUnit)obj; int invNbr = 0; // Write positive exponents first. int startIndex = toAppendTo.length(); for (int i=0; i < unit.size(); i++) { String label = labelFor(unit.get(i).getUnit()); int pow = unit.get(i).getPow(); int root = unit.get(i).getRoot(); if (pow >= 0) { if (startIndex != toAppendTo.length()) { toAppendTo.append('·'); // Separator. } append(toAppendTo, label, pow, root); } else { invNbr++; } } // Write negative exponents. if (invNbr != 0) { if (startIndex == toAppendTo.length()) { toAppendTo.append('1'); // e.g. 1/s } toAppendTo.append('/'); if (invNbr > 1) { toAppendTo.append('('); } startIndex = toAppendTo.length(); for (int i=0; i < unit.size(); i++) { String label = labelFor(unit.get(i).getUnit()); int pow = unit.get(i).getPow(); int root = unit.get(i).getRoot(); if (pow < 0) { if (startIndex != toAppendTo.length()) { toAppendTo.append('·'); // Separator. } append(toAppendTo, label, -pow, root); } } if (invNbr > 1) { toAppendTo.append(')'); } } } else { throw new IllegalArgumentException( "Cannot format given Object as a Unit"); } return toAppendTo; } } private static void append(StringBuffer str, String symbol, int pow, int root) { str.append(symbol); if ((pow != 1) || (root != 1)) { // Write exponent. if ((pow == 2) && (root == 1)) { str.append('²'); // Square } else if ((pow == 3) && (root == 1)) { str.append('³'); // Cubic } else { // Use general exponent form. str.append("^" + String.valueOf(pow)); if (root != 1) { str.append(':' + String.valueOf(root)); } } } } /** * This inner class represents the HTML unit format. */ private static final class Html extends UnitFormat { /** * Holds the default instance. */ private static final Html INSTANCE = new Html(); // Implements abstract method. public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { if (obj instanceof Unit) { String label = labelFor((Unit)obj); if (label != null) { toAppendTo.append(label); return toAppendTo; // Done. } } if (obj instanceof ProductUnit) { ProductUnit unit = (ProductUnit)obj; int startIndex = toAppendTo.length(); for (int i=0; i < unit.size(); i++) { if (startIndex != toAppendTo.length()) { toAppendTo.append("·"); // Separator. } String label = labelFor(unit.get(i).getUnit()); int pow = unit.get(i).getPow(); int root = unit.get(i).getRoot(); toAppendTo.append(label); if ((pow != 1) || (root != 1)) { // Write exponent. toAppendTo.append("" + String.valueOf(pow)); if (root != 1) { toAppendTo.append(":" + String.valueOf(root)); } toAppendTo.append(""); } } } else { throw new IllegalArgumentException( "Cannot format given Object as a Unit"); } return toAppendTo; } } /** * This inner class represents the ASCII unit format. */ private static final class Ascii extends UnitFormat { /** * Holds the default instance. */ private static final Ascii INSTANCE = new Ascii(); // Implements abstract method. public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { if (obj instanceof Unit) { String label = labelFor((Unit)obj); if (label != null) { toAppendTo.append(label); return toAppendTo; // Done. } } if (obj instanceof ProductUnit) { ProductUnit unit = (ProductUnit)obj; int startIndex = toAppendTo.length(); for (int i=0; i < unit.size(); i++) { if (startIndex != toAppendTo.length()) { toAppendTo.append(" "); // Separator. } String label = labelFor(unit.get(i).getUnit()); int pow = unit.get(i).getPow(); int root = unit.get(i).getRoot(); toAppendTo.append(label); if ((pow != 1) || (root != 1)) { // Write exponent. toAppendTo.append("^" + String.valueOf(pow)); if (root != 1) { toAppendTo.append(":" + String.valueOf(root)); } } } } else { throw new IllegalArgumentException( "Cannot format given Object as a Unit"); } return toAppendTo; } } static { // Force SI/NonSI initialization (load unit database). try { Class.forName("javax.units.SI"); Class.forName("javax.units.NonSI"); } catch (Exception e) { e.printStackTrace(); } } }