001/* 002 * Units of Measurement Reference Implementation 003 * Copyright (c) 2005-2024, 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.MixedQuantity; 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.9, $Date: 2024-10-12 $ 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 constructor */ 091 private NumberDelimiterQuantityFormat() { } 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 this.localeSensitive = unitFormat.isLocaleSensitive(); // adjusting localeSensitive based on 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 an instance of {@link NumberDelimiterQuantityFormat} with a particular {@link FormatBehavior}, either locale-sensitive or locale-neutral, 211 * and a desired number style.<br> 212 * For example: <code>NumberDelimiterQuantityFormat.getInstance(LOCALE_NEUTRAL))</code> returns<br> 213 * <code>new NumberDelimiterQuantityFormat.Builder().setNumberFormat(NumberFormat.getInstance(Locale.ROOT)).setUnitFormat(SimpleUnitFormat.getInstance()).build();</code> 214 * @implNote 215 * Note: <code>numberStyle</code> will be ignored before Java 17. Although the <code>COMPACT</code> {@linkplain NumberFormat} is already available from Java 12, Indriya supports major LTS versions like 8, 11 or 17. 216 * 217 * @param behavior 218 * the format behavior to apply. 219 * @param numberStyle 220 * the number format style to apply. 221 * @return <code>NumberDelimiterQuantityFormat.getInstance(NumberFormat.getInstance(), UnitFormat.getInstance())</code> 222 * @since 2.9 223 */ 224 public static NumberDelimiterQuantityFormat getInstance(final FormatBehavior behavior, int numberStyle) { 225 return getInstance(behavior); 226 } 227 228 /** 229 * Returns a new instance of {@link Builder}. 230 * 231 * @return a new {@link Builder}. 232 */ 233 public static final Builder builder() { 234 return new Builder(); 235 } 236 237 /** 238 * Returns the default format. 239 * 240 * @return the desired format. 241 */ 242 public static NumberDelimiterQuantityFormat getInstance() { 243 return getInstance(LOCALE_NEUTRAL); 244 } 245 246 /** 247 * Returns the quantity format using the specified number format and unit format (the number and unit are separated by one space). 248 * 249 * @param numberFormat 250 * the number format. 251 * @param unitFormat 252 * the unit format. 253 * @return the corresponding format. 254 */ 255 public static NumberDelimiterQuantityFormat getInstance(NumberFormat numberFormat, UnitFormat unitFormat) { 256 return new NumberDelimiterQuantityFormat.Builder().setNumberFormat(numberFormat).setUnitFormat(unitFormat).build(); 257 } 258 259 @Override 260 public Appendable format(Quantity<?> quantity, Appendable dest) throws IOException { 261 int fract = 0; 262 /* 263 if (quantity instanceof MixedQuantity) { 264 final MixedQuantity<?> compQuant = (MixedQuantity<?>) quantity; 265 if (compQuant.getUnit() instanceof MixedUnit) { 266 final MixedUnit<?> compUnit = (MixedUnit<?>) compQuant.getUnit(); 267 final Number[] values = compQuant.getValues(); 268 if (values.length == compUnit.getUnits().size()) { 269 final StringBuffer sb = new StringBuffer(); // we use StringBuffer here because of java.text.Format compatibility 270 for (int i = 0; i < values.length; i++) { 271 if (values[i] != null) { 272 fract = getFractionDigitsCount(values[i].doubleValue()); 273 } else { 274 fract = 0; 275 } 276 if (fract > 1) { 277 numberFormat.setMaximumFractionDigits(fract + 1); 278 } 279 sb.append(numberFormat.format(values[i])); 280 sb.append(delimiter); 281 sb.append(unitFormat.format(compUnit.getUnits().get(i))); 282 if (i < values.length - 1) { 283 sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not 284 // formatting 285 } 286 } 287 return sb; 288 } else { 289 throw new IllegalArgumentException( 290 String.format("%s values don't match %s in mixed unit", values.length, compUnit.getUnits().size())); 291 } 292 } else { 293 throw new MeasurementException("A mixed quantity must contain a mixed unit"); 294 } 295 } else { 296 */ 297 if (quantity != null && quantity.getValue() != null) { 298 fract = getFractionDigitsCount(quantity.getValue().doubleValue()); 299 } 300 if (fract > 1) { 301 numberFormat.setMaximumFractionDigits(fract + 1); 302 } 303 dest.append(numberFormat.format(quantity.getValue())); 304 if (quantity.getUnit().equals(AbstractUnit.ONE)) 305 return dest; 306 dest.append(delimiter); 307 return unitFormat.format(quantity.getUnit(), dest); 308 //} 309 } 310 311 @Override 312 public Quantity<?> parse(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException, MeasurementParseException { 313 final String str = csq.toString(); 314 final int index = cursor.getIndex(); 315 if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) { 316 if (primaryUnit != null) { 317 return parseMixedAsPrimary(str, numberFormat, unitFormat, primaryUnit, delimiter, mixDelimiter, index); 318 } else { 319 return parseMixedAsLeading(str, numberFormat, unitFormat, delimiter, mixDelimiter, index); 320 } 321 } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) { 322 if (primaryUnit != null) { 323 return parseMixedAsPrimary(str, numberFormat, unitFormat, primaryUnit, delimiter, index); 324 } else { 325 return parseMixedAsLeading(str, numberFormat, unitFormat, delimiter, index); 326 } 327 } 328 final Number number = numberFormat.parse(str, cursor); 329 if (number == null) 330 throw new IllegalArgumentException("Number cannot be parsed"); 331 final String[] parts = str.substring(index).split(delimiter); 332 if (parts.length < 2) { 333 throw new IllegalArgumentException("No Unit found"); 334 } 335 final Unit unit = unitFormat.parse(parts[1]); 336 return Quantities.getQuantity(number, unit); 337 } 338 339 @Override 340 protected Quantity<?> parse(CharSequence csq, int index) throws IllegalArgumentException, MeasurementParseException { 341 return parse(csq, new ParsePosition(index)); 342 } 343 344 @Override 345 public Quantity<?> parse(CharSequence csq) throws IllegalArgumentException, MeasurementParseException { 346 return parse(csq, 0); 347 } 348 349 @Override 350 public String toString() { 351 return getClass().getSimpleName(); 352 } 353 354 @Override 355 public boolean isLocaleSensitive() { 356 return localeSensitive; 357 } 358 359 @Override 360 protected StringBuffer formatMixed(MixedQuantity<?> comp, StringBuffer dest) { 361 final StringBuffer sb = new StringBuffer(); 362 int i = 0; 363 for (Quantity<?> q : comp.getQuantities()) { 364 sb.append(format(q)); 365 if (i < comp.getQuantities().size() - 1 ) { 366 sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not 367 } 368 i++; 369 } 370 return sb; 371 } 372 373 public MixedQuantity<?> parseMixed(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException, MeasurementParseException { 374 final String str = csq.toString(); 375 final int index = cursor.getIndex(); 376 if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) { 377 return CommonFormatter.parseMixed(str, numberFormat, unitFormat, delimiter, mixDelimiter, index); 378 } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) { 379 return CommonFormatter.parseMixed(str, numberFormat, unitFormat, delimiter, index); 380 } 381 final Number number = numberFormat.parse(str, cursor); 382 if (number == null) 383 throw new IllegalArgumentException("Number cannot be parsed"); 384 final String[] parts = str.substring(index).split(delimiter); 385 if (parts.length < 2) { 386 throw new IllegalArgumentException("No Unit found"); 387 } 388 final Unit unit = unitFormat.parse(parts[1]); 389 return MixedQuantity.of(Quantities.getQuantity(number, unit)); 390 } 391 392 protected MixedQuantity<?> parseMixed(CharSequence csq, int index) throws IllegalArgumentException, MeasurementParseException { 393 return parseMixed(csq, new ParsePosition(index)); 394 } 395 396 public MixedQuantity<?> parseMixed(CharSequence csq) throws IllegalArgumentException, MeasurementParseException { 397 return parseMixed(csq, 0); 398 } 399 400 // Private helper methods 401 402 private static int getFractionDigitsCount(double d) { 403 if (d >= 1) { // we only need the fraction digits 404 d = d - (long) d; 405 } 406 if (d == 0) { // nothing to count 407 return 0; 408 } 409 d *= 10; // shifts 1 digit to left 410 int count = 1; 411 while (d - (long) d != 0) { // keeps shifting until there are no more 412 // fractions 413 d *= 10; 414 count++; 415 } 416 return count; 417 } 418}