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