/* | |
* Copyright (c) 2005, 2017, Werner Keil and others. | |
* All rights reserved. This program and the accompanying materials | |
* are made available under the terms of the Eclipse Public License v1.0 | |
* which accompanies this distribution, and is available at | |
* http://www.eclipse.org/legal/epl-v10.html | |
* | |
* Contributors: | |
* Werner Keil, Eric Russell - initial API and implementation | |
*/ | |
package org.eclipse.uomo.units.impl.format; | |
import static org.eclipse.uomo.units.SI.CUBIC_METRE; | |
import static org.eclipse.uomo.units.SI.GRAM; | |
import static org.eclipse.uomo.units.SI.KILOGRAM; | |
import static org.eclipse.uomo.units.impl.system.USCustomary.LITER; | |
import java.io.IOException; | |
import java.io.StringReader; | |
import java.math.BigInteger; | |
import java.text.FieldPosition; | |
import java.text.ParsePosition; | |
import java.util.Formattable; | |
import java.util.Formatter; | |
import java.util.Locale; | |
import java.util.Map; | |
import java.util.ResourceBundle; | |
import org.eclipse.uomo.units.AbstractConverter; | |
import org.eclipse.uomo.units.AbstractConverter.Compound; | |
import org.eclipse.uomo.units.AbstractUnitFormat; | |
import org.eclipse.uomo.units.AbstractUnit; | |
import org.eclipse.uomo.units.SymbolMap; | |
import org.eclipse.uomo.units.impl.AnnotatedUnit; | |
import org.eclipse.uomo.units.impl.BaseUnit; | |
import org.eclipse.uomo.units.impl.TransformedUnit; | |
import org.eclipse.uomo.units.impl.converter.AddConverter; | |
import org.eclipse.uomo.units.impl.converter.ExpConverter; | |
import org.eclipse.uomo.units.impl.converter.LogConverter; | |
import org.eclipse.uomo.units.impl.converter.MultiplyConverter; | |
import org.eclipse.uomo.units.impl.converter.RationalConverter; | |
import org.eclipse.uomo.units.Messages; | |
import javax.measure.Unit; | |
import javax.measure.UnitConverter; | |
/** | |
* <p> | |
* This class represents the local sensitive format. | |
* </p> | |
* | |
* <h3>Here is the grammar for CommonUnits in Extended Backus-Naur Form (EBNF)</h3> | |
* <p> | |
* Note that the grammar has been left-factored to be suitable for use by a top-down parser generator such as <a | |
* href="https://javacc.dev.java.net/">JavaCC</a> | |
* </p> | |
* <table width="90%" * align="center"> | |
* <tr> | |
* <th colspan="3" align="left">Lexical Entities:</th> | |
* </tr> | |
* <tr valign="top"> | |
* <td><sign></td> | |
* <td>:=</td> | |
* <td>"+" | "-"</td> | |
* </tr> | |
* <tr valign="top"> | |
* <td><digit></td> | |
* <td>:=</td> | |
* <td>"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"</td> | |
* </tr> | |
* <tr valign="top"> | |
* <td><superscript_digit></td> | |
* <td>:=</td> | |
* <td>"⁰" | "¹" | "²" | "³" | "⁴" | "⁵" | "⁶" | "⁷" | "⁸" | "⁹"</td> | |
* </tr> | |
* <tr valign="top"> | |
* <td><integer></td> | |
* <td>:=</td> | |
* <td>(<digit>)+</td> | |
* </tr> | |
* <tr * valign="top"> | |
* <td><number></td> | |
* <td>:=</td> | |
* <td>(<sign>)? (<digit>)* (".")? (<digit>)+ (("e" | "E") (<sign>)? (<digit>)+)?</td> | |
* </tr> | |
* <tr valign="top"> | |
* <td><exponent></td> | |
* <td>:=</td> | |
* <td>( "^" ( <sign> )? <integer> ) <br> | |
* | ( "^(" (<sign>)? <integer> ( "/" (<sign>)? <integer> )? ")" ) <br> | |
* | ( <superscript_digit> )+</td> | |
* </tr> | |
* <tr valign="top"> | |
* <td><initial_char></td> | |
* <td>:=</td> | |
* <td>? Any Unicode character excluding the following: ASCII control & whitespace (\u0000 - \u0020), decimal digits '0'-'9', '(' | |
* (\u0028), ')' (\u0029), '*' (\u002A), '+' (\u002B), '-' (\u002D), '.' (\u002E), '/' (\u005C), ':' (\u003A), '^' | |
* (\u005E), '²' (\u00B2), '³' (\u00B3), '·' (\u00B7), '¹' (\u00B9), '⁰' (\u2070), '⁴' (\u2074), '⁵' (\u2075), '⁶' | |
* (\u2076), '⁷' (\u2077), '⁸' (\u2078), '⁹' (\u2079) ?</td> | |
* </tr> | |
* <tr valign="top"> | |
* <td><unit_identifier></td> | |
* <td>:=</td> | |
* <td><initial_char> ( <initial_char> | <digit> )*</td> | |
* </tr> | |
* <tr> | |
* <th colspan="3" align="left">Non-Terminals:</th> | |
* </tr> | |
* <tr * valign="top"> | |
* <td><unit_expr></td> | |
* <td>:=</td> | |
* <td><compound_expr></td> | |
* </tr> | |
* <tr valign="top"> | |
* <td><compound_expr></td> | |
* <td>:=</td> | |
* <td><add_expr> ( ":" <add_expr> )*</td> | |
* </tr> | |
* <tr valign="top"> | |
* <td><add_expr></td> | |
* <td>:=</td> | |
* <td>( <number> <sign> )? <mul_expr> ( <sign> <number> )?</td> | |
* </tr> | |
* <tr valign="top"> | |
* <td><mul_expr></td> | |
* <td>:=</td> | |
* <td><exponent_expr> ( ( ( "*" | "·" ) <exponent_expr> ) | ( "/" <exponent_expr> ) )*</td> | |
* </tr> | |
* <tr valign="top"> | |
* <td><exponent_expr></td> | |
* <td>:=</td> | |
* <td>( <atomic_expr> ( <exponent> )? ) <br> | |
* | (<integer> "^" <atomic_expr>) <br> | |
* | ( ( "log" ( <integer> )? ) | "ln" ) "(" <add_expr> ")" )</td> | |
* </tr> | |
* <tr valign="top"> | |
* <td><atomic_expr></td> | |
* <td>:=</td> | |
* <td><number> <br> | |
* | <unit_identifier> <br> | |
* | ( "(" <add_expr> ")" )</td> | |
* </tr> | |
* </table> | |
* | |
* @author <a href="mailto:eric-r@northwestern.edu">Eric Russell</a> | |
* @author <a href="mailto:units@catmedia.us">Werner Keil</a> | |
* @version 1.0.2, April 30, 2017 | |
* @since 1.0 | |
*/ | |
public class LocalUnitFormat extends AbstractUnitFormat { | |
// //////////////////////////////////////////////////// | |
// Class variables // | |
// //////////////////////////////////////////////////// | |
/** | |
* DefaultQuantityFactory locale instance. If the default locale is changed after the class is initialized, this instance will no longer be used. | |
*/ | |
private static final LocalUnitFormat DEFAULT_INSTANCE = new LocalUnitFormat(SymbolMap.of(ResourceBundle.getBundle(LocalUnitFormat.class | |
.getPackage().getName() + ".messages"))); | |
/** | |
* Multiplicand character | |
*/ | |
private static final char MIDDLE_DOT = '\u00b7'; | |
/** | |
* Operator precedence for the addition and subtraction operations | |
*/ | |
private static final int ADDITION_PRECEDENCE = 0; | |
/** | |
* Operator precedence for the multiplication and division operations | |
*/ | |
private static final int PRODUCT_PRECEDENCE = ADDITION_PRECEDENCE + 2; | |
/** | |
* Operator precedence for the exponentiation and logarithm operations | |
*/ | |
private static final int EXPONENT_PRECEDENCE = PRODUCT_PRECEDENCE + 2; | |
/** | |
* Operator precedence for a unit identifier containing no mathematical operations (i.e., consisting exclusively of an identifier and possibly a | |
* prefix). Defined to be <code>Integer.MAX_VALUE</code> so that no operator can have a higher precedence. | |
*/ | |
private static final int NOOP_PRECEDENCE = Integer.MAX_VALUE; | |
// ///////////////// | |
// Class methods // | |
// ///////////////// | |
/** | |
* Returns the instance for the current default locale (non-ascii characters are allowed) | |
*/ | |
public static LocalUnitFormat getInstance() { | |
return DEFAULT_INSTANCE; | |
} | |
/** | |
* Returns an instance for the given locale. | |
* | |
* @param locale | |
*/ | |
public static LocalUnitFormat getInstance(Locale locale) { | |
return new LocalUnitFormat(SymbolMap.of(ResourceBundle.getBundle(LocalUnitFormat.class.getPackage().getName() + ".messages", locale))); | |
} | |
/** Returns an instance for the given symbol map. */ | |
public static LocalUnitFormat getInstance(SymbolMap symbols) { | |
return new LocalUnitFormat(symbols); | |
} | |
// ////////////////////// | |
// Instance variables // | |
// ////////////////////// | |
/** | |
* The symbol map used by this instance to map between {@link Unit Unit}s and <code>String</code>s, etc... | |
*/ | |
private final transient SymbolMap symbolMap; | |
// //////////////// | |
// Constructors // | |
// //////////////// | |
/** | |
* Base constructor. | |
* | |
* @param symbols | |
* the symbol mapping. | |
*/ | |
private LocalUnitFormat(SymbolMap symbols) { | |
symbolMap = symbols; | |
} | |
// ////////////////////// | |
// Instance methods // | |
// ////////////////////// | |
/** | |
* Get the symbol map used by this instance to map between {@link AbstractUnit Unit}s and <code>String</code>s, etc... | |
* | |
* @return SymbolMap the current symbol map | |
*/ | |
@Override | |
protected SymbolMap getSymbols() { | |
return symbolMap; | |
} | |
// ////////////// | |
// Formatting // | |
// ////////////// | |
@Override | |
public Appendable format(Unit<?> unit, Appendable appendable) throws IOException { | |
if (!(unit instanceof AbstractUnit)) { | |
return appendable.append(unit.toString()); // Unknown unit (use | |
// intrinsic toString() | |
// method) | |
} | |
formatInternal(unit, appendable); | |
return appendable; | |
} | |
public boolean isLocaleSensitive() { | |
return true; | |
} | |
protected Unit<?> parse(CharSequence csq, int index) throws ParserException { | |
return parse(csq, new ParsePosition(index)); | |
} | |
public Unit<?> parse(CharSequence csq, ParsePosition cursor) throws ParserException { | |
// Parsing reads the whole character sequence from the parse position. | |
int start = cursor.getIndex(); | |
int end = csq.length(); | |
if (end <= start) { | |
return AbstractUnit.ONE; | |
} | |
String source = csq.subSequence(start, end).toString().trim(); | |
if (source.length() == 0) { | |
return AbstractUnit.ONE; | |
} | |
try { | |
LocalUnitFormatParser parser = new LocalUnitFormatParser(symbolMap, new StringReader(source)); | |
Unit<?> result = parser.parseUnit(); | |
cursor.setIndex(end); | |
return result; | |
} catch (TokenException e) { | |
if (e.currentToken != null) { | |
cursor.setErrorIndex(start + e.currentToken.endColumn); | |
} else { | |
cursor.setErrorIndex(start); | |
} | |
throw new IllegalArgumentException(e); // TODO should we throw | |
// ParserException here, | |
// too? | |
} catch (TokenMgrError e) { | |
cursor.setErrorIndex(start); | |
throw new ParserException(e); | |
} | |
} | |
@Override | |
public Unit<? extends Quantity<?>> parse(CharSequence csq) throws ParserException { | |
return parse(csq, new ParsePosition(0)); | |
} | |
/** | |
* Format the given unit to the given StringBuilder, then return the operator precedence of the outermost operator in the unit expression that was | |
* formatted. See {@link ConverterFormat} for the constants that define the various precedence values. | |
* | |
* @param unit | |
* the unit to be formatted | |
* @param buffer | |
* the <code>StringBuilder</code> to be written to | |
* @return the operator precedence of the outermost operator in the unit expression that was output | |
*/ | |
/* | |
* private int formatInternal(Unit<?> unit, Appendable buffer) throws | |
* IOException { if (unit instanceof AnnotatedUnit) { unit = | |
* ((AnnotatedUnit) unit).getActualUnit(); } String symbol = | |
* symbolMap.getSymbol((AbstractUnit<?>) unit); if (symbol != null) { | |
* buffer.append(symbol); return NOOP_PRECEDENCE; } else if | |
* (unit.getBaseUnits() != null) { Map<? extends Unit, Integer> productUnits | |
* = unit.getBaseUnits(); int negativeExponentCount = 0; // Write positive | |
* exponents first... boolean start = true; for (Unit u : | |
* productUnits.keySet()) { int pow = productUnits.get(u); if (pow >= 0) { | |
* formatExponent(u, pow, 1, !start, buffer); start = false; } else { | |
* negativeExponentCount += 1; } } // ..then write negative exponents. if | |
* (negativeExponentCount > 0) { if (start) { buffer.append('1'); } | |
* buffer.append('/'); if (negativeExponentCount > 1) { buffer.append('('); | |
* } start = true; for (Unit u : productUnits.keySet()) { int pow = | |
* productUnits.get(u); if (pow < 0) { formatExponent(u, -pow, 1, !start, | |
* buffer); start = false; } } if (negativeExponentCount > 1) { | |
* buffer.append(')'); } } return PRODUCT_PRECEDENCE; } else if | |
* ((!((AbstractUnit)unit).isSystemUnit()) || unit.equals(Units.KILOGRAM)) { | |
* UnitConverter converter = null; boolean printSeparator = false; | |
* StringBuffer temp = new StringBuffer(); int unitPrecedence = | |
* NOOP_PRECEDENCE; if (unit.equals(Units.KILOGRAM)) { // A special case | |
* because KILOGRAM is a BaseUnit instead of // a transformed unit, even | |
* though it has a prefix. converter = MetricPrefix.KILO.getConverter(); | |
* unitPrecedence = formatInternal(Units.GRAM, temp); printSeparator = true; | |
* } else { Unit parentUnit = unit.getSystemUnit(); converter = | |
* unit.getConverterTo(parentUnit); if (parentUnit.equals(Units.KILOGRAM)) { | |
* // More special-case hackery to work around gram/kilogram // incosistency | |
* parentUnit = Units.GRAM; converter = | |
* converter.concatenate(MetricPrefix.KILO.getConverter()); } unitPrecedence | |
* = formatInternal(parentUnit, temp); printSeparator = | |
* !parentUnit.equals(Units.ONE); } int result = formatConverter(converter, | |
* printSeparator, unitPrecedence, temp); buffer.append(temp); return | |
* result; } else if (unit.getSymbol() != null) { | |
* buffer.append(unit.getSymbol()); return NOOP_PRECEDENCE; } else { throw | |
* new IllegalArgumentException( | |
* "Cannot format the given Object as a Unit (unsupported unit type " + | |
* unit.getClass().getName() + ")"); } } | |
*/ | |
@SuppressWarnings({ "rawtypes", "unchecked" }) | |
private int formatInternal(Unit<?> unit, Appendable buffer) throws IOException { | |
if (unit instanceof AnnotatedUnit<?>) { | |
unit = ((AnnotatedUnit<?>) unit).getActualUnit(); | |
// } else if (unit instanceof ProductUnit<?>) { | |
// ProductUnit<?> p = (ProductUnit<?>)unit; | |
} | |
String symbol = symbolMap.getSymbol((AbstractUnit<?>) unit); | |
if (symbol != null) { | |
buffer.append(symbol); | |
return NOOP_PRECEDENCE; | |
} else if (unit.getBaseUnits() != null) { | |
Map<Unit<?>, Integer> productUnits = (Map<Unit<?>, Integer>) unit.getBaseUnits(); | |
int negativeExponentCount = 0; | |
// Write positive exponents first... | |
boolean start = true; | |
for (Map.Entry<Unit<?>, Integer> e : productUnits.entrySet()) { | |
int pow = e.getValue(); | |
if (pow >= 0) { | |
formatExponent(e.getKey(), pow, 1, !start, buffer); | |
start = false; | |
} else { | |
negativeExponentCount += 1; | |
} | |
} | |
// ..then write negative exponents. | |
if (negativeExponentCount > 0) { | |
if (start) { | |
buffer.append('1'); | |
} | |
buffer.append('/'); | |
if (negativeExponentCount > 1) { | |
buffer.append('('); | |
} | |
start = true; | |
for (Map.Entry<Unit<?>, Integer> e : productUnits.entrySet()) { | |
int pow = e.getValue(); | |
if (pow < 0) { | |
formatExponent(e.getKey(), -pow, 1, !start, buffer); | |
start = false; | |
} | |
} | |
if (negativeExponentCount > 1) { | |
buffer.append(')'); | |
} | |
} | |
return PRODUCT_PRECEDENCE; | |
} else if (unit instanceof BaseUnit<?>) { | |
buffer.append(((BaseUnit<?>) unit).getSymbol()); | |
return NOOP_PRECEDENCE; | |
} else if (unit instanceof AlternateUnit<?>) { // unit.getSymbol() != | |
// null) { // Alternate | |
// unit. | |
buffer.append(unit.getSymbol()); | |
return NOOP_PRECEDENCE; | |
} else { // A transformed unit or new unit type! | |
UnitConverter converter = null; | |
boolean printSeparator = false; | |
StringBuilder temp = new StringBuilder(); | |
int unitPrecedence = NOOP_PRECEDENCE; | |
Unit<?> parentUnit = unit.getSystemUnit(); | |
converter = ((AbstractUnit<?>) unit).getSystemConverter(); | |
if (KILOGRAM.equals(parentUnit)) { | |
// More special-case hackery to work around gram/kilogram | |
// incosistency | |
if (unit.equals(GRAM)) { | |
buffer.append(symbolMap.getSymbol(GRAM)); | |
return NOOP_PRECEDENCE; | |
} | |
parentUnit = GRAM; | |
if (unit instanceof TransformedUnit<?>) { | |
converter = ((TransformedUnit<?>) unit).getConverter(); | |
} else { | |
converter = unit.getConverterTo((Unit) GRAM); | |
} | |
} else if (CUBIC_METRE.equals(parentUnit)) { | |
if (converter != null) { | |
parentUnit = LITRE; | |
} | |
} | |
if (unit instanceof TransformedUnit) { | |
TransformedUnit<?> transUnit = (TransformedUnit<?>) unit; | |
if (parentUnit == null) | |
parentUnit = transUnit.getParentUnit(); | |
// String x = parentUnit.toString(); | |
converter = transUnit.getConverter(); | |
} | |
unitPrecedence = formatInternal(parentUnit, temp); | |
printSeparator = !parentUnit.equals(AbstractUnit.ONE); | |
int result = formatConverter(converter, printSeparator, unitPrecedence, temp); | |
buffer.append(temp); | |
return result; | |
} | |
} | |
/** | |
* Format the given unit raised to the given fractional power to the given <code>StringBuffer</code>. | |
* | |
* @param unit | |
* Unit the unit to be formatted | |
* @param pow | |
* int the numerator of the fractional power | |
* @param root | |
* int the denominator of the fractional power | |
* @param continued | |
* boolean <code>true</code> if the converter expression should begin with an operator, otherwise <code>false</code>. This will always be | |
* true unless the unit being modified is equal to Unit.ONE. | |
* @param buffer | |
* StringBuffer the buffer to append to. No assumptions should be made about its content. | |
*/ | |
private void formatExponent(Unit<?> unit, int pow, int root, boolean continued, Appendable buffer) throws IOException { | |
if (continued) { | |
buffer.append(MIDDLE_DOT); | |
} | |
StringBuffer temp = new StringBuffer(); | |
int unitPrecedence = formatInternal(unit, temp); | |
if (unitPrecedence < PRODUCT_PRECEDENCE) { | |
temp.insert(0, '('); | |
temp.append(')'); | |
} | |
buffer.append(temp); | |
if ((root == 1) && (pow == 1)) { | |
// do nothing | |
} else if ((root == 1) && (pow > 1)) { | |
String powStr = Integer.toString(pow); | |
for (int i = 0; i < powStr.length(); i += 1) { | |
char c = powStr.charAt(i); | |
switch (c) { | |
case '0': | |
buffer.append('\u2070'); | |
break; | |
case '1': | |
buffer.append('\u00b9'); | |
break; | |
case '2': | |
buffer.append('\u00b2'); | |
break; | |
case '3': | |
buffer.append('\u00b3'); | |
break; | |
case '4': | |
buffer.append('\u2074'); | |
break; | |
case '5': | |
buffer.append('\u2075'); | |
break; | |
case '6': | |
buffer.append('\u2076'); | |
break; | |
case '7': | |
buffer.append('\u2077'); | |
break; | |
case '8': | |
buffer.append('\u2078'); | |
break; | |
case '9': | |
buffer.append('\u2079'); | |
break; | |
} | |
} | |
} else if (root == 1) { | |
buffer.append("^"); | |
buffer.append(String.valueOf(pow)); | |
} else { | |
buffer.append("^("); | |
buffer.append(String.valueOf(pow)); | |
buffer.append('/'); | |
buffer.append(String.valueOf(root)); | |
buffer.append(')'); | |
} | |
} | |
/** | |
* Formats the given converter to the given StringBuffer and returns the operator precedence of the converter's mathematical operation. This is the | |
* default implementation, which supports all built-in UnitConverter implementations. Note that it recursively calls itself in the case of a | |
* {@link javax.measure.converter.UnitConverter.Compound Compound} converter. | |
* | |
* @param converter | |
* the converter to be formatted | |
* @param continued | |
* <code>true</code> if the converter expression should begin with an operator, otherwise <code>false</code>. | |
* @param unitPrecedence | |
* the operator precedence of the operation expressed by the unit being modified by the given converter. | |
* @param buffer | |
* the <code>StringBuffer</code> to append to. | |
* @return the operator precedence of the given UnitConverter | |
*/ | |
private int formatConverter(UnitConverter converter, boolean continued, int unitPrecedence, StringBuilder buffer) { | |
MetricPrefix prefix = symbolMap.getPrefix(converter); | |
if ((prefix != null) && (unitPrecedence == NOOP_PRECEDENCE)) { | |
buffer.insert(0, symbolMap.getSymbol(prefix)); | |
return NOOP_PRECEDENCE; | |
} else if (converter instanceof AddConverter) { | |
if (unitPrecedence < ADDITION_PRECEDENCE) { | |
buffer.insert(0, '('); | |
buffer.append(')'); | |
} | |
double offset = ((AddConverter) converter).getOffset(); | |
if (offset < 0) { | |
buffer.append("-"); | |
offset = -offset; | |
} else if (continued) { | |
buffer.append("+"); | |
} | |
long lOffset = (long) offset; | |
if (lOffset == offset) { | |
buffer.append(lOffset); | |
} else { | |
buffer.append(offset); | |
} | |
return ADDITION_PRECEDENCE; | |
} else if (converter instanceof MultiplyConverter) { | |
if (unitPrecedence < PRODUCT_PRECEDENCE) { | |
buffer.insert(0, '('); | |
buffer.append(')'); | |
} | |
if (continued) { | |
buffer.append(MIDDLE_DOT); | |
} | |
double factor = ((MultiplyConverter) converter).getFactor(); | |
long lFactor = (long) factor; | |
if (lFactor == factor) { | |
buffer.append(lFactor); | |
} else { | |
buffer.append(factor); | |
} | |
return PRODUCT_PRECEDENCE; | |
} else if (converter instanceof RationalConverter) { | |
if (unitPrecedence < PRODUCT_PRECEDENCE) { | |
buffer.insert(0, '('); | |
buffer.append(')'); | |
} | |
RationalConverter rationalConverter = (RationalConverter) converter; | |
if (!rationalConverter.getDividend().equals(BigInteger.ONE)) { | |
if (continued) { | |
buffer.append(MIDDLE_DOT); | |
} | |
buffer.append(rationalConverter.getDividend()); | |
} | |
if (!rationalConverter.getDivisor().equals(BigInteger.ONE)) { | |
buffer.append('/'); | |
buffer.append(rationalConverter.getDivisor()); | |
} | |
return PRODUCT_PRECEDENCE; | |
} else { // All other converter type (e.g. exponential) we use the | |
// string representation. | |
buffer.insert(0, converter.toString() + "("); | |
buffer.append(")"); | |
return EXPONENT_PRECEDENCE; | |
} | |
} | |
} |