001/* 002 * Units of Measurement Reference Implementation 003 * Copyright (c) 2005-2025, 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 java.io.IOException; 033import java.text.FieldPosition; 034import java.text.ParsePosition; 035import java.util.HashMap; 036import java.util.Map; 037import java.util.stream.Collectors; 038import java.util.stream.Stream; 039 040import javax.measure.BinaryPrefix; 041import javax.measure.MeasurementError; 042import javax.measure.MetricPrefix; 043import javax.measure.Prefix; 044import javax.measure.Quantity; 045import javax.measure.Unit; 046import javax.measure.UnitConverter; 047import javax.measure.format.MeasurementParseException; 048import javax.measure.format.UnitFormat; 049 050import static javax.measure.MetricPrefix.MICRO; 051 052import tech.units.indriya.AbstractUnit; 053import tech.units.indriya.function.AddConverter; 054import tech.units.indriya.function.MultiplyConverter; 055import tech.units.indriya.function.RationalNumber; 056import tech.units.indriya.unit.AlternateUnit; 057import tech.units.indriya.unit.AnnotatedUnit; 058import tech.units.indriya.unit.BaseUnit; 059import tech.units.indriya.unit.ProductUnit; 060import tech.units.indriya.unit.TransformedUnit; 061import tech.units.indriya.unit.Units; 062 063import static tech.units.indriya.format.FormatConstants.MIDDLE_DOT; 064 065/** 066 * <p> 067 * This class implements the {@link UnitFormat} interface for formatting and parsing {@link Unit units}. 068 * </p> 069 * 070 * <p> 071 * For all SI units, the <b>24 SI prefixes</b> used to form decimal multiples and sub-multiples are recognized. As well as the <b>8 binary prefixes</b>.<br> 072 * {@link Units} are directly recognized. For example:<br> 073 * <code> 074 * UnitFormat format = SimpleUnitFormat.getInstance();<br> 075 * format.parse("m°C").equals(MetricPrefix.MILLI(Units.CELSIUS));<br> 076 * format.parse("kW").equals(MetricPrefix.KILO(Units.WATT));<br> 077 * format.parse("ft").equals(Units.METRE.multiply(0.3048))</code> 078 * </p> 079 * 080 * @author <a href="mailto:jean-marie@dautelle.com">Jean-Marie Dautelle</a> 081 * @author <a href="mailto:werner@units.tech">Werner Keil</a> 082 * @author Eric Russell 083 * @author Andi Huber 084 * @version 2.19, Mar 29, 2025 085 * @since 1.0 086 */ 087public abstract class SimpleUnitFormat extends AbstractUnitFormat { 088 // private static final long serialVersionUID = 4149424034841739785L; 089 090 /** 091 * Flavor of this format 092 * 093 * @author Werner 094 * 095 */ 096 public static enum Flavor { 097 /** @deprecated use DEFAULT */ 098 Default, 099 /** The default format flavor */ 100 DEFAULT, 101 /** The ASCII_INSTANCE format flavor */ 102 ASCII 103 } 104 105 private static final String MU = "\u03bc"; 106 107 /** 108 * Holds the default format instance. 109 */ 110 private static final DefaultFormat DEFAULT_INSTANCE = new DefaultFormat().init(); 111 112 /** 113 * Holds the ASCII_INSTANCE format instance. 114 */ 115 private static final ASCIIFormat ASCII_INSTANCE = new ASCIIFormat().init(); 116 117 /** 118 * Returns the globally shared unit format instance (used by {@link AbstractUnit#parse(CharSequence) AbstractUnit.parse()} and 119 * {@link AbstractUnit#toString() AbstractUnit.toString()}). 120 * 121 * @return the default unit format. 122 */ 123 public static SimpleUnitFormat getInstance() { 124 return getInstance(Flavor.DEFAULT); 125 } 126 127 /** 128 * Returns the {@link SimpleUnitFormat} in the desired {@link Flavor} 129 * 130 * @return the instance for the given {@link Flavor}. 131 */ 132 public static SimpleUnitFormat getInstance(Flavor flavor) { 133 switch (flavor) { 134 case ASCII: 135 return SimpleUnitFormat.ASCII_INSTANCE; 136 default: 137 return DEFAULT_INSTANCE; 138 } 139 } 140 141 /** 142 * Similar to {@link #getInstance()}, but returns a new, non-shared unit format instance, 143 * instead of a shared singleton instance. 144 * 145 * @return a new instance of the default unit format. 146 * @see #getInstance() 147 * @since 2.7 148 */ 149 public static SimpleUnitFormat getNewInstance() { 150 return getNewInstance(Flavor.DEFAULT); 151 } 152 153 /** 154 * Similar to {@link #getInstance(Flavor)}, but returns a new {@link SimpleUnitFormat} instance in the desired 155 * {@link Flavor}, instead of a shared singleton instance. 156 * 157 * @return a new instance for the given {@link Flavor}. 158 * @see #getInstance(Flavor) 159 * @since 2.7 160 */ 161 public static SimpleUnitFormat getNewInstance(Flavor flavor) { 162 switch (flavor) { 163 case ASCII: 164 return new ASCIIFormat().init(); 165 default: 166 return new DefaultFormat().init(); 167 } 168 } 169 170 /** 171 * Base constructor. 172 */ 173 protected SimpleUnitFormat() { 174 } 175 176 /** 177 * Formats the specified unit. 178 * 179 * @param unit 180 * the unit to format. 181 * @param appendable 182 * the appendable destination. 183 * @throws IOException 184 * if an error occurs. 185 */ 186 public abstract Appendable format(Unit<?> unit, Appendable appendable) throws IOException; 187 188 /** 189 * Parses a sequence of character to produce a unit or a rational product of unit. 190 * 191 * @param csq 192 * the <code>CharSequence</code> to parse. 193 * @param pos 194 * an object holding the parsing index and error position. 195 * @return an {@link Unit} parsed from the character sequence. 196 * @throws IllegalArgumentException 197 * if the character sequence contains an illegal syntax. 198 */ 199 @SuppressWarnings("rawtypes") 200 public abstract Unit<? extends Quantity> parseProductUnit(CharSequence csq, ParsePosition pos) throws MeasurementParseException; 201 202 /** 203 * Parses a sequence of character to produce a single unit. 204 * 205 * @param csq 206 * the <code>CharSequence</code> to parse. 207 * @param pos 208 * an object holding the parsing index and error position. 209 * @return an {@link Unit} parsed from the character sequence. 210 * @throws IllegalArgumentException 211 * if the character sequence does not contain a valid unit identifier. 212 */ 213 @SuppressWarnings("rawtypes") 214 public abstract Unit<? extends Quantity> parseSingleUnit(CharSequence csq, ParsePosition pos) throws MeasurementParseException; 215 216 /** 217 * Attaches a system-wide label to the specified unit. For example: <code>SimpleUnitFormat.getInstance().label(DAY.multiply(365), "year"); 218 * SimpleUnitFormat.getInstance().label(METER.multiply(0.3048), "ft");</code> If the specified label is already associated to an unit the previous 219 * association is discarded or ignored. 220 * <p> 221 * If you set a different label without calling {@link #removeLabel(Unit)}), {@link #removeAlias(Unit, String)}), using the old label, or {@link #removeAliases(Unit)}) on the given unit, the old label is overwritten for <b>labeling/<b> purposes, but it remains like an <b>alias</b> (it still works for parsing). 222 * </p> 223 * @param unit 224 * the unit being labeled. 225 * @param label 226 * the new label for this unit. 227 * @throws IllegalArgumentException 228 * if the label is not a {@link SimpleUnitFormat#isValidIdentifier(String)} valid identifier. 229 */ 230 public abstract void label(Unit<?> unit, String label); 231 232 /** 233 * Removes the system-wide label (added by {@link #label(Unit, String)}) and all system-wide aliases (added by {@link #alias(Unit, String)}) for this unit. 234 * 235 * @param unit 236 * the unit for which label shall be removed. 237 */ 238 public abstract void removeLabel(Unit<?> unit); 239 240 /** 241 * Attaches a system-wide alias to this unit. Multiple aliases may be attached to the same unit. Aliases are used during parsing to recognize 242 * different variants of the same unit. For example: <code> SimpleUnitFormat.getInstance().alias(METER.multiply(0.3048), "foot"); 243 * SimpleUnitFormat.getInstance().alias(METRE.multiply(0.3048), "feet"); SimpleUnitFormat.getInstance().alias(METER, "meter"); 244 * SimpleUnitFormat.getInstance().alias(METRE, "meter"); </code> If the specified alias is already associated to a unit or applied as a label, the association is 245 * replaced by the new one. 246 * 247 * @param unit 248 * the unit being aliased. 249 * @param alias 250 * the alias attached to this unit. 251 * @throws IllegalArgumentException 252 * if the label is not a {@link SimpleUnitFormat#isValidIdentifier(String)} valid identifier. 253 */ 254 public abstract void alias(Unit<?> unit, String alias); 255 256 /** 257 * Removes the given system-wide alias (added by {@link #alias(Unit, String)}) for this unit and keeps the label (added by {@link #label(Unit, String)}) 258 * 259 * @param unit 260 * the unit for which alias shall be removed. 261 * 262 * @param alias 263 * the alias to be removed. 264 */ 265 public abstract void removeAlias(Unit<?> unit, String alias); 266 267 /** 268 * Removes all system-wide aliases (added by {@link #alias(Unit, String)}) for this unit and keeps the label (added by {@link #label(Unit, String)}) 269 * 270 * @param unit 271 * the unit for which aliases shall be removed. 272 */ 273 public abstract void removeAliases(Unit<?> unit); 274 275 /** 276 * Indicates if the specified name can be used as unit identifier. 277 * 278 * @param name 279 * the identifier to be tested. 280 * @return <code>true</code> if the name specified can be used as label or alias for this format;<code>false</code> otherwise. 281 */ 282 protected abstract boolean isValidIdentifier(String name); 283 284 /** 285 * Formats an unit and appends the resulting text to a given string buffer (implements <code>java.text.Format</code>). 286 * 287 * @param unit 288 * the unit to format. 289 * @param toAppendTo 290 * where the text is to be appended 291 * @param pos 292 * the field position (not used). 293 * @return <code>toAppendTo</code> 294 */ 295 public final StringBuffer format(Object unit, final StringBuffer toAppendTo, FieldPosition pos) { 296 try { 297 final Object dest = toAppendTo; 298 if (dest instanceof Appendable) { 299 format((Unit<?>) unit, (Appendable) dest); 300 } else { // When retroweaver is used to produce 1.4 binaries. TODO is this still relevant? 301 format((Unit<?>) unit, new Appendable() { 302 public Appendable append(char arg0) throws IOException { 303 toAppendTo.append(arg0); 304 return null; 305 } 306 public Appendable append(CharSequence arg0) throws IOException { 307 toAppendTo.append(arg0); 308 return null; 309 } 310 public Appendable append(CharSequence arg0, int arg1, int arg2) throws IOException { 311 toAppendTo.append(arg0.subSequence(arg1, arg2)); 312 return null; 313 } 314 }); 315 } 316 return toAppendTo; 317 } catch (IOException e) { 318 throw new MeasurementError(e); // Should never happen. 319 } 320 } 321 322 /** 323 * Parses the text from a string to produce an object (implements <code>java.text.Format</code>). 324 * 325 * @param source 326 * the string source, part of which should be parsed. 327 * @param pos 328 * the cursor position. 329 * @return the corresponding unit or <code>null</code> if the string cannot be parsed. 330 */ 331 public final Unit<?> parseObject(String source, ParsePosition pos) throws MeasurementParseException { 332 return parseProductUnit(source, pos); 333 } 334 335 /** 336 * This class represents an exponent with both a power (numerator) and a root (denominator). 337 */ 338 private static class Exponent { 339 public final int pow; 340 public final int root; 341 342 public Exponent(int pow, int root) { 343 this.pow = pow; 344 this.root = root; 345 } 346 } 347 348 /** 349 * This class represents the default (Unicode) format. 350 * internal class, please extend either SimpleUnitFormat or AbstractUnitFormat 351 */ 352 static class DefaultFormat extends SimpleUnitFormat { 353 354 // Initializes the standard unit databases. 355 356 static final Unit<?>[] METRIC_UNITS = { Units.AMPERE, Units.BECQUEREL, Units.CANDELA, Units.COULOMB, Units.FARAD, Units.GRAY, Units.HENRY, 357 Units.HERTZ, Units.JOULE, Units.KATAL, Units.KELVIN, Units.LUMEN, Units.LUX, Units.METRE, Units.MOLE, Units.NEWTON, Units.OHM, Units.PASCAL, 358 Units.RADIAN, Units.SECOND, Units.SIEMENS, Units.SIEVERT, Units.STERADIAN, Units.TESLA, Units.VOLT, Units.WATT, Units.WEBER }; 359 360 static final String[] METRIC_PREFIX_SYMBOLS = 361 Stream.of(MetricPrefix.values()) 362 .map(Prefix::getSymbol) 363 .collect(Collectors.toList()) 364 .toArray(new String[] {}); 365 366 // TODO try to consolidate those 367 static final UnitConverter[] METRIC_PREFIX_CONVERTERS = 368 Stream.of(MetricPrefix.values()) 369 .map(MultiplyConverter::ofPrefix) 370 .collect(Collectors.toList()) 371 .toArray(new UnitConverter[] {}); 372 373 static final String[] BINARY_PREFIX_SYMBOLS = 374 Stream.of(BinaryPrefix.values()) 375 .map(Prefix::getSymbol) 376 .collect(Collectors.toList()) 377 .toArray(new String[] {}); 378 379 static final UnitConverter[] BINARY_PREFIX_CONVERTERS = 380 Stream.of(BinaryPrefix.values()) 381 .map(MultiplyConverter::ofPrefix) 382 .collect(Collectors.toList()) 383 .toArray(new UnitConverter[] {}); 384 385 /** 386 * Holds the unique symbols collection (base units or alternate units). 387 */ 388 private final Map<String, Unit<?>> symbolToUnit = new HashMap<>(); 389 390 private static enum Token { EOF, IDENTIFIER, OPEN_PAREN, CLOSE_PAREN, EXPONENT, MULTIPLY, DIVIDE, 391 PLUS, INTEGER, FLOAT }; 392 393 394 DefaultFormat() { 395 // Hack, somehow µg is not found. 396 symbolToUnit.put(MetricPrefix.MICRO.getSymbol() + "g", MICRO(Units.GRAM)); 397 symbolToUnit.put("μg", MICRO(Units.GRAM)); 398 symbolToUnit.put(MU + "g", MICRO(Units.GRAM)); 399 } 400 401 private DefaultFormat init() { 402 403 for (int i = 0; i < METRIC_UNITS.length; i++) { 404 Unit<?> si = METRIC_UNITS[i]; 405 String symbol = (si instanceof BaseUnit) ? ((BaseUnit<?>) si).getSymbol() : ((AlternateUnit<?>) si).getSymbol(); 406 label(si, symbol); 407 for (int j = 0; j < METRIC_PREFIX_SYMBOLS.length; j++) { 408 Unit<?> u = si.prefix(MetricPrefix.values()[j]); 409 label(u, METRIC_PREFIX_SYMBOLS[j] + symbol); 410 if ( "µ".equals(METRIC_PREFIX_SYMBOLS[j]) ) { 411 label(u, MU + symbol); 412 } 413 } // TODO what about BINARY_PREFIX here? 414 } 415 416 // -- GRAM/KILOGRAM 417 418 label(Units.GRAM, "g"); 419 for(MetricPrefix prefix : MetricPrefix.values()) { 420 switch (prefix) { 421 case KILO: 422 label(Units.KILOGRAM, "kg"); 423 break; 424 case MICRO: 425 label(Units.GRAM.prefix(prefix), prefix.getSymbol()+"g"); 426 break; 427 default: 428 label(Units.GRAM.prefix(prefix), prefix.getSymbol()+"g"); 429 break; 430 } 431 } 432 433 label(MICRO(Units.GRAM), MetricPrefix.MICRO.getSymbol() + "g"); 434 435 // Alias in ASCIIFormat for Ohm 436 aliasWithPrefixes(Units.OHM, "Ohm"); 437 438 // Special case for DEGREE_CELSIUS. 439 labelWithPrefixes(Units.CELSIUS, "℃"); 440 aliasWithPrefixes(Units.CELSIUS, "°C"); 441 442 // Additional cases and aliases 443 label(AbstractUnit.ONE, "one"); 444 label(Units.PERCENT, "%"); 445 446 // https://en.wikipedia.org/wiki/Non-SI_units_mentioned_in_the_SI#Units_officially_accepted_for_use_with_the_SI 447 // The SI prefixes can be used with several of these units, but not, for example, with the non-SI units of time. 448 // Also see https://github.com/unitsofmeasurement/indriya/issues/433 449 label(Units.MINUTE, "min"); 450 label(Units.HOUR, "h"); 451 label(Units.DAY, "d"); 452 alias(Units.DAY, "day"); 453 label(Units.WEEK, "wk"); 454 alias(Units.WEEK, "week"); 455 label(Units.YEAR, "yr"); 456 alias(Units.YEAR, "y"); 457 alias(Units.YEAR, "year"); 458 alias(Units.YEAR, "days365"); 459 alias(Units.YEAR, "a"); 460 label(Units.MONTH, "mo"); 461 alias(Units.MONTH, "mon"); 462 alias(Units.MONTH, "month"); 463 label(Units.KILOMETRE_PER_HOUR, "km/h"); 464 labelWithPrefixes(Units.SQUARE_METRE, "m\u00B2"); 465 aliasWithPrefixes(Units.SQUARE_METRE, "\u33A1"); 466 aliasWithPrefixes(Units.SQUARE_METRE, "m2"); 467 labelWithPrefixes(Units.CUBIC_METRE, "m\u00B3"); 468 aliasWithPrefixes(Units.CUBIC_METRE, "\u33A5"); 469 aliasWithPrefixes(Units.CUBIC_METRE, "m3"); 470 labelWithPrefixes(Units.LITRE, "l"); 471 472 return this; 473 } 474 475 /** 476 * Holds the name to unit mapping. 477 */ 478 protected final Map<String, Unit<?>> nameToUnit = new HashMap<>(); 479 480 /** 481 * Holds the unit to name mapping. 482 */ 483 protected final Map<Unit<?>, String> unitToName = new HashMap<>(); 484 485 @Override 486 public String toString() { 487 return SimpleUnitFormat.class.getSimpleName(); 488 } 489 490 @Override 491 public void label(Unit<?> unit, String label) { 492 if (!isValidIdentifier(label)) 493 throw new IllegalArgumentException("Label: " + label + " is not a valid identifier."); 494 synchronized (this) { 495 nameToUnit.put(label, unit); 496 unitToName.put(unit, label); 497 } 498 } 499 500 @Override 501 public void removeLabel(Unit<?> unit) { 502 unitToName.remove(unit); 503 nameToUnit.entrySet().removeIf(e -> e.getValue().equals(unit)); 504 } 505 506 @Override 507 public void alias(Unit<?> unit, String alias) { 508 if (!isValidIdentifier(alias)) 509 throw new IllegalArgumentException("Alias: " + alias + " is not a valid identifier."); 510 synchronized (this) { 511 nameToUnit.put(alias, unit); 512 } 513 } 514 515 @Override 516 public void removeAlias(Unit<?> unit, String alias) { 517 nameToUnit.remove(alias); 518 } 519 520 @Override 521 public void removeAliases(Unit<?> unit) { 522 final String alias = unitToName.get(unit); 523 nameToUnit.entrySet().removeIf(e -> e.getValue().equals(unit) && !e.getKey().equals(alias)); 524 } 525 526 @Override 527 protected boolean isValidIdentifier(String name) { 528 if ((name == null) || (name.length() == 0)) 529 return false; 530 return isUnitIdentifierPart(name.charAt(0)); 531 } 532 533 /** 534 * Applies {@link #label(Unit, String)} for this unit and all standard prefixes. 535 * 536 * @param unit a unit 537 * @param label a label 538 */ 539 private void labelWithPrefixes(Unit<?> unit, String label) { 540 label(unit, label); 541 // TODO try to optimize this 542 for (int i = 0; i < METRIC_PREFIX_SYMBOLS.length; i++) { 543 label(unit.prefix(MetricPrefix.values()[i]), METRIC_PREFIX_SYMBOLS[i] + label); 544 } 545 for (int i = 0; i < BINARY_PREFIX_SYMBOLS.length; i++) { 546 label(unit.prefix(BinaryPrefix.values()[i]), BINARY_PREFIX_SYMBOLS[i] + label); 547 } 548 } 549 550 /** 551 * Applies {@link #alias(Unit, String)} for this unit and all standard prefixes. 552 * 553 * @param unit a unit 554 * @param alias an alias 555 */ 556 private void aliasWithPrefixes(Unit<?> unit, String alias) { 557 alias(unit, alias); 558 // TODO try to optimize this 559 for (int i = 0; i < METRIC_PREFIX_SYMBOLS.length; i++) { 560 alias(unit.prefix(MetricPrefix.values()[i]), METRIC_PREFIX_SYMBOLS[i] + alias); 561 } 562 for (int i = 0; i < BINARY_PREFIX_SYMBOLS.length; i++) { 563 alias(unit.prefix(BinaryPrefix.values()[i]), BINARY_PREFIX_SYMBOLS[i] + alias); 564 } 565 566 } 567 568 protected static boolean isUnitIdentifierPart(char ch) { 569 return Character.isLetter(ch) 570 || (!Character.isWhitespace(ch) && !Character.isDigit(ch) && (ch != MIDDLE_DOT) && (ch != '*') && (ch != '/') && (ch != '(') && (ch != ')') 571 && (ch != '[') && (ch != ']') && (ch != '\u00b9') && (ch != '\u00b2') && (ch != '\u00b3') && (ch != '^') && (ch != '+') && (ch != '-')); 572 } 573 574 // Returns the name for the specified unit or null if product unit. 575 protected String nameFor(Unit<?> unit) { 576 // Searches label database. 577 String label = unitToName.get(unit); 578 if (label != null) 579 return label; 580 if (unit instanceof BaseUnit) 581 return ((BaseUnit<?>) unit).getSymbol(); 582 if (unit instanceof AlternateUnit) 583 return ((AlternateUnit<?>) unit).getSymbol(); 584 if (unit instanceof TransformedUnit) { 585 TransformedUnit<?> tfmUnit = (TransformedUnit<?>) unit; 586 if (tfmUnit.getSymbol() != null) { 587 return tfmUnit.getSymbol(); 588 } 589 Unit<?> baseUnit = tfmUnit.getParentUnit(); 590 UnitConverter cvtr = tfmUnit.getConverter(); // tfmUnit.getSystemConverter(); 591 StringBuilder result = new StringBuilder(); 592 String baseUnitName = baseUnit.toString(); 593 String prefix = prefixFor(cvtr); 594 if ((baseUnitName.indexOf(MIDDLE_DOT) >= 0) || (baseUnitName.indexOf('*') >= 0) || (baseUnitName.indexOf('/') >= 0)) { 595 // We could use parentheses whenever baseUnits is an 596 // instanceof ProductUnit, but most ProductUnits have 597 // aliases, 598 // so we'd end up with a lot of unnecessary parentheses. 599 result.append('('); 600 result.append(baseUnitName); 601 result.append(')'); 602 } else { 603 result.append(baseUnitName); 604 } 605 if (prefix != null) { 606 result.insert(0, prefix); 607 } else { 608 if (cvtr instanceof AddConverter) { 609 result.append('+'); 610 result.append(((AddConverter) cvtr).getOffset()); 611 } else if (cvtr instanceof MultiplyConverter) { 612 Number scaleFactor = ((MultiplyConverter) cvtr).getFactor(); 613 if(scaleFactor instanceof RationalNumber) { 614 615 RationalNumber rational = (RationalNumber)scaleFactor; 616 RationalNumber reciprocal = rational.reciprocal(); 617 if(reciprocal.isInteger()) { 618 result.append('/'); 619 result.append(reciprocal.toString()); // renders as integer 620 } else { 621 result.append('*'); 622 result.append(scaleFactor); 623 } 624 625 } else { 626 result.append('*'); 627 result.append(scaleFactor); 628 } 629 630 } else { // Other converters. 631 return "[" + baseUnit + "?]"; 632 } 633 } 634 return result.toString(); 635 } 636 if (unit instanceof AnnotatedUnit<?>) { 637 AnnotatedUnit<?> annotatedUnit = (AnnotatedUnit<?>) unit; 638 final StringBuilder annotable = new StringBuilder(nameFor(annotatedUnit.getActualUnit())); 639 if (annotatedUnit.getAnnotation() != null) { 640 annotable.append('{'); // TODO maybe also configure this one similar to mix delimiter 641 annotable.append(annotatedUnit.getAnnotation()); 642 annotable.append('}'); 643 } 644 return annotable.toString(); 645 } 646 return null; // Product unit. 647 } 648 649 // Returns the prefix for the specified unit converter. 650 protected String prefixFor(UnitConverter converter) { 651 for (int i = 0; i < METRIC_PREFIX_CONVERTERS.length; i++) { 652 if (METRIC_PREFIX_CONVERTERS[i].equals(converter)) { 653 return METRIC_PREFIX_SYMBOLS[i]; 654 } 655 } 656 for (int j = 0; j < BINARY_PREFIX_CONVERTERS.length; j++) { 657 if (BINARY_PREFIX_CONVERTERS[j].equals(converter)) { 658 return BINARY_PREFIX_SYMBOLS[j]; 659 } 660 } 661 return null; // TODO or return blank? 662 } 663 664 // Returns the unit for the specified name. 665 protected Unit<?> unitFor(String name) { 666 Unit<?> unit = nameToUnit.get(name); 667 if (unit != null) { 668 return unit; 669 } else { 670 unit = symbolToUnit.get(name); 671 } 672 return unit; 673 } 674 675 // ////////////////////////// 676 // Parsing. 677 @SuppressWarnings({ "rawtypes", "unchecked" }) 678 public Unit<? extends Quantity> parseSingleUnit(CharSequence csq, ParsePosition pos) throws MeasurementParseException { 679 int startIndex = pos.getIndex(); 680 String name = readIdentifier(csq, pos); 681 Unit unit = unitFor(name); 682 check(unit != null, name + " not recognized", csq, startIndex); 683 return unit; 684 } 685 686 @SuppressWarnings({ "rawtypes", "unchecked" }) 687 @Override 688 public Unit<? extends Quantity> parseProductUnit(CharSequence csq, ParsePosition pos) throws MeasurementParseException { 689 Unit result = null; 690 if (csq == null) { 691 throw new MeasurementParseException("Cannot parse null", csq, pos.getIndex()); 692 } else { 693 result = unitFor(csq.toString()); 694 if (result != null) 695 return result; 696 } 697 result = AbstractUnit.ONE; 698 Token token = nextToken(csq, pos); 699 switch (token) { 700 case IDENTIFIER: 701 result = parseSingleUnit(csq, pos); 702 break; 703 case OPEN_PAREN: 704 pos.setIndex(pos.getIndex() + 1); 705 result = parseProductUnit(csq, pos); 706 token = nextToken(csq, pos); 707 check(token == Token.CLOSE_PAREN, "')' expected", csq, pos.getIndex()); 708 pos.setIndex(pos.getIndex() + 1); 709 break; 710 default: 711 break; 712 } 713 token = nextToken(csq, pos); 714 while (true) { 715 switch (token) { 716 case EXPONENT: 717 Exponent e = readExponent(csq, pos); 718 if (e.pow != 1) { 719 result = result.pow(e.pow); 720 } 721 if (e.root != 1) { 722 result = result.root(e.root); 723 } 724 break; 725 case MULTIPLY: 726 pos.setIndex(pos.getIndex() + 1); 727 token = nextToken(csq, pos); 728 if (token == Token.INTEGER) { 729 long n = readLong(csq, pos); 730 if (n != 1) { 731 result = result.multiply(n); 732 } 733 } else if (token == Token.FLOAT) { 734 double d = readDouble(csq, pos); 735 if (d != 1.0) { 736 result = result.multiply(d); 737 } 738 } else { 739 result = result.multiply(parseProductUnit(csq, pos)); 740 } 741 break; 742 case DIVIDE: 743 pos.setIndex(pos.getIndex() + 1); 744 token = nextToken(csq, pos); 745 if (token == Token.INTEGER) { 746 long n = readLong(csq, pos); 747 if (n != 1) { 748 result = result.divide(n); 749 } 750 } else if (token == Token.FLOAT) { 751 double d = readDouble(csq, pos); 752 if (d != 1.0) { 753 result = result.divide(d); 754 } 755 } else { 756 result = result.divide(parseProductUnit(csq, pos)); 757 } 758 break; 759 case PLUS: 760 pos.setIndex(pos.getIndex() + 1); 761 token = nextToken(csq, pos); 762 if (token == Token.INTEGER) { 763 long n = readLong(csq, pos); 764 if (n != 1) { 765 result = result.shift(n); 766 } 767 } else if (token == Token.FLOAT) { 768 double d = readDouble(csq, pos); 769 if (d != 1.0) { 770 result = result.shift(d); 771 } 772 } else { 773 throw new MeasurementParseException("not a number", csq, pos.getIndex()); 774 } 775 break; 776 case EOF: 777 case CLOSE_PAREN: 778 return result; 779 default: 780 throw new MeasurementParseException("unexpected token " + token, csq, pos.getIndex()); 781 } 782 token = nextToken(csq, pos); 783 } 784 } 785 786 private static Token nextToken(CharSequence csq, ParsePosition pos) { 787 final int length = csq.length(); 788 while (pos.getIndex() < length) { 789 char c = csq.charAt(pos.getIndex()); 790 if (isUnitIdentifierPart(c)) { 791 return Token.IDENTIFIER; 792 } else if (c == '(') { 793 return Token.OPEN_PAREN; 794 } else if (c == ')') { 795 return Token.CLOSE_PAREN; 796 } else if ((c == '^') || (c == '\u00b9') || (c == '\u00b2') || (c == '\u00b3')) { 797 return Token.EXPONENT; 798 } else if (c == '*') { 799 if (csq.length() == pos.getIndex() + 1) { 800 throw new MeasurementParseException("unexpected token " + Token.EOF, csq, pos.getIndex()); // return ; 801 } 802 char c2 = csq.charAt(pos.getIndex() + 1); 803 return c2 == '*' ? Token.EXPONENT : Token.MULTIPLY; 804 } else if (c == MIDDLE_DOT) { 805 return Token.MULTIPLY; 806 } else if (c == '/') { 807 return Token.DIVIDE; 808 } else if (c == '+') { 809 return Token.PLUS; 810 } else if ((c == '-') || Character.isDigit(c)) { 811 int index = pos.getIndex() + 1; 812 while ((index < length) && (Character.isDigit(c) || (c == '-') || (c == '.') || (c == 'E'))) { 813 c = csq.charAt(index++); 814 if (c == '.') { 815 return Token.FLOAT; 816 } 817 } 818 return Token.INTEGER; 819 } 820 pos.setIndex(pos.getIndex() + 1); 821 } 822 return Token.EOF; 823 } 824 825 private static void check(boolean expr, String message, CharSequence csq, int index) throws MeasurementParseException { 826 if (!expr) { 827 throw new MeasurementParseException(message + " (in " + csq + " at index " + index + ")", index); 828 } 829 } 830 831 private static Exponent readExponent(CharSequence csq, ParsePosition pos) { 832 char c = csq.charAt(pos.getIndex()); 833 if (c == '^') { 834 pos.setIndex(pos.getIndex() + 1); 835 } else if (c == '*') { 836 pos.setIndex(pos.getIndex() + 2); 837 } 838 final int length = csq.length(); 839 int pow = 0; 840 boolean isPowNegative = false; 841 boolean parseRoot = false; 842 843 POWERLOOP: while (pos.getIndex() < length) { 844 c = csq.charAt(pos.getIndex()); 845 switch(c) { 846 case '-': isPowNegative = true; break; 847 case '\u00b9': pow = pow * 10 + 1; break; 848 case '\u00b2': pow = pow * 10 + 2; break; 849 case '\u00b3': pow = pow * 10 + 3; break; 850 case ':': parseRoot = true; break POWERLOOP; 851 default: 852 if (c >= '0' && c <= '9') pow = pow * 10 + (c - '0'); 853 else break POWERLOOP; 854 } 855 pos.setIndex(pos.getIndex() + 1); 856 } 857 if (pow == 0) pow = 1; 858 859 int root = 0; 860 boolean isRootNegative = false; 861 if (parseRoot) { 862 pos.setIndex(pos.getIndex() + 1); 863 ROOTLOOP: while (pos.getIndex() < length) { 864 c = csq.charAt(pos.getIndex()); 865 switch(c) { 866 case '-': isRootNegative = true; break; 867 case '\u00b9': root = root * 10 + 1; break; 868 case '\u00b2': root = root * 10 + 2; break; 869 case '\u00b3': root = root * 10 + 3; break; 870 default: 871 if (c >= '0' && c <= '9') root = root * 10 + (c - '0'); 872 else break ROOTLOOP; 873 } 874 pos.setIndex(pos.getIndex() + 1); 875 } 876 } 877 if (root == 0) root = 1; 878 879 return new Exponent(isPowNegative ? -pow : pow, isRootNegative ? -root : root); 880 } 881 882 private static long readLong(CharSequence csq, ParsePosition pos) { 883 final int length = csq.length(); 884 int result = 0; 885 boolean isNegative = false; 886 while (pos.getIndex() < length) { 887 char c = csq.charAt(pos.getIndex()); 888 if (c == '-') { 889 isNegative = true; 890 } else if ((c >= '0') && (c <= '9')) { 891 result = result * 10 + (c - '0'); 892 } else { 893 break; 894 } 895 pos.setIndex(pos.getIndex() + 1); 896 } 897 return isNegative ? -result : result; 898 } 899 900 private static double readDouble(CharSequence csq, ParsePosition pos) { 901 final int length = csq.length(); 902 int start = pos.getIndex(); 903 int end = start + 1; 904 while (end < length) { 905 if ("0123456789+-.E".indexOf(csq.charAt(end)) < 0) { 906 break; 907 } 908 end += 1; 909 } 910 pos.setIndex(end + 1); 911 return Double.parseDouble(csq.subSequence(start, end).toString()); 912 } 913 914 private static String readIdentifier(CharSequence csq, ParsePosition pos) { 915 final int length = csq.length(); 916 int start = pos.getIndex(); 917 int i = start; 918 while ((++i < length) && isUnitIdentifierPart(csq.charAt(i))) { 919 } 920 pos.setIndex(i); 921 return csq.subSequence(start, i).toString(); 922 } 923 924 // ////////////////////////// 925 // Formatting. 926 927 @Override 928 public Appendable format(Unit<?> unit, Appendable appendable) throws IOException { 929 String name = nameFor(unit); 930 if (name != null) { 931 return appendable.append(name); 932 } 933 if (!(unit instanceof ProductUnit)) { 934 throw new IllegalArgumentException("Cannot format given Object as a Unit"); 935 } 936 937 // Product unit. 938 ProductUnit<?> productUnit = (ProductUnit<?>) unit; 939 940 // Special case: self-powered product unit 941 if (productUnit.getUnitCount() == 1 && productUnit.getUnit(0) instanceof ProductUnit) { 942 final ProductUnit<?> powerUnit = (ProductUnit<?>) productUnit.getUnit(0); 943 // is the sub-unit known under a given label? 944 if (nameFor(powerUnit) == null) 945 // apply the power to the sub-units and format those instead 946 return format(ProductUnit.ofPow(powerUnit, productUnit.getUnitPow(0)), appendable); 947 } 948 949 int invNbr = 0; 950 951 // Write positive exponents first. 952 boolean start = true; 953 for (int i = 0; i < productUnit.getUnitCount(); i++) { 954 int pow = productUnit.getUnitPow(i); 955 if (pow >= 0) { 956 if (!start) { 957 appendable.append(MIDDLE_DOT); // Separator. 958 } 959 name = nameFor(productUnit.getUnit(i)); 960 int root = productUnit.getUnitRoot(i); 961 append(appendable, name, pow, root); 962 start = false; 963 } else { 964 invNbr++; 965 } 966 } 967 968 // Write negative exponents. 969 if (invNbr != 0) { 970 if (start) { 971 appendable.append('1'); // e.g. 1/s 972 } 973 appendable.append('/'); 974 if (invNbr > 1) { 975 appendable.append('('); 976 } 977 start = true; 978 for (int i = 0; i < productUnit.getUnitCount(); i++) { 979 int pow = productUnit.getUnitPow(i); 980 if (pow < 0) { 981 name = nameFor(productUnit.getUnit(i)); 982 int root = productUnit.getUnitRoot(i); 983 if (!start) { 984 appendable.append(MIDDLE_DOT); // Separator. 985 } 986 append(appendable, name, -pow, root); 987 start = false; 988 } 989 } 990 if (invNbr > 1) { 991 appendable.append(')'); 992 } 993 } 994 return appendable; 995 } 996 997 private static void append(Appendable appendable, CharSequence symbol, int pow, int root) throws IOException { 998 appendable.append(symbol); 999 if ((pow != 1) || (root != 1)) { 1000 // Write exponent. 1001 if ((pow == 2) && (root == 1)) { 1002 appendable.append('\u00b2'); // Square 1003 } else if ((pow == 3) && (root == 1)) { 1004 appendable.append('\u00b3'); // Cubic 1005 } else { 1006 // Use general exponent form. 1007 appendable.append('^'); 1008 appendable.append(String.valueOf(pow)); 1009 if (root != 1) { 1010 appendable.append(':'); 1011 appendable.append(String.valueOf(root)); 1012 } 1013 } 1014 } 1015 } 1016 1017 // private static final long serialVersionUID = 1L; 1018 1019 @Override 1020 public Unit<?> parse(CharSequence csq) throws MeasurementParseException { 1021 return parse(csq, 0); 1022 } 1023 1024 protected Unit<?> parse(CharSequence csq, int index) throws IllegalArgumentException { 1025 return parse(csq, new ParsePosition(index)); 1026 } 1027 1028 @Override 1029 public Unit<?> parse(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException { 1030 return parseObject(csq.toString(), cursor); 1031 } 1032 } 1033 1034 /** 1035 * This class represents the ASCII_INSTANCE format. 1036 */ 1037 private static final class ASCIIFormat extends DefaultFormat { 1038 1039 private ASCIIFormat() { 1040 super(); 1041 } 1042 1043 private ASCIIFormat init() { 1044 1045 // ASCII_INSTANCE 1046 for (int i = 0; i < METRIC_UNITS.length; i++) { 1047 Unit<?> si = METRIC_UNITS[i]; 1048 String symbol = (si instanceof BaseUnit) ? ((BaseUnit<?>) si).getSymbol() : ((AlternateUnit<?>) si).getSymbol(); 1049 if (isAllAscii(symbol)) 1050 label(si, symbol); 1051 for (int j = 0; j < METRIC_PREFIX_SYMBOLS.length; j++) { 1052 Unit<?> u = si.prefix(MetricPrefix.values()[j]); 1053 if ( "µ".equals(METRIC_PREFIX_SYMBOLS[j]) ) { 1054 label(u, "micro" + asciiSymbol(symbol)); 1055 } 1056 } // TODO what about BINARY_PREFIX here? 1057 } 1058 1059 // -- GRAM/KILOGRAM 1060 1061 label(Units.GRAM, "g"); 1062 for(MetricPrefix prefix : MetricPrefix.values()) { 1063 switch (prefix) { 1064 case KILO: 1065 label(Units.KILOGRAM, "kg"); 1066 break; 1067 case MICRO: 1068 label(MICRO(Units.GRAM), "microg"); // instead of 'µg' -> 'microg' 1069 break; 1070 default: 1071 label(Units.GRAM.prefix(prefix), prefix.getSymbol()+"g"); 1072 break; 1073 } 1074 } 1075 1076 // ASCIIFormat for Ohm 1077 labelWithAsciiPrefixes(Units.OHM, "Ohm"); 1078 1079 // Special case for DEGREE_CELSIUS. 1080 labelWithAsciiPrefixes(Units.CELSIUS, "Celsius"); 1081 aliasWithAsciiPrefixes(Units.CELSIUS, "Cel"); 1082 1083 label(Units.METRE, "m"); 1084 label(Units.SECOND, "s"); 1085 label(Units.KILOMETRE_PER_HOUR, "km/h"); 1086 alias(Units.SQUARE_METRE, "m2"); 1087 alias(Units.CUBIC_METRE, "m3"); 1088 1089 // -- LITRE 1090 1091 label(Units.LITRE, "l"); 1092 for(Prefix prefix : MetricPrefix.values()) { 1093 if(prefix==MICRO) { 1094 label(MICRO(Units.LITRE), "microL"); // instead of 'µL' -> 'microL' 1095 } else { 1096 label(Units.LITRE.prefix(prefix), prefix.getSymbol()+"L"); 1097 } 1098 } 1099 label(Units.NEWTON, "N"); 1100 label(Units.RADIAN, "rad"); 1101 1102 label(AbstractUnit.ONE, "one"); 1103 1104 return this; 1105 } 1106 1107 1108 @Override 1109 protected String nameFor(Unit<?> unit) { 1110 // First search if specific ASCII_INSTANCE name should be used. 1111 String name = unitToName.get(unit); 1112 if (name != null) 1113 return name; 1114 // Else returns default name. 1115 return DEFAULT_INSTANCE.nameFor(unit); 1116 } 1117 1118 @Override 1119 protected Unit<?> unitFor(String name) { 1120 // First search if specific ASCII_INSTANCE name. 1121 Unit<?> unit = nameToUnit.get(name); 1122 if (unit != null) 1123 return unit; 1124 // Else returns default mapping. 1125 return DEFAULT_INSTANCE.unitFor(name); 1126 } 1127 1128 @Override 1129 public String toString() { 1130 return "SimpleUnitFormat - ASCII"; 1131 } 1132 1133 @Override 1134 public Appendable format(Unit<?> unit, Appendable appendable) throws IOException { 1135 String name = nameFor(unit); 1136 if (name != null) 1137 return appendable.append(name); 1138 if (!(unit instanceof ProductUnit)) 1139 throw new IllegalArgumentException("Cannot format given Object as a Unit"); 1140 1141 ProductUnit<?> productUnit = (ProductUnit<?>) unit; 1142 for (int i = 0; i < productUnit.getUnitCount(); i++) { 1143 if (i != 0) { 1144 appendable.append('*'); // Separator. 1145 } 1146 name = nameFor(productUnit.getUnit(i)); 1147 int pow = productUnit.getUnitPow(i); 1148 int root = productUnit.getUnitRoot(i); 1149 appendable.append(name); 1150 if ((pow != 1) || (root != 1)) { 1151 // Use general exponent form. 1152 appendable.append('^'); 1153 appendable.append(String.valueOf(pow)); 1154 if (root != 1) { 1155 appendable.append(':'); 1156 appendable.append(String.valueOf(root)); 1157 } 1158 } 1159 } 1160 return appendable; 1161 } 1162 1163 @Override 1164 protected boolean isValidIdentifier(String name) { 1165 if ((name == null) || (name.length() == 0)) 1166 return false; 1167 // label must not begin with a digit or mathematical operator 1168 return isUnitIdentifierPart(name.charAt(0)) && isAllAscii(name); 1169 /* 1170 * for (int i = 0; i < name.length(); i++) { if 1171 * (!isAsciiCharacter(name.charAt(i))) return false; } return true; 1172 */ 1173 } 1174 1175 /** 1176 * Applies {@link #alias(Unit, String)} for this unit and all standard prefixes, if the alias contains only ASCII characters. 1177 * 1178 * @param unit a unit 1179 * @param alias an alias 1180 */ 1181 private void aliasWithAsciiPrefixes(Unit<?> unit, String alias) { 1182 if (isValidIdentifier(alias)) { 1183 alias(unit, alias); 1184 for (int i = 0; i < METRIC_PREFIX_SYMBOLS.length; i++) { 1185 alias(unit.prefix(MetricPrefix.values()[i]), asciiPrefix(METRIC_PREFIX_SYMBOLS[i]) + alias); 1186 } 1187 for (int i = 0; i < BINARY_PREFIX_SYMBOLS.length; i++) { 1188 alias(unit.prefix(BinaryPrefix.values()[i]), asciiPrefix(BINARY_PREFIX_SYMBOLS[i]) + alias); 1189 } 1190 } 1191 } 1192 1193 /** 1194 * Applies {@link #label(Unit, String)} for this unit and all standard prefixes, if the label contains only ASCII characters. 1195 * 1196 * @param unit a unit 1197 * @param alias an label 1198 */ 1199 private void labelWithAsciiPrefixes(Unit<?> unit, String label) { 1200 if (isValidIdentifier(label)) { 1201 label(unit, label); 1202 for (int i = 0; i < METRIC_PREFIX_SYMBOLS.length; i++) { 1203 label(unit.prefix(MetricPrefix.values()[i]), asciiPrefix(METRIC_PREFIX_SYMBOLS[i]) + label); 1204 } 1205 for (int i = 0; i < BINARY_PREFIX_SYMBOLS.length; i++) { 1206 label(unit.prefix(BinaryPrefix.values()[i]), asciiPrefix(BINARY_PREFIX_SYMBOLS[i]) + label); 1207 } 1208 } 1209 } 1210 1211 private static String asciiPrefix(String prefix) { 1212 return "µ".equals(prefix) ? "micro" : prefix; 1213 } 1214 1215 private static String asciiSymbol(String s) { 1216 return "Ω".equals(s) ? "Ohm" : s; 1217 } 1218 1219 /** to check if a string only contains US-ASCII_INSTANCE characters */ 1220 private static boolean isAllAscii(String input) { 1221 boolean isASCII = true; 1222 for (int i = 0; i < input.length(); i++) { 1223 int c = input.charAt(i); 1224 if (c > 0x7F) { 1225 isASCII = false; 1226 break; 1227 } 1228 } 1229 return isASCII; 1230 } 1231 } 1232}