001/* 002 * Units of Measurement Reference Implementation 003 * Copyright (c) 2005-2023, Jean-Marie Dautelle, Werner Keil, Otavio Santana. 004 * 005 * All rights reserved. 006 * 007 * Redistribution and use in source and binary forms, with or without modification, 008 * are permitted provided that the following conditions are met: 009 * 010 * 1. Redistributions of source code must retain the above copyright notice, 011 * this list of conditions and the following disclaimer. 012 * 013 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions 014 * and the following disclaimer in the documentation and/or other materials provided with the distribution. 015 * 016 * 3. Neither the name of JSR-385, Indriya nor the names of their contributors may be used to endorse or promote products 017 * derived from this software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 020 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 021 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 022 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 023 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 026 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 028 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030package tech.units.indriya.format; 031 032import static tech.units.indriya.format.ConverterFormatter.formatConverterLocal; 033import static tech.units.indriya.format.FormatConstants.*; 034 035import javax.measure.Quantity; 036import javax.measure.Unit; 037import javax.measure.UnitConverter; 038import javax.measure.format.MeasurementParseException; 039import tech.units.indriya.AbstractUnit; 040import tech.units.indriya.internal.format.UnitFormatParser; 041import tech.units.indriya.unit.AlternateUnit; 042import tech.units.indriya.unit.AnnotatedUnit; 043import tech.units.indriya.unit.BaseUnit; 044import tech.units.indriya.unit.ProductUnit; 045import tech.units.indriya.unit.TransformedUnit; 046 047import static tech.units.indriya.unit.Units.CUBIC_METRE; 048import static tech.units.indriya.unit.Units.GRAM; 049import static tech.units.indriya.unit.Units.KILOGRAM; 050import static tech.units.indriya.unit.Units.LITRE; 051 052import java.io.IOException; 053import java.io.StringReader; 054import java.text.ParsePosition; 055import java.util.Locale; 056import java.util.Map; 057import java.util.ResourceBundle; 058 059/** 060 * <p> 061 * This class represents the local sensitive format. 062 * </p> 063 * 064 * <h3>Here is the grammar for CommonUnits in Extended Backus-Naur Form (EBNF)</h3> 065 * <p> 066 * Note that the grammar has been left-factored to be suitable for use by a top-down parser generator such as <a 067 * href="https://javacc.dev.java.net/">JavaCC</a> 068 * </p> 069 * <table width="90%" * align="center"> 070 * <tr> 071 * <th colspan="3" align="left">Lexical Entities:</th> 072 * </tr> 073 * <tr valign="top"> 074 * <td><sign></td> 075 * <td>:=</td> 076 * <td>"+" | "-"</td> 077 * </tr> 078 * <tr valign="top"> 079 * <td><digit></td> 080 * <td>:=</td> 081 * <td>"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"</td> 082 * </tr> 083 * <tr valign="top"> 084 * <td><superscript_digit></td> 085 * <td>:=</td> 086 * <td>"⁰" | "¹" | "²" | "³" | "⁴" | "⁵" | "⁶" | "⁷" | "⁸" | "⁹"</td> 087 * </tr> 088 * <tr valign="top"> 089 * <td><integer></td> 090 * <td>:=</td> 091 * <td>(<digit>)+</td> 092 * </tr> 093 * <tr * valign="top"> 094 * <td><number></td> 095 * <td>:=</td> 096 * <td>(<sign>)? (<digit>)* (".")? (<digit>)+ (("e" | "E") (<sign>)? (<digit>)+)?</td> 097 * </tr> 098 * <tr valign="top"> 099 * <td><exponent></td> 100 * <td>:=</td> 101 * <td>( "^" ( <sign> )? <integer> ) <br> 102 * | ( "^(" (<sign>)? <integer> ( "/" (<sign>)? <integer> )? ")" ) <br> 103 * | ( <superscript_digit> )+</td> 104 * </tr> 105 * <tr valign="top"> 106 * <td><initial_char></td> 107 * <td>:=</td> 108 * <td>? Any Unicode character excluding the following: ASCII control & whitespace (\u0000 - \u0020), decimal digits '0'-'9', '(' 109 * (\u0028), ')' (\u0029), '*' (\u002A), '+' (\u002B), '-' (\u002D), '.' (\u002E), '/' (\u005C), ':' (\u003A), '^' 110 * (\u005E), '²' (\u00B2), '³' (\u00B3), '·' (\u00B7), '¹' (\u00B9), '⁰' (\u2070), '⁴' (\u2074), '⁵' (\u2075), '⁶' 111 * (\u2076), '⁷' (\u2077), '⁸' (\u2078), '⁹' (\u2079) ?</td> 112 * </tr> 113 * <tr valign="top"> 114 * <td><unit_identifier></td> 115 * <td>:=</td> 116 * <td><initial_char> ( <initial_char> | <digit> )*</td> 117 * </tr> 118 * <tr> 119 * <th colspan="3" align="left">Non-Terminals:</th> 120 * </tr> 121 * <tr * valign="top"> 122 * <td><unit_expr></td> 123 * <td>:=</td> 124 * <td><mix_expr></td> 125 * </tr> 126 * <tr valign="top"> 127 * <td><mix_expr></td> 128 * <td>:=</td> 129 * <td><add_expr> ( ":" <add_expr> )*</td> 130 * </tr> 131 * <tr valign="top"> 132 * <td><add_expr></td> 133 * <td>:=</td> 134 * <td>( <number> <sign> )? <mul_expr> ( <sign> <number> )?</td> 135 * </tr> 136 * <tr valign="top"> 137 * <td><mul_expr></td> 138 * <td>:=</td> 139 * <td><exponent_expr> ( ( ( "*" | "·" ) <exponent_expr> ) | ( "/" <exponent_expr> ) )*</td> 140 * </tr> 141 * <tr valign="top"> 142 * <td><exponent_expr></td> 143 * <td>:=</td> 144 * <td>( <atomic_expr> ( <exponent> )? ) <br> 145 * | (<integer> "^" <atomic_expr>) <br> 146 * | ( ( "log" ( <integer> )? ) | "ln" ) "(" <add_expr> ")" )</td> 147 * </tr> 148 * <tr valign="top"> 149 * <td><atomic_expr></td> 150 * <td>:=</td> 151 * <td><number> <br> 152 * | <unit_identifier> <br> 153 * | ( "(" <add_expr> ")" )</td> 154 * </tr> 155 * </table> 156 * 157 * @author <a href="mailto:eric-r@northwestern.edu">Eric Russell</a> 158 * @author <a href="mailto:werner@units.tech">Werner Keil</a> 159 * @version 1.4, March 3, 2020 160 * @since 1.0 161 */ 162public class LocalUnitFormat extends AbstractUnitFormat { 163 164 ////////////////////////////////////////////////////// 165 // Class variables // 166 ////////////////////////////////////////////////////// 167 /** 168 * DefaultQuantityFactory locale instance. If the default locale is changed after the class is initialized, this instance will no longer be used. 169 */ 170 private static final LocalUnitFormat DEFAULT_INSTANCE = new LocalUnitFormat(SymbolMap.of(ResourceBundle.getBundle(LocalUnitFormat.class 171 .getPackage().getName() + ".messages"))); 172 173 // ///////////////// 174 // Class methods // 175 // ///////////////// 176 /** 177 * Returns the instance for the current default locale (non-ascii characters are allowed) 178 */ 179 public static LocalUnitFormat getInstance() { 180 return DEFAULT_INSTANCE; 181 } 182 183 /** 184 * Returns an instance for the given locale. 185 * 186 * @param locale the locale to use 187 */ 188 public static LocalUnitFormat getInstance(Locale locale) { 189 return new LocalUnitFormat(SymbolMap.of(ResourceBundle.getBundle(LocalUnitFormat.class.getPackage().getName() + ".messages", locale))); 190 } 191 192 /** Returns an instance for the given symbol map. */ 193 public static LocalUnitFormat getInstance(SymbolMap symbols) { 194 return new LocalUnitFormat(symbols); 195 } 196 197 // ////////////////////// 198 // Instance variables // 199 // ////////////////////// 200 /** 201 * The symbol map used by this instance to map between {@link Unit Unit}s and <code>String</code>s, etc... 202 */ 203 private final transient SymbolMap symbolMap; 204 205 // //////////////// 206 // Constructors // 207 // //////////////// 208 /** 209 * Base constructor. 210 * 211 * @param symbols 212 * the symbol mapping. 213 */ 214 private LocalUnitFormat(SymbolMap symbols) { 215 symbolMap = symbols; 216 } 217 218 //////////////////////// 219 // Instance methods // 220 //////////////////////// 221 /** 222 * Get the symbol map used by this instance to map between {@link AbstractUnit Unit}s and <code>String</code>s, etc... 223 * 224 * @return SymbolMap the current symbol map 225 */ 226 protected SymbolMap getSymbols() { 227 return symbolMap; 228 } 229 230 @Override 231 public String toString() { 232 return getClass().getSimpleName(); 233 } 234 235 //////////////// 236 // Formatting // 237 //////////////// 238 @Override 239 public Appendable format(Unit<?> unit, Appendable appendable) throws IOException { 240 if (!(unit instanceof AbstractUnit)) { 241 return appendable.append(unit.toString()); // Unknown unit (use 242 // intrinsic toString() 243 // method) 244 } 245 formatInternal(unit, appendable); 246 return appendable; 247 } 248 249 public boolean isLocaleSensitive() { 250 return true; 251 } 252 253 protected Unit<?> parse(CharSequence csq, int index) throws MeasurementParseException { 254 return parse(csq, new ParsePosition(index)); 255 } 256 257 public Unit<?> parse(CharSequence csq, ParsePosition cursor) throws MeasurementParseException { 258 // Parsing reads the whole character sequence from the parse position. 259 int start = cursor.getIndex(); 260 int end = csq.length(); 261 if (end <= start) { 262 return AbstractUnit.ONE; 263 } 264 String source = csq.subSequence(start, end).toString().trim(); 265 if (source.length() == 0) { 266 return AbstractUnit.ONE; 267 } 268 try { 269 UnitFormatParser parser = new UnitFormatParser(symbolMap, new StringReader(source)); 270 Unit<?> result = parser.parseUnit(); 271 cursor.setIndex(end); 272 return result; 273 } catch (TokenException e) { 274 if (e.currentToken != null) { 275 cursor.setErrorIndex(start + e.currentToken.endColumn); 276 } else { 277 cursor.setErrorIndex(start); 278 } 279 throw new IllegalArgumentException(e); // TODO should we throw 280 // ParserException here, 281 // too? 282 } catch (TokenMgrError e) { 283 cursor.setErrorIndex(start); 284 throw new MeasurementParseException(e); 285 } 286 } 287 288 @Override 289 public Unit<? extends Quantity<?>> parse(CharSequence csq) throws MeasurementParseException { 290 return parse(csq, new ParsePosition(0)); 291 } 292 293 /** 294 * Format the given unit to the given StringBuilder, then return the operator precedence of the outermost operator in the unit expression that was 295 * formatted. See {@link ConverterFormat} for the constants that define the various precedence values. 296 * 297 * @param unit 298 * the unit to be formatted 299 * @param buffer 300 * the <code>StringBuilder</code> to be written to 301 * @return the operator precedence of the outermost operator in the unit expression that was output 302 */ 303 @SuppressWarnings({ "rawtypes", "unchecked" }) 304 private int formatInternal(Unit<?> unit, Appendable buffer) throws IOException { 305 if (unit instanceof AnnotatedUnit<?>) { 306 unit = ((AnnotatedUnit<?>) unit).getActualUnit(); 307 // } else if (unit instanceof ProductUnit<?>) { 308 // ProductUnit<?> p = (ProductUnit<?>)unit; 309 } 310 // TODO is the behavior similar to EBNFUnitFormat for AnnotatedUnit? 311 String symbol = symbolMap.getSymbol((AbstractUnit<?>) unit); 312 if (symbol != null) { 313 buffer.append(symbol); 314 return NOOP_PRECEDENCE; 315 } else if (unit instanceof ProductUnit<?> && unit.getBaseUnits() != null) { 316 Map<Unit<?>, Integer> productUnits = (Map<Unit<?>, Integer>) unit.getBaseUnits(); 317 int negativeExponentCount = 0; 318 // Write positive exponents first... 319 boolean start = true; 320 for (Map.Entry<Unit<?>, Integer> e : productUnits.entrySet()) { 321 int pow = e.getValue(); 322 if (pow >= 0) { 323 formatExponent(e.getKey(), pow, 1, !start, buffer); 324 start = false; 325 } else { 326 negativeExponentCount += 1; 327 } 328 } 329 // ..then write negative exponents. 330 if (negativeExponentCount > 0) { 331 if (start) { 332 buffer.append('1'); 333 } 334 buffer.append('/'); 335 if (negativeExponentCount > 1) { 336 buffer.append('('); 337 } 338 start = true; 339 for (Map.Entry<Unit<?>, Integer> e : productUnits.entrySet()) { 340 int pow = e.getValue(); 341 if (pow < 0) { 342 formatExponent(e.getKey(), -pow, 1, !start, buffer); 343 start = false; 344 } 345 } 346 if (negativeExponentCount > 1) { 347 buffer.append(')'); 348 } 349 } 350 return PRODUCT_PRECEDENCE; 351 } else if (unit instanceof BaseUnit<?>) { 352 buffer.append(((BaseUnit<?>) unit).getSymbol()); 353 return NOOP_PRECEDENCE; 354 } else if (unit instanceof AlternateUnit<?>) { // unit.getSymbol() != 355 // null) { // Alternate 356 // unit. 357 buffer.append(unit.getSymbol()); 358 return NOOP_PRECEDENCE; 359 // TODO add case for MixedUnit 360 } else { // A transformed unit or new unit type! 361 UnitConverter converter = null; 362 boolean printSeparator = false; 363 StringBuilder temp = new StringBuilder(); 364 int unitPrecedence = NOOP_PRECEDENCE; 365 Unit<?> parentUnit = unit.getSystemUnit(); 366 converter = ((AbstractUnit<?>) unit).getSystemConverter(); 367 if (KILOGRAM.equals(parentUnit)) { 368 // More special-case hackery to work around gram/kilogram 369 // incosistency 370 if (unit.equals(GRAM)) { 371 buffer.append(symbolMap.getSymbol(GRAM)); 372 return NOOP_PRECEDENCE; 373 } 374 parentUnit = GRAM; 375 if (unit instanceof TransformedUnit<?>) { 376 converter = ((TransformedUnit<?>) unit).getConverter(); 377 } else { 378 converter = unit.getConverterTo((Unit) GRAM); 379 } 380 } else if (CUBIC_METRE.equals(parentUnit)) { 381 if (converter != null) { 382 parentUnit = LITRE; 383 } 384 } 385 386 if (unit instanceof TransformedUnit) { 387 TransformedUnit<?> transUnit = (TransformedUnit<?>) unit; 388 if (parentUnit == null) 389 parentUnit = transUnit.getParentUnit(); 390 // String x = parentUnit.toString(); 391 converter = transUnit.getConverter(); 392 } 393 394 unitPrecedence = formatInternal(parentUnit, temp); 395 printSeparator = !parentUnit.equals(AbstractUnit.ONE); 396 int result = formatConverterLocal(converter, printSeparator, unitPrecedence, temp, symbolMap); 397 buffer.append(temp); 398 return result; 399 } 400 } 401 402 /** 403 * Format the given unit raised to the given fractional power to the given <code>StringBuffer</code>. 404 * 405 * @param unit 406 * Unit the unit to be formatted 407 * @param pow 408 * int the numerator of the fractional power 409 * @param root 410 * int the denominator of the fractional power 411 * @param continued 412 * boolean <code>true</code> if the converter expression should begin with an operator, otherwise <code>false</code>. This will always be 413 * true unless the unit being modified is equal to Unit.ONE. 414 * @param buffer 415 * StringBuffer the buffer to append to. No assumptions should be made about its content. 416 */ 417 private void formatExponent(Unit<?> unit, int pow, int root, boolean continued, Appendable buffer) throws IOException { 418 if (continued) { 419 buffer.append(MIDDLE_DOT); 420 } 421 StringBuffer temp = new StringBuffer(); 422 int unitPrecedence = formatInternal(unit, temp); 423 if (unitPrecedence < PRODUCT_PRECEDENCE) { 424 temp.insert(0, '('); 425 temp.append(')'); 426 } 427 buffer.append(temp); 428 if ((root == 1) && (pow == 1)) { 429 // do nothing 430 } else if ((root == 1) && (pow > 1)) { 431 String powStr = Integer.toString(pow); 432 for (int i = 0; i < powStr.length(); i += 1) { 433 char c = powStr.charAt(i); 434 switch (c) { 435 case '0': 436 buffer.append('\u2070'); 437 break; 438 case '1': 439 buffer.append('\u00b9'); 440 break; 441 case '2': 442 buffer.append('\u00b2'); 443 break; 444 case '3': 445 buffer.append('\u00b3'); 446 break; 447 case '4': 448 buffer.append('\u2074'); 449 break; 450 case '5': 451 buffer.append('\u2075'); 452 break; 453 case '6': 454 buffer.append('\u2076'); 455 break; 456 case '7': 457 buffer.append('\u2077'); 458 break; 459 case '8': 460 buffer.append('\u2078'); 461 break; 462 case '9': 463 buffer.append('\u2079'); 464 break; 465 } 466 } 467 } else if (root == 1) { 468 buffer.append("^"); 469 buffer.append(String.valueOf(pow)); 470 } else { 471 buffer.append("^("); 472 buffer.append(String.valueOf(pow)); 473 buffer.append('/'); 474 buffer.append(String.valueOf(root)); 475 buffer.append(')'); 476 } 477 } 478}