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