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.7, $Date: 2021-05-24 $ 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 constructor */ 092 private NumberDelimiterQuantityFormat() { } 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 this.localeSensitive = unitFormat.isLocaleSensitive(); // adjusting localeSensitive based on UnitFormat 128 return this; 129 } 130 131 /** 132 * Sets the primary unit parameter for multiple {@link MixedQuantity mixed quantities} to the given {@code Unit}. 133 * @param primary the primary {@link Unit} 134 * @throws NullPointerException if {@code primary} is {@code null} 135 * @return this {@code NumberDelimiterQuantityFormat.Builder} 136 */ 137 public Builder setPrimaryUnit(final Unit primary) { 138 Objects.requireNonNull(primary); 139 this.primaryUnit = primary; 140 return this; 141 } 142 143 /** 144 * Sets the delimiter between a {@code NumberFormat} and {@code UnitFormat}. 145 * @param delimiter the delimiter to use 146 * @throws NullPointerException if {@code delimiter} is {@code null} 147 * @return this {@code NumberDelimiterQuantityFormat.Builder} 148 */ 149 public Builder setDelimiter(String delimiter) { 150 Objects.requireNonNull(delimiter); 151 this.delimiter = delimiter; 152 return this; 153 } 154 155 /** 156 * Sets the radix delimiter between multiple {@link MixedQuantity mixed quantities}. 157 * @param radixPartsDelimiter the delimiter to use 158 * @throws NullPointerException if {@code radixPartsDelimiter} is {@code null} 159 * @return this {@code NumberDelimiterQuantityFormat.Builder} 160 */ 161 public Builder setRadixPartsDelimiter(String radixPartsDelimiter) { 162 Objects.requireNonNull(radixPartsDelimiter); 163 this.mixedRadixDelimiter = radixPartsDelimiter; 164 return this; 165 } 166 167 /** 168 * Sets the {@code localeSensitive} flag. 169 * @param localeSensitive the flag, if the {@code NumberDelimiterQuantityFormat} to be built will depend on a {@code Locale} to perform its tasks. 170 * @return this {@code NumberDelimiterQuantityFormat.Builder} 171 * @see UnitFormat#isLocaleSensitive() 172 */ 173 public Builder setLocaleSensitive(boolean localeSensitive) { 174 this.localeSensitive = localeSensitive; 175 return this; 176 } 177 178 public NumberDelimiterQuantityFormat build() { 179 NumberDelimiterQuantityFormat quantityFormat = new NumberDelimiterQuantityFormat(); 180 quantityFormat.numberFormat = this.numberFormat; 181 quantityFormat.unitFormat = this.unitFormat; 182 quantityFormat.primaryUnit = this.primaryUnit; 183 quantityFormat.delimiter = this.delimiter; 184 quantityFormat.mixDelimiter = this.mixedRadixDelimiter; 185 quantityFormat.localeSensitive = this.localeSensitive; 186 return quantityFormat; 187 } 188 } 189 190 /** 191 * Returns an instance of {@link NumberDelimiterQuantityFormat} with a particular {@link FormatBehavior}, either locale-sensitive or locale-neutral. 192 * For example: <code>NumberDelimiterQuantityFormat.getInstance(LOCALE_NEUTRAL))</code> returns<br> 193 * <code>new NumberDelimiterQuantityFormat.Builder() 194 .setNumberFormat(NumberFormat.getInstance(Locale.ROOT)).setUnitFormat(SimpleUnitFormat.getInstance()).build();</code> 195 * 196 * @param behavior 197 * the format behavior to apply. 198 * @return <code>NumberDelimiterQuantityFormat.getInstance(NumberFormat.getInstance(), UnitFormat.getInstance())</code> 199 */ 200 public static NumberDelimiterQuantityFormat getInstance(final FormatBehavior behavior) { 201 switch (behavior) { 202 case LOCALE_SENSITIVE: 203 return LOCAL_INSTANCE; 204 case LOCALE_NEUTRAL: 205 default: 206 return SIMPLE_INSTANCE; 207 } 208 } 209 210 /** 211 * Returns a new instance of {@link Builder}. 212 * 213 * @return a new {@link Builder}. 214 */ 215 public static final Builder builder() { 216 return new Builder(); 217 } 218 219 /** 220 * Returns the default format. 221 * 222 * @return the desired format. 223 */ 224 public static NumberDelimiterQuantityFormat getInstance() { 225 return getInstance(LOCALE_NEUTRAL); 226 } 227 228 /** 229 * Returns the quantity format using the specified number format and unit format (the number and unit are separated by one space). 230 * 231 * @param numberFormat 232 * the number format. 233 * @param unitFormat 234 * the unit format. 235 * @return the corresponding format. 236 */ 237 public static NumberDelimiterQuantityFormat getInstance(NumberFormat numberFormat, UnitFormat unitFormat) { 238 return new NumberDelimiterQuantityFormat.Builder().setNumberFormat(numberFormat).setUnitFormat(unitFormat).build(); 239 } 240 241 @Override 242 public Appendable format(Quantity<?> quantity, Appendable dest) throws IOException { 243 int fract = 0; 244 /* 245 if (quantity instanceof MixedQuantity) { 246 final MixedQuantity<?> compQuant = (MixedQuantity<?>) quantity; 247 if (compQuant.getUnit() instanceof MixedUnit) { 248 final MixedUnit<?> compUnit = (MixedUnit<?>) compQuant.getUnit(); 249 final Number[] values = compQuant.getValues(); 250 if (values.length == compUnit.getUnits().size()) { 251 final StringBuffer sb = new StringBuffer(); // we use StringBuffer here because of java.text.Format compatibility 252 for (int i = 0; i < values.length; i++) { 253 if (values[i] != null) { 254 fract = getFractionDigitsCount(values[i].doubleValue()); 255 } else { 256 fract = 0; 257 } 258 if (fract > 1) { 259 numberFormat.setMaximumFractionDigits(fract + 1); 260 } 261 sb.append(numberFormat.format(values[i])); 262 sb.append(delimiter); 263 sb.append(unitFormat.format(compUnit.getUnits().get(i))); 264 if (i < values.length - 1) { 265 sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not 266 // formatting 267 } 268 } 269 return sb; 270 } else { 271 throw new IllegalArgumentException( 272 String.format("%s values don't match %s in mixed unit", values.length, compUnit.getUnits().size())); 273 } 274 } else { 275 throw new MeasurementException("A mixed quantity must contain a mixed unit"); 276 } 277 } else { 278 */ 279 if (quantity != null && quantity.getValue() != null) { 280 fract = getFractionDigitsCount(quantity.getValue().doubleValue()); 281 } 282 if (fract > 1) { 283 numberFormat.setMaximumFractionDigits(fract + 1); 284 } 285 dest.append(numberFormat.format(quantity.getValue())); 286 if (quantity.getUnit().equals(AbstractUnit.ONE)) 287 return dest; 288 dest.append(delimiter); 289 return unitFormat.format(quantity.getUnit(), dest); 290 //} 291 } 292 293 @Override 294 public Quantity<?> parse(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException, MeasurementParseException { 295 final String str = csq.toString(); 296 final int index = cursor.getIndex(); 297 if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) { 298 if (primaryUnit != null) { 299 return parseMixedAsPrimary(str, numberFormat, unitFormat, primaryUnit, delimiter, mixDelimiter, index); 300 } else { 301 return parseMixedAsLeading(str, numberFormat, unitFormat, delimiter, mixDelimiter, index); 302 } 303 } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) { 304 if (primaryUnit != null) { 305 return parseMixedAsPrimary(str, numberFormat, unitFormat, primaryUnit, delimiter, index); 306 } else { 307 return parseMixedAsLeading(str, numberFormat, unitFormat, delimiter, index); 308 } 309 } 310 final Number number = numberFormat.parse(str, cursor); 311 if (number == null) 312 throw new IllegalArgumentException("Number cannot be parsed"); 313 final String[] parts = str.substring(index).split(delimiter); 314 if (parts.length < 2) { 315 throw new IllegalArgumentException("No Unit found"); 316 } 317 final Unit unit = unitFormat.parse(parts[1]); 318 return Quantities.getQuantity(number, unit); 319 } 320 321 @Override 322 protected Quantity<?> parse(CharSequence csq, int index) throws IllegalArgumentException, MeasurementParseException { 323 return parse(csq, new ParsePosition(index)); 324 } 325 326 @Override 327 public Quantity<?> parse(CharSequence csq) throws IllegalArgumentException, MeasurementParseException { 328 return parse(csq, 0); 329 } 330 331 @Override 332 public String toString() { 333 return getClass().getSimpleName(); 334 } 335 336 @Override 337 public boolean isLocaleSensitive() { 338 return localeSensitive; 339 } 340 341 @Override 342 protected StringBuffer formatMixed(MixedQuantity<?> comp, StringBuffer dest) { 343 final StringBuffer sb = new StringBuffer(); 344 int i = 0; 345 for (Quantity<?> q : comp.getQuantities()) { 346 sb.append(format(q)); 347 if (i < comp.getQuantities().size() - 1 ) { 348 sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not 349 } 350 i++; 351 } 352 return sb; 353 } 354 355 public MixedQuantity<?> parseMixed(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException, MeasurementParseException { 356 final String str = csq.toString(); 357 final int index = cursor.getIndex(); 358 if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) { 359 return CommonFormatter.parseMixed(str, numberFormat, unitFormat, delimiter, mixDelimiter, index); 360 } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) { 361 return CommonFormatter.parseMixed(str, numberFormat, unitFormat, delimiter, index); 362 } 363 final Number number = numberFormat.parse(str, cursor); 364 if (number == null) 365 throw new IllegalArgumentException("Number cannot be parsed"); 366 final String[] parts = str.substring(index).split(delimiter); 367 if (parts.length < 2) { 368 throw new IllegalArgumentException("No Unit found"); 369 } 370 final Unit unit = unitFormat.parse(parts[1]); 371 return MixedQuantity.of(Quantities.getQuantity(number, unit)); 372 } 373 374 protected MixedQuantity<?> parseMixed(CharSequence csq, int index) throws IllegalArgumentException, MeasurementParseException { 375 return parseMixed(csq, new ParsePosition(index)); 376 } 377 378 public MixedQuantity<?> parseMixed(CharSequence csq) throws IllegalArgumentException, MeasurementParseException { 379 return parseMixed(csq, 0); 380 } 381 382 @Override 383 @Deprecated 384 protected StringBuffer formatCompound(CompoundQuantity<?> comp, StringBuffer dest) { 385 final StringBuffer sb = new StringBuffer(); 386 int i = 0; 387 for (Quantity<?> q : comp.getQuantities()) { 388 sb.append(format(q)); 389 if (i < comp.getQuantities().size() - 1 ) { 390 sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not 391 } 392 i++; 393 } 394 return sb; 395 } 396 397 @Deprecated 398 public CompoundQuantity<?> parseCompound(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException, MeasurementParseException { 399 final String str = csq.toString(); 400 final int index = cursor.getIndex(); 401 if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) { 402 return CommonFormatterOld.parseCompound(str, numberFormat, unitFormat, delimiter, mixDelimiter, index); 403 } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) { 404 return CommonFormatterOld.parseCompound(str, numberFormat, unitFormat, delimiter, index); 405 } 406 final Number number = numberFormat.parse(str, cursor); 407 if (number == null) 408 throw new IllegalArgumentException("Number cannot be parsed"); 409 final String[] parts = str.substring(index).split(delimiter); 410 if (parts.length < 2) { 411 throw new IllegalArgumentException("No Unit found"); 412 } 413 final Unit unit = unitFormat.parse(parts[1]); 414 return CompoundQuantity.of(Quantities.getQuantity(number, unit)); 415 } 416 417 @Deprecated 418 protected CompoundQuantity<?> parseCompound(CharSequence csq, int index) throws IllegalArgumentException, MeasurementParseException { 419 return parseCompound(csq, new ParsePosition(index)); 420 } 421 422 @Deprecated 423 public CompoundQuantity<?> parseCompound(CharSequence csq) throws IllegalArgumentException, MeasurementParseException { 424 return parseCompound(csq, 0); 425 } 426 427 // Private helper methods 428 429 private static int getFractionDigitsCount(double d) { 430 if (d >= 1) { // we only need the fraction digits 431 d = d - (long) d; 432 } 433 if (d == 0) { // nothing to count 434 return 0; 435 } 436 d *= 10; // shifts 1 digit to left 437 int count = 1; 438 while (d - (long) d != 0) { // keeps shifting until there are no more 439 // fractions 440 d *= 10; 441 count++; 442 } 443 return count; 444 } 445}