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