001/* 002 * Units of Measurement Reference Implementation 003 * Copyright (c) 2005-2021, 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.FormatBehavior.LOCALE_NEUTRAL; 033import static tech.units.indriya.format.CommonFormatter.parseMixedAsLeading; 034import static tech.units.indriya.format.CommonFormatter.parseMixedAsPrimary; 035 036import java.io.IOException; 037import java.text.NumberFormat; 038import java.text.ParsePosition; 039import java.util.Locale; 040import java.util.Objects; 041 042import javax.measure.Quantity; 043import javax.measure.Unit; 044import javax.measure.format.MeasurementParseException; 045import javax.measure.format.UnitFormat; 046 047import tech.units.indriya.AbstractUnit; 048import tech.units.indriya.quantity.CompoundQuantity; 049import tech.units.indriya.quantity.MixedQuantity; 050import tech.units.indriya.quantity.Quantities; 051 052/** 053 * An implementation of {@link javax.measure.format.QuantityFormat QuantityFormat} combining {@linkplain NumberFormat} and {@link UnitFormat} 054 * separated by a delimiter. 055 * 056 * @author <a href="mailto:werner@units.tech">Werner Keil</a> 057 * @author <a href="mailto:thodoris.bais@gmail.com">Thodoris Bais</a> 058 * 059 * @version 2.4, $Date: 2021-01-26 $ 060 * @since 2.0 061 */ 062@SuppressWarnings({ "rawtypes", "unchecked" }) 063public class NumberDelimiterQuantityFormat extends AbstractQuantityFormat { 064 065 /** 066 * Holds the default format instance (SimpleUnitFormat). 067 */ 068 private static final NumberDelimiterQuantityFormat SIMPLE_INSTANCE = new NumberDelimiterQuantityFormat.Builder() 069 .setNumberFormat(NumberFormat.getInstance(Locale.ROOT)).setUnitFormat(SimpleUnitFormat.getInstance()).build(); 070 071 /** 072 * Holds the localized format instance. 073 */ 074 private static final NumberDelimiterQuantityFormat LOCAL_INSTANCE = new NumberDelimiterQuantityFormat.Builder() 075 .setNumberFormat(NumberFormat.getInstance()) 076 .setUnitFormat(LocalUnitFormat.getInstance()) 077 .setLocaleSensitive(true).build(); 078 079 /** 080 * 081 */ 082 private static final long serialVersionUID = 3546952599885869402L; 083 084 private transient NumberFormat numberFormat; 085 private transient UnitFormat unitFormat; 086 private transient Unit primaryUnit; 087 private String delimiter; 088 private String mixDelimiter; 089 private boolean localeSensitive; 090 091 private NumberDelimiterQuantityFormat() { 092 /* private constructor */ } 093 094 /** 095 * A fluent Builder to easily create new instances of <code>NumberDelimiterQuantityFormat</code>. 096 */ 097 public static class Builder { 098 099 private transient NumberFormat numberFormat; 100 private transient UnitFormat unitFormat; 101 private transient Unit primaryUnit; 102 private transient String delimiter = DEFAULT_DELIMITER; 103 private transient String mixedRadixDelimiter; 104 private boolean localeSensitive; 105 106 /** 107 * Sets the numberFormat parameter to the given {@code NumberFormat}. 108 * @param numberFormat the {@link NumberFormat} 109 * @throws NullPointerException if {@code numberFormat} is {@code null} 110 * @return this {@code NumberDelimiterQuantityFormat.Builder} 111 */ 112 public Builder setNumberFormat(NumberFormat numberFormat) { 113 Objects.requireNonNull(numberFormat); 114 this.numberFormat = numberFormat; 115 return this; 116 } 117 118 /** 119 * Sets the unitFormat parameter to the given {@code UnitFormat}. 120 * @param unitFormat the {@link UnitFormat} 121 * @throws NullPointerException if {@code unitFormat} is {@code null} 122 * @return this {@code NumberDelimiterQuantityFormat.Builder} 123 */ 124 public Builder setUnitFormat(UnitFormat unitFormat) { 125 Objects.requireNonNull(unitFormat); 126 this.unitFormat = unitFormat; 127 return this; 128 } 129 130 /** 131 * Sets the primary unit parameter for multiple {@link MixedQuantity mixed quantities} to the given {@code Unit}. 132 * @param primary the primary {@link Unit} 133 * @throws NullPointerException if {@code primary} is {@code null} 134 * @return this {@code NumberDelimiterQuantityFormat.Builder} 135 */ 136 public Builder setPrimaryUnit(final Unit primary) { 137 Objects.requireNonNull(primary); 138 this.primaryUnit = primary; 139 return this; 140 } 141 142 /** 143 * Sets the delimiter between a {@code NumberFormat} and {@code UnitFormat}. 144 * @param delimiter the delimiter to use 145 * @throws NullPointerException if {@code delimiter} is {@code null} 146 * @return this {@code NumberDelimiterQuantityFormat.Builder} 147 */ 148 public Builder setDelimiter(String delimiter) { 149 Objects.requireNonNull(delimiter); 150 this.delimiter = delimiter; 151 return this; 152 } 153 154 /** 155 * Sets the radix delimiter between multiple {@link MixedQuantity mixed quantities}. 156 * @param radixPartsDelimiter the delimiter to use 157 * @throws NullPointerException if {@code radixPartsDelimiter} is {@code null} 158 * @return this {@code NumberDelimiterQuantityFormat.Builder} 159 */ 160 public Builder setRadixPartsDelimiter(String radixPartsDelimiter) { 161 Objects.requireNonNull(radixPartsDelimiter); 162 this.mixedRadixDelimiter = radixPartsDelimiter; 163 return this; 164 } 165 166 /** 167 * Sets the {@code localeSensitive} flag. 168 * @param localeSensitive the flag, if the {@code NumberDelimiterQuantityFormat} to be built will depend on a {@code Locale} to perform its tasks. 169 * @return this {@code NumberDelimiterQuantityFormat.Builder} 170 * @see UnitFormat#isLocaleSensitive() 171 */ 172 public Builder setLocaleSensitive(boolean localeSensitive) { 173 this.localeSensitive = localeSensitive; 174 return this; 175 } 176 177 public NumberDelimiterQuantityFormat build() { 178 NumberDelimiterQuantityFormat quantityFormat = new NumberDelimiterQuantityFormat(); 179 quantityFormat.numberFormat = this.numberFormat; 180 quantityFormat.unitFormat = this.unitFormat; 181 quantityFormat.primaryUnit = this.primaryUnit; 182 quantityFormat.delimiter = this.delimiter; 183 quantityFormat.mixDelimiter = this.mixedRadixDelimiter; 184 quantityFormat.localeSensitive = this.localeSensitive; 185 return quantityFormat; 186 } 187 } 188 189 /** 190 * Returns an instance of {@link NumberDelimiterQuantityFormat} with a particular {@link FormatBehavior}, either locale-sensitive or locale-neutral. 191 * For example: <code>NumberDelimiterQuantityFormat.getInstance(LOCALE_NEUTRAL))</code> returns<br> 192 * <code>new NumberDelimiterQuantityFormat.Builder() 193 .setNumberFormat(NumberFormat.getInstance(Locale.ROOT)).setUnitFormat(SimpleUnitFormat.getInstance()).build();</code> 194 * 195 * @param behavior 196 * the format behavior to apply. 197 * @return <code>NumberDelimiterQuantityFormat.getInstance(NumberFormat.getInstance(), UnitFormat.getInstance())</code> 198 */ 199 public static NumberDelimiterQuantityFormat getInstance(final FormatBehavior behavior) { 200 switch (behavior) { 201 case LOCALE_SENSITIVE: 202 return LOCAL_INSTANCE; 203 case LOCALE_NEUTRAL: 204 default: 205 return SIMPLE_INSTANCE; 206 } 207 } 208 209 /** 210 * Returns a new instance of {@link Builder}. 211 * 212 * @return a new {@link Builder}. 213 */ 214 public static final Builder builder() { 215 return new Builder(); 216 } 217 218 /** 219 * Returns the default format. 220 * 221 * @return the desired format. 222 */ 223 public static NumberDelimiterQuantityFormat getInstance() { 224 return getInstance(LOCALE_NEUTRAL); 225 } 226 227 /** 228 * Returns the quantity format using the specified number format and unit format (the number and unit are separated by one space). 229 * 230 * @param numberFormat 231 * the number format. 232 * @param unitFormat 233 * the unit format. 234 * @return the corresponding format. 235 */ 236 public static NumberDelimiterQuantityFormat getInstance(NumberFormat numberFormat, UnitFormat unitFormat) { 237 return new NumberDelimiterQuantityFormat.Builder().setNumberFormat(numberFormat).setUnitFormat(unitFormat).build(); 238 } 239 240 @Override 241 public Appendable format(Quantity<?> quantity, Appendable dest) throws IOException { 242 int fract = 0; 243 /* 244 if (quantity instanceof MixedQuantity) { 245 final MixedQuantity<?> compQuant = (MixedQuantity<?>) quantity; 246 if (compQuant.getUnit() instanceof MixedUnit) { 247 final MixedUnit<?> compUnit = (MixedUnit<?>) compQuant.getUnit(); 248 final Number[] values = compQuant.getValues(); 249 if (values.length == compUnit.getUnits().size()) { 250 final StringBuffer sb = new StringBuffer(); // we use StringBuffer here because of java.text.Format compatibility 251 for (int i = 0; i < values.length; i++) { 252 if (values[i] != null) { 253 fract = getFractionDigitsCount(values[i].doubleValue()); 254 } else { 255 fract = 0; 256 } 257 if (fract > 1) { 258 numberFormat.setMaximumFractionDigits(fract + 1); 259 } 260 sb.append(numberFormat.format(values[i])); 261 sb.append(delimiter); 262 sb.append(unitFormat.format(compUnit.getUnits().get(i))); 263 if (i < values.length - 1) { 264 sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not 265 // formatting 266 } 267 } 268 return sb; 269 } else { 270 throw new IllegalArgumentException( 271 String.format("%s values don't match %s in mixed unit", values.length, compUnit.getUnits().size())); 272 } 273 } else { 274 throw new MeasurementException("A mixed quantity must contain a mixed unit"); 275 } 276 } else { 277 */ 278 if (quantity != null && quantity.getValue() != null) { 279 fract = getFractionDigitsCount(quantity.getValue().doubleValue()); 280 } 281 if (fract > 1) { 282 numberFormat.setMaximumFractionDigits(fract + 1); 283 } 284 dest.append(numberFormat.format(quantity.getValue())); 285 if (quantity.getUnit().equals(AbstractUnit.ONE)) 286 return dest; 287 dest.append(delimiter); 288 return unitFormat.format(quantity.getUnit(), dest); 289 //} 290 } 291 292 @Override 293 public Quantity<?> parse(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException, MeasurementParseException { 294 final String str = csq.toString(); 295 final int index = cursor.getIndex(); 296 if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) { 297 if (primaryUnit != null) { 298 return parseMixedAsPrimary(str, numberFormat, unitFormat, primaryUnit, delimiter, mixDelimiter, index); 299 } else { 300 return parseMixedAsLeading(str, numberFormat, unitFormat, delimiter, mixDelimiter, index); 301 } 302 } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) { 303 if (primaryUnit != null) { 304 return parseMixedAsPrimary(str, numberFormat, unitFormat, primaryUnit, delimiter, index); 305 } else { 306 return parseMixedAsLeading(str, numberFormat, unitFormat, delimiter, index); 307 } 308 } 309 final Number number = numberFormat.parse(str, cursor); 310 if (number == null) 311 throw new IllegalArgumentException("Number cannot be parsed"); 312 final String[] parts = str.substring(index).split(delimiter); 313 if (parts.length < 2) { 314 throw new IllegalArgumentException("No Unit found"); 315 } 316 final Unit unit = unitFormat.parse(parts[1]); 317 return Quantities.getQuantity(number, unit); 318 } 319 320 @Override 321 protected Quantity<?> parse(CharSequence csq, int index) throws IllegalArgumentException, MeasurementParseException { 322 return parse(csq, new ParsePosition(index)); 323 } 324 325 @Override 326 public Quantity<?> parse(CharSequence csq) throws IllegalArgumentException, MeasurementParseException { 327 return parse(csq, 0); 328 } 329 330 @Override 331 public String toString() { 332 return getClass().getSimpleName(); 333 } 334 335 @Override 336 public boolean isLocaleSensitive() { 337 return localeSensitive; 338 } 339 340 @Override 341 protected StringBuffer formatMixed(MixedQuantity<?> comp, StringBuffer dest) { 342 final StringBuffer sb = new StringBuffer(); 343 int i = 0; 344 for (Quantity<?> q : comp.getQuantities()) { 345 sb.append(format(q)); 346 if (i < comp.getQuantities().size() - 1 ) { 347 sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not 348 } 349 i++; 350 } 351 return sb; 352 } 353 354 public MixedQuantity<?> parseMixed(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException, MeasurementParseException { 355 final String str = csq.toString(); 356 final int index = cursor.getIndex(); 357 if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) { 358 return CommonFormatter.parseMixed(str, numberFormat, unitFormat, delimiter, mixDelimiter, index); 359 } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) { 360 return CommonFormatter.parseMixed(str, numberFormat, unitFormat, delimiter, index); 361 } 362 final Number number = numberFormat.parse(str, cursor); 363 if (number == null) 364 throw new IllegalArgumentException("Number cannot be parsed"); 365 final String[] parts = str.substring(index).split(delimiter); 366 if (parts.length < 2) { 367 throw new IllegalArgumentException("No Unit found"); 368 } 369 final Unit unit = unitFormat.parse(parts[1]); 370 return MixedQuantity.of(Quantities.getQuantity(number, unit)); 371 } 372 373 protected MixedQuantity<?> parseMixed(CharSequence csq, int index) throws IllegalArgumentException, MeasurementParseException { 374 return parseMixed(csq, new ParsePosition(index)); 375 } 376 377 public MixedQuantity<?> parseMixed(CharSequence csq) throws IllegalArgumentException, MeasurementParseException { 378 return parseMixed(csq, 0); 379 } 380 381 @Override 382 @Deprecated 383 protected StringBuffer formatCompound(CompoundQuantity<?> comp, StringBuffer dest) { 384 final StringBuffer sb = new StringBuffer(); 385 int i = 0; 386 for (Quantity<?> q : comp.getQuantities()) { 387 sb.append(format(q)); 388 if (i < comp.getQuantities().size() - 1 ) { 389 sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not 390 } 391 i++; 392 } 393 return sb; 394 } 395 396 @Deprecated 397 public CompoundQuantity<?> parseCompound(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException, MeasurementParseException { 398 final String str = csq.toString(); 399 final int index = cursor.getIndex(); 400 if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) { 401 return CommonFormatterOld.parseCompound(str, numberFormat, unitFormat, delimiter, mixDelimiter, index); 402 } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) { 403 return CommonFormatterOld.parseCompound(str, numberFormat, unitFormat, delimiter, index); 404 } 405 final Number number = numberFormat.parse(str, cursor); 406 if (number == null) 407 throw new IllegalArgumentException("Number cannot be parsed"); 408 final String[] parts = str.substring(index).split(delimiter); 409 if (parts.length < 2) { 410 throw new IllegalArgumentException("No Unit found"); 411 } 412 final Unit unit = unitFormat.parse(parts[1]); 413 return CompoundQuantity.of(Quantities.getQuantity(number, unit)); 414 } 415 416 @Deprecated 417 protected CompoundQuantity<?> parseCompound(CharSequence csq, int index) throws IllegalArgumentException, MeasurementParseException { 418 return parseCompound(csq, new ParsePosition(index)); 419 } 420 421 @Deprecated 422 public CompoundQuantity<?> parseCompound(CharSequence csq) throws IllegalArgumentException, MeasurementParseException { 423 return parseCompound(csq, 0); 424 } 425 426 // Private helper methods 427 428 private static int getFractionDigitsCount(double d) { 429 if (d >= 1) { // we only need the fraction digits 430 d = d - (long) d; 431 } 432 if (d == 0) { // nothing to count 433 return 0; 434 } 435 d *= 10; // shifts 1 digit to left 436 int count = 1; 437 while (d - (long) d != 0) { // keeps shifting until there are no more 438 // fractions 439 d *= 10; 440 count++; 441 } 442 return count; 443 } 444 445}