001/* 002 * Units of Measurement Systems 003 * Copyright (c) 2005-2017, Jean-Marie Dautelle, Werner Keil and others. 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-363, Units of Measurement nor the names of their contributors may be used to 017 * endorse or promote products 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 systems.uom.ucum.format; 031 032import static systems.uom.ucum.format.UCUMConverterFormatter.formatConverter; 033import static tec.units.indriya.AbstractUnit.ONE; 034import si.uom.SI; 035import systems.uom.ucum.internal.format.UCUMFormatParser; 036import tec.units.indriya.AbstractUnit; 037import tec.units.indriya.format.AbstractUnitFormat; 038import tec.units.indriya.format.SymbolMap; 039import tec.units.indriya.internal.format.TokenException; 040import tec.units.indriya.internal.format.TokenMgrError; 041import tec.units.indriya.unit.AnnotatedUnit; 042import tec.units.indriya.unit.MetricPrefix; 043//import tec.units.indriya.unit.MetricPrefix; 044import tec.units.indriya.unit.TransformedUnit; 045 046import javax.measure.Quantity; 047import javax.measure.Unit; 048import javax.measure.UnitConverter; 049import javax.measure.format.ParserException; 050 051import java.io.ByteArrayInputStream; 052import java.io.IOException; 053import java.text.ParsePosition; 054import java.util.*; 055 056/** 057 * <p> 058 * This class provides the interface for formatting and parsing 059 * {@link AbstractUnit units} according to the 060 * <a href="http://unitsofmeasure.org/">Uniform Code for CommonUnits of 061 * Measure</a> (UCUM). 062 * </p> 063 * 064 * <p> 065 * For a technical/historical overview of this format please read 066 * <a href="http://www.pubmedcentral.nih.gov/articlerender.fcgi?artid=61354"> 067 * CommonUnits of Measure in Clinical Information Systems</a>. 068 * </p> 069 * 070 * <p> 071 * As of revision 1.16, the BNF in the UCUM standard contains an 072 * <a href="http://unitsofmeasure.org/ticket/4">error</a>. I've attempted to 073 * work around the problem by modifying the BNF productions for <Term>. 074 * Once the error in the standard is corrected, it may be necessary to modify 075 * the productions in the UCUMFormatParser.jj file to conform to the standard. 076 * </p> 077 * 078 * @author <a href="mailto:eric-r@northwestern.edu">Eric Russell</a> 079 * @author <a href="mailto:units@catmedia.us">Werner Keil</a> 080 * @version 0.7.5, 30 April 2017 081 */ 082public abstract class UCUMFormat extends AbstractUnitFormat { 083 /** 084 * 085 */ 086 // private static final long serialVersionUID = 8586656823290135155L; 087 088 // A helper to declare bundle names for all instances 089 private static final String BUNDLE_BASE = UCUMFormat.class.getName(); 090 091 // ///////////////// 092 // Class methods // 093 // ///////////////// 094 095 /** 096 * Returns the instance for formatting/parsing using the given variant 097 * 098 * @param variant 099 * the <strong>UCUM</strong> variant to use 100 * @return a {@link UCUMFormat} instance 101 */ 102 public static UCUMFormat getInstance(Variant variant) { 103 switch (variant) { 104 case CASE_INSENSITIVE: 105 return Parsing.DEFAULT_CI; 106 case CASE_SENSITIVE: 107 return Parsing.DEFAULT_CS; 108 case PRINT: 109 return Print.DEFAULT; 110 default: 111 throw new IllegalArgumentException("Unknown variant: " + variant); 112 } 113 } 114 115 /** 116 * Returns an instance for formatting and parsing using user defined symbols 117 * 118 * @param variant 119 * the <strong>UCUM</strong> variant to use 120 * @param symbolMap 121 * the map of user defined symbols to use 122 * @return a {@link UCUMFormat} instance 123 */ 124 public static UCUMFormat getInstance(Variant variant, SymbolMap symbolMap) { 125 switch (variant) { 126 case CASE_INSENSITIVE: 127 return new Parsing(symbolMap, false); 128 case CASE_SENSITIVE: 129 return new Parsing(symbolMap, true); 130 case PRINT: 131 return new Print(symbolMap); 132 default: 133 throw new IllegalArgumentException("Unknown variant: " + variant); 134 } 135 } 136 137 /** 138 * The symbol map used by this instance to map between {@link AbstractUnit 139 * Unit}s and <code>String</code>s. 140 */ 141 final SymbolMap symbolMap; 142 143 /** 144 * Get the symbol map used by this instance to map between 145 * {@link AbstractUnit Unit}s and <code>String</code>s, etc... 146 * 147 * @return SymbolMap the current symbol map 148 */ 149 @Override 150 protected SymbolMap getSymbols() { 151 return symbolMap; 152 } 153 154 ////////////////// 155 // Constructors // 156 ////////////////// 157 /** 158 * Base constructor. 159 */ 160 UCUMFormat(SymbolMap symbolMap) { 161 this.symbolMap = symbolMap; 162 } 163 164 // /////////// 165 // Parsing // 166 // /////////// 167 public abstract Unit<? extends Quantity<?>> parse(CharSequence csq, ParsePosition cursor) throws ParserException; 168 169 protected Unit<?> parse(CharSequence csq, int index) throws ParserException { 170 return parse(csq, new ParsePosition(index)); 171 } 172 173 @Override 174 public abstract Unit<? extends Quantity<?>> parse(CharSequence csq) throws ParserException; 175 176 //////////////// 177 // Formatting // 178 //////////////// 179 @SuppressWarnings({ "rawtypes", "unchecked" }) 180 public Appendable format(Unit<?> unknownUnit, Appendable appendable) throws IOException { 181 if (!(unknownUnit instanceof AbstractUnit)) { 182 throw new UnsupportedOperationException( 183 "The UCUM format supports only known units (AbstractUnit instances)"); 184 } 185 AbstractUnit unit = (AbstractUnit) unknownUnit; 186 CharSequence symbol; 187 CharSequence annotation = null; 188 if (unit instanceof AnnotatedUnit) { 189 AnnotatedUnit annotatedUnit = (AnnotatedUnit) unit; 190 unit = annotatedUnit.getActualUnit(); 191 annotation = annotatedUnit.getAnnotation(); 192 } 193 String mapSymbol = symbolMap.getSymbol(unit); 194 if (mapSymbol != null) { 195 symbol = mapSymbol; 196 } else if (unit instanceof TransformedUnit) { 197 final StringBuilder temp = new StringBuilder(); 198 final Unit<?> parentUnit = ((TransformedUnit) unit).getParentUnit(); 199 final UnitConverter converter = unit.getConverterTo(parentUnit); 200 final boolean printSeparator = !parentUnit.equals(ONE); 201 202 format(parentUnit, temp); 203 formatConverter(converter, printSeparator, temp, symbolMap); 204 205 symbol = temp; 206 } else if (unit.getBaseUnits() != null) { 207 Map<? extends AbstractUnit<?>, Integer> productUnits = unit.getBaseUnits(); 208 StringBuffer app = new StringBuffer(); 209 for (AbstractUnit<?> u : productUnits.keySet()) { 210 StringBuffer temp = new StringBuffer(); 211 temp = (StringBuffer) format(u, temp); 212 if ((temp.indexOf(".") >= 0) || (temp.indexOf("/") >= 0)) { 213 temp.insert(0, '('); 214 temp.append(')'); 215 } 216 int pow = productUnits.get(u); 217 int indexToAppend; 218 if (app.length() > 0) { // Not the first unit. 219 220 if (pow >= 0) { 221 222 if (app.indexOf("1/") >= 0) { 223 indexToAppend = app.indexOf("1/"); 224 app.replace(indexToAppend, indexToAppend + 2, "/"); 225 // this statement make sure that (1/y).x will be 226 // (x/y) 227 228 } else if (app.indexOf("/") >= 0) { 229 indexToAppend = app.indexOf("/"); 230 app.insert(indexToAppend, "."); 231 indexToAppend++; 232 // this statement make sure that (x/z).y will be 233 // (x.y/z) 234 235 } else { 236 app.append('.'); 237 indexToAppend = app.length(); 238 // this statement make sure that (x).y will be (x.y) 239 } 240 241 } else { 242 app.append('/'); 243 pow = -pow; 244 245 indexToAppend = app.length(); 246 // this statement make sure that (x).y^-z will be 247 // (x/y^z), where z would be added if it has a value 248 // different than 1. 249 } 250 251 } else { // First unit. 252 253 if (pow < 0) { 254 app.append("1/"); 255 pow = -pow; 256 // this statement make sure that x^-y will be (1/x^y), 257 // where z would be added if it has a value different 258 // than 1. 259 } 260 261 indexToAppend = app.length(); 262 } 263 264 app.insert(indexToAppend, temp); 265 266 if (pow != 1) { 267 app.append(Integer.toString(pow)); 268 // this statement make sure that the power will be added if 269 // it's different than 1. 270 } 271 } 272 symbol = app; 273 } else if (!unit.isSystemUnit() || unit.equals(SI.KILOGRAM)) { 274 final StringBuilder temp = new StringBuilder(); 275 UnitConverter converter; 276 boolean printSeparator; 277 if (unit.equals(SI.KILOGRAM)) { 278 // A special case because KILOGRAM is a BaseUnit instead of 279 // a transformed unit, for compatibility with existing SI 280 // unit system. 281 format(SI.GRAM, temp); 282 converter = MetricPrefix.KILO.getConverter(); 283 printSeparator = true; 284 } else { 285 Unit<?> parentUnit = unit.getSystemUnit(); 286 converter = unit.getConverterTo(parentUnit); 287 if (parentUnit.equals(SI.KILOGRAM)) { 288 // More special-case hackery to work around gram/kilogram 289 // inconsistency 290 parentUnit = SI.GRAM; 291 converter = converter.concatenate(MetricPrefix.KILO.getConverter()); 292 } 293 format(parentUnit, temp); 294 printSeparator = !parentUnit.equals(ONE); 295 } 296 formatConverter(converter, printSeparator, temp, symbolMap); 297 symbol = temp; 298 } else if (unit.getSymbol() != null) { 299 symbol = unit.getSymbol(); 300 } else { 301 throw new IllegalArgumentException("Cannot format the given Object as UCUM units (unsupported unit " 302 + unit.getClass().getName() + "). " 303 + "Custom units types should override the toString() method as the default implementation uses the UCUM format."); 304 } 305 306 appendable.append(symbol); 307 if (annotation != null && annotation.length() > 0) { 308 appendAnnotation(symbol, annotation, appendable); 309 } 310 311 return appendable; 312 } 313 314 public void label(Unit<?> unit, String label) { 315 throw new UnsupportedOperationException("label() not supported by this implementation"); 316 } 317 318 public boolean isLocaleSensitive() { 319 return false; 320 } 321 322 void appendAnnotation(CharSequence symbol, CharSequence annotation, Appendable appendable) throws IOException { 323 appendable.append('{'); 324 appendable.append(annotation); 325 appendable.append('}'); 326 } 327 328 // static final ResourceBundle.Control getControl(final String key) { 329 // return new ResourceBundle.Control() { 330 // @Override 331 // public List<Locale> getCandidateLocales(String baseName, Locale locale) { 332 // if (baseName == null) 333 // throw new NullPointerException(); 334 // if (locale.equals(new Locale(key))) { 335 // return Arrays.asList( 336 // locale, 337 // Locale.GERMANY, 338 // // no Locale.GERMAN here 339 // Locale.ROOT); 340 // } else if (locale.equals(Locale.GERMANY)) { 341 // return Arrays.asList( 342 // locale, 343 // // no Locale.GERMAN here 344 // Locale.ROOT); 345 // } 346 // return super.getCandidateLocales(baseName, locale); 347 // } 348 // }; 349 // } 350 351 // ///////////////// 352 // Inner classes // 353 // ///////////////// 354 355 /** 356 * Variant of unit representation in the UCUM standard 357 * 358 * @see <a href= 359 * "http://unitsofmeasure.org/ucum.html#section-Character-Set-and-Lexical-Rules"> 360 * UCUM - Character Set and Lexical Rules</a> 361 */ 362 public static enum Variant { 363 CASE_SENSITIVE, CASE_INSENSITIVE, PRINT 364 } 365 366 /** 367 * The Print format is used to output units according to the "print" column 368 * in the UCUM standard. Because "print" symbols in UCUM are not unique, 369 * this class of UCUMFormat may not be used for parsing, only for 370 * formatting. 371 */ 372 private static final class Print extends UCUMFormat { 373 374 /** 375 * 376 */ 377 // private static final long serialVersionUID = 2990875526976721414L; 378 private static final SymbolMap PRINT_SYMBOLS = SymbolMap.of(ResourceBundle.getBundle(BUNDLE_BASE + "_Print")); 379 private static final Print DEFAULT = new Print(PRINT_SYMBOLS); 380 381 public Print(SymbolMap symbols) { 382 super(symbols); 383 } 384 385 @Override 386 public Unit<? extends Quantity<?>> parse(CharSequence csq, ParsePosition pos) throws IllegalArgumentException { 387 throw new UnsupportedOperationException( 388 "The print format is for pretty-printing of units only. Parsing is not supported."); 389 } 390 391 @Override 392 void appendAnnotation(CharSequence symbol, CharSequence annotation, Appendable appendable) throws IOException { 393 if (symbol != null && symbol.length() > 0) { 394 appendable.append('('); 395 appendable.append(annotation); 396 appendable.append(')'); 397 } else { 398 appendable.append(annotation); 399 } 400 } 401 402 @Override 403 public Unit<? extends Quantity<?>> parse(CharSequence csq) throws IllegalArgumentException { 404 return parse(csq, new ParsePosition(0)); 405 406 } 407 } 408 409 /** 410 * The Parsing format outputs formats and parses units according to the 411 * "c/s" or "c/i" column in the UCUM standard, depending on which SymbolMap 412 * is passed to its constructor. 413 */ 414 private static final class Parsing extends UCUMFormat { 415 // private static final long serialVersionUID = -922531801940132715L; 416 private static final SymbolMap CASE_SENSITIVE_SYMBOLS = SymbolMap 417 .of(ResourceBundle.getBundle(BUNDLE_BASE + "_CS", new ResourceBundle.Control() { 418 @Override 419 public List<Locale> getCandidateLocales(String baseName, Locale locale) { 420 if (baseName == null) 421 throw new NullPointerException(); 422 if (locale.equals(new Locale("", "CS"))) { 423 return Arrays.asList(locale, Locale.ROOT); 424 } 425 return super.getCandidateLocales(baseName, locale); 426 } 427 })); 428 private static final SymbolMap CASE_INSENSITIVE_SYMBOLS = SymbolMap 429 .of(ResourceBundle.getBundle(BUNDLE_BASE + "_CI", new ResourceBundle.Control() { 430 @Override 431 public List<Locale> getCandidateLocales(String baseName, Locale locale) { 432 if (baseName == null) 433 throw new NullPointerException(); 434 if (locale.equals(new Locale("", "CI"))) { 435 return Arrays.asList(locale, Locale.ROOT); 436 } else if (locale.equals(Locale.GERMANY)) { // TODO 437 // why 438 // GERMANY? 439 return Arrays.asList(locale, 440 // no Locale.GERMAN here 441 Locale.ROOT); 442 } 443 return super.getCandidateLocales(baseName, locale); 444 } 445 })); 446 private static final Parsing DEFAULT_CS = new Parsing(CASE_SENSITIVE_SYMBOLS, true); 447 private static final Parsing DEFAULT_CI = new Parsing(CASE_INSENSITIVE_SYMBOLS, false); 448 private final boolean caseSensitive; 449 450 public Parsing(SymbolMap symbols, boolean caseSensitive) { 451 super(symbols); 452 this.caseSensitive = caseSensitive; 453 } 454 455 @Override 456 public Unit<? extends Quantity<?>> parse(CharSequence csq, ParsePosition cursor) throws ParserException { 457 // Parsing reads the whole character sequence from the parse 458 // position. 459 int start = cursor.getIndex(); 460 int end = csq.length(); 461 if (end <= start) { 462 return ONE; 463 } 464 String source = csq.subSequence(start, end).toString().trim(); 465 if (source.length() == 0) { 466 return ONE; 467 } 468 if (!caseSensitive) { 469 source = source.toUpperCase(); 470 } 471 UCUMFormatParser parser = new UCUMFormatParser(symbolMap, new ByteArrayInputStream(source.getBytes())); 472 try { 473 Unit<?> result = parser.parseUnit(); 474 cursor.setIndex(end); 475 return result; 476 } catch (TokenException e) { 477 if (e.currentToken != null) { 478 cursor.setErrorIndex(start + e.currentToken.endColumn); 479 } else { 480 cursor.setErrorIndex(start); 481 } 482 throw new ParserException(e); 483 } catch (TokenMgrError e) { 484 cursor.setErrorIndex(start); 485 throw new IllegalArgumentException(e.getMessage()); 486 } 487 } 488 489 @Override 490 public Unit<? extends Quantity<?>> parse(CharSequence csq) throws ParserException { 491 return parse(csq, new ParsePosition(0)); 492 } 493 } 494}