1 /* 2 * Copyright (C) 2018 Alberto Irurueta Carro (alberto@irurueta.com) 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.irurueta.units; 17 18 import java.math.BigDecimal; 19 import java.math.RoundingMode; 20 import java.text.FieldPosition; 21 import java.text.MessageFormat; 22 import java.text.NumberFormat; 23 import java.text.ParseException; 24 import java.util.Locale; 25 import java.util.Objects; 26 27 /** 28 * Base class to format and parse a given measure using its value and unit. 29 * 30 * @param <M> type of measurement (i.e. Distance or Surface). 31 * @param <U> type of unit (i.e. DistanceUnit or SurfaceUnit). 32 */ 33 @SuppressWarnings("WeakerAccess") 34 public abstract class MeasureFormatter<M extends Measurement<U>, U extends Enum<?>> implements Cloneable { 35 36 /** 37 * Default pattern to format values and units together into a single string. 38 * {0} corresponds to the value, {1} corresponds to the unit part. 39 */ 40 public static final String DEFAULT_VALUE_AND_UNIT_FORMAT_PATTERN = "{0} {1}"; 41 42 /** 43 * Internal string formatter. 44 */ 45 NumberFormat numberFormat; 46 47 /** 48 * Internal string formatter. 49 */ 50 private MessageFormat format; 51 52 /** 53 * Internal locale. 54 */ 55 private Locale locale; 56 57 /** 58 * Pattern to format values and unit together into a single string. {0} corresponds to 59 * the value, {1} corresponds to the unit part. 60 */ 61 private String valueAndUnitFormatPattern; 62 63 /** 64 * Constructor. 65 */ 66 MeasureFormatter() { 67 numberFormat = NumberFormat.getInstance(); 68 locale = Locale.getDefault(); 69 valueAndUnitFormatPattern = DEFAULT_VALUE_AND_UNIT_FORMAT_PATTERN; 70 } 71 72 /** 73 * Constructor with locale. 74 * 75 * @param locale locale. 76 * @throws IllegalArgumentException if locale is null. 77 */ 78 MeasureFormatter(final Locale locale) { 79 if (locale == null) { 80 throw new IllegalArgumentException(); 81 } 82 83 numberFormat = NumberFormat.getInstance(locale); 84 this.locale = locale; 85 valueAndUnitFormatPattern = DEFAULT_VALUE_AND_UNIT_FORMAT_PATTERN; 86 } 87 88 /** 89 * Determines if two measure formatters are equal by comparing all of its internal 90 * parameters. 91 * 92 * @param obj another object to compare. 93 * @return true if provided object is assumed to be equal to this instance. 94 */ 95 @Override 96 public boolean equals(final Object obj) { 97 if (obj == null) { 98 return false; 99 } 100 if (!(obj instanceof MeasureFormatter)) { 101 return false; 102 } 103 if (this == obj) { 104 return true; 105 } 106 107 //noinspection unchecked 108 final var other = (MeasureFormatter<M, U>) obj; 109 return numberFormat.equals(other.numberFormat) && 110 locale.equals(other.locale) && 111 valueAndUnitFormatPattern.equals(other.valueAndUnitFormatPattern); 112 } 113 114 /** 115 * Hash code generated for this instance. 116 * Hash codes can be internally used by some collections to coarsely compare objects. 117 * 118 * @return hash code. 119 */ 120 @Override 121 public int hashCode() { 122 return Objects.hash(numberFormat, format, locale, valueAndUnitFormatPattern); 123 } 124 125 /** 126 * Formats provided measurement value and unit into a string representation. 127 * 128 * @param value a measurement value. 129 * @param unit a measurement unit. 130 * @return string representation of provided measurement value and unit. 131 */ 132 public String format(final Number value, final U unit) { 133 return MessageFormat.format(valueAndUnitFormatPattern, numberFormat.format(value), getUnitSymbol(unit)); 134 } 135 136 /** 137 * Formats provided measurement value and unit into a string representation 138 * and appends the result into provided string buffer. 139 * 140 * @param value a measurement value. 141 * @param unit a measurement unit. 142 * @param toAppendTo buffer to append the result to. 143 * @param pos field position where result will be appended. 144 * @return provided string buffer where result is appended. 145 */ 146 public StringBuffer format( 147 final Number value, final U unit, 148 final StringBuffer toAppendTo, final FieldPosition pos) { 149 if (format == null) { 150 format = new MessageFormat(valueAndUnitFormatPattern); 151 } 152 return format.format(new Object[]{numberFormat.format(value), getUnitSymbol(unit)}, toAppendTo, pos); 153 } 154 155 /** 156 * Formats provided measurement value and unit into a string representation. 157 * 158 * @param value a measurement value. 159 * @param unit a measurement unit. 160 * @return string representation of provided measurement value and unit. 161 */ 162 public String format(final double value, final U unit) { 163 return format(BigDecimal.valueOf(value), unit); 164 } 165 166 /** 167 * Formats provided measurement value and unit into a string representation 168 * and appends the result into provided string buffer. 169 * 170 * @param value a measurement value. 171 * @param unit a measurement unit. 172 * @param toAppendTo buffer to append the result to. 173 * @param pos field position where result will be appended. 174 * @return provided string buffer where result is appended. 175 */ 176 public StringBuffer format( 177 final double value, final U unit, final StringBuffer toAppendTo, final FieldPosition pos) { 178 return format(BigDecimal.valueOf(value), unit, toAppendTo, pos); 179 } 180 181 /** 182 * Formats provided measurement into a string representation. 183 * 184 * @param measurement a measurement. 185 * @return string representation of provided measurement. 186 */ 187 public String format(final M measurement) { 188 return format(measurement.getValue(), measurement.getUnit()); 189 } 190 191 /** 192 * Formats provided measurement into a string representation and appends the 193 * result into provided string buffer. 194 * 195 * @param measurement a measurement. 196 * @param toAppendTo buffer to append the result to. 197 * @param pos field position where result will be appended. 198 * @return provided string buffer where result is appended. 199 */ 200 public StringBuffer format( 201 final M measurement, final StringBuffer toAppendTo, final FieldPosition pos) { 202 return format(measurement.getValue(), measurement.getUnit(), toAppendTo, pos); 203 } 204 205 /** 206 * Formats and converts provided measurement value and unit using unit system 207 * assigned to locale of this instance (if no locale has been provided it 208 * is assumed that the system default locale is used). 209 * If provided value is too large for provided unit, this method will 210 * convert it to a more appropriate unit. 211 * 212 * @param value a measurement value. 213 * @param unit a measurement unit. 214 * @return a string representation of measurement value and unit. 215 */ 216 public String formatAndConvert(final Number value, final U unit) { 217 return formatAndConvert(value, unit, getUnitSystem()); 218 } 219 220 /** 221 * Formats and converts provided measurement value and unit using unit system 222 * assigned to locale of this instance (if no locale has been provided it 223 * is assumed that the system default locale is used). 224 * If provided value is too large for provided unit, this method will 225 * convert it to a more appropriate unit. 226 * 227 * @param value a measurement value. 228 * @param unit a measurement unit. 229 * @return a string representation of measurement value and unit. 230 */ 231 public String formatAndConvert(final double value, final U unit) { 232 return formatAndConvert(BigDecimal.valueOf(value), unit); 233 } 234 235 /** 236 * Formats and converts provided measurement using unit system assigned to 237 * locale of this instance (if no locale has been provided it is assumed 238 * that the system default locale is used). 239 * If provided measurement value is too large for its unit, this method 240 * will convert it to a more appropriate unit. 241 * 242 * @param measurement measurement to be formatted. 243 * @return a string representation of measurement value and unit. 244 */ 245 public String formatAndConvert(final M measurement) { 246 return formatAndConvert(measurement.getValue(), measurement.getUnit()); 247 } 248 249 /** 250 * Formats and converts provided measurement value and unit using provided 251 * unit system. 252 * If provided value is too large for provided unit, this method will 253 * convert it to a more appropriate unit using provided unit system (either 254 * metric or imperial). 255 * 256 * @param value a measurement value. 257 * @param unit a measurement unit. 258 * @param system system unit to convert measurement to. 259 * @return a string representation of measurement value and unit. 260 */ 261 public abstract String formatAndConvert(final Number value, final U unit, final UnitSystem system); 262 263 /** 264 * Formats and converts provided measurement value and unit using provided 265 * unit system. 266 * If provided value is too large for provided unit, this method will 267 * convert it to a more appropriate unit using provided unit system (either 268 * metric or imperial). 269 * 270 * @param value a measurement value. 271 * @param unit a measurement unit. 272 * @param system system unit to convert measurement to. 273 * @return a string representation of measurement value and unit. 274 */ 275 public String formatAndConvert(final double value, final U unit, final UnitSystem system) { 276 return formatAndConvert(BigDecimal.valueOf(value), unit, system); 277 } 278 279 /** 280 * Formats and converts provided measurement using provided unit system. 281 * If provided measurement value is too large for its unit, this method 282 * will convert it to a more appropriate unit using provided unit 283 * system. 284 * 285 * @param measurement a measurement to be formatted. 286 * @param unitSystem system unit to convert measurement to. 287 * @return a string representation of measurement value and unit. 288 */ 289 public String formatAndConvert(final M measurement, final UnitSystem unitSystem) { 290 return formatAndConvert(measurement.getValue(), measurement.getUnit(), unitSystem); 291 } 292 293 /** 294 * Returns available locales for this formatter. 295 * 296 * @return available locales. 297 */ 298 public static Locale[] getAvailableLocales() { 299 return NumberFormat.getAvailableLocales(); 300 } 301 302 /** 303 * Gets locale assigned to this instance. 304 * Locale determines number format and unit system (metric or imperial) 305 * if not specified. 306 * 307 * @return a locale. 308 */ 309 public Locale getLocale() { 310 return locale; 311 } 312 313 /** 314 * Returns maximum fraction digits to be shown when formatting a measure. 315 * 316 * @return maximum fraction digits. 317 */ 318 public int getMaximumFractionDigits() { 319 return numberFormat.getMaximumFractionDigits(); 320 } 321 322 /** 323 * Sets maximum fraction digits to use when formatting a measure. 324 * 325 * @param newValue maximum fraction digits to be set. 326 */ 327 public void setMaximumFractionDigits(final int newValue) { 328 numberFormat.setMaximumFractionDigits(newValue); 329 } 330 331 /** 332 * Returns maximum integer digits to be shown when formatting a measure. 333 * 334 * @return maximum integer digits. 335 */ 336 public int getMaximumIntegerDigits() { 337 return numberFormat.getMaximumIntegerDigits(); 338 } 339 340 /** 341 * Sets maximum integer digits to use when formatting a measure. 342 * 343 * @param newValue maximum integer digits to be set. 344 */ 345 public void setMaximumIntegerDigits(final int newValue) { 346 numberFormat.setMaximumIntegerDigits(newValue); 347 } 348 349 /** 350 * Returns minimum fraction digits to be shown when formatting a measure. 351 * 352 * @return minimum fraction digits. 353 */ 354 public int getMinimumFractionDigits() { 355 return numberFormat.getMinimumFractionDigits(); 356 } 357 358 /** 359 * Sets minimum fraction digits to use when formatting a measure. 360 * 361 * @param newValue minimum fraction digits to be set. 362 */ 363 public void setMinimumFractionDigits(final int newValue) { 364 numberFormat.setMinimumFractionDigits(newValue); 365 } 366 367 /** 368 * Returns minimum integer digits to be shown when formatting a measure. 369 * 370 * @return minimum integer digits. 371 */ 372 public int getMinimumIntegerDigits() { 373 return numberFormat.getMinimumIntegerDigits(); 374 } 375 376 /** 377 * Sets minimum integer digits to use when formatting a measure. 378 * 379 * @param newValue minimum integer digits to be set. 380 */ 381 public void setMinimumIntegerDigits(final int newValue) { 382 numberFormat.setMinimumIntegerDigits(newValue); 383 } 384 385 /** 386 * Returns rounding mode to be used when formatting a measure. 387 * 388 * @return rounding mode to be used when formatting a measure. 389 */ 390 public RoundingMode getRoundingMode() { 391 return numberFormat.getRoundingMode(); 392 } 393 394 /** 395 * Specifies rounding mode to use when formatting a measure. 396 * 397 * @param roundingMode rounding mode to be set. 398 */ 399 public void setRoundingMode(final RoundingMode roundingMode) { 400 numberFormat.setRoundingMode(roundingMode); 401 } 402 403 /** 404 * Indicates if grouping is used when formatting a measure. 405 * 406 * @return true if grouping is used, false otherwise. 407 */ 408 public boolean isGroupingUsed() { 409 return numberFormat.isGroupingUsed(); 410 } 411 412 /** 413 * Sets if grouping is used when formatting a measure. 414 * 415 * @param newValue true if grouping is enabled, false otherwise. 416 */ 417 public void setGroupingUsed(final boolean newValue) { 418 numberFormat.setGroupingUsed(newValue); 419 } 420 421 /** 422 * Indicates if only integer values are parsed. 423 * 424 * @return true if only integer values are parsed, false otherwise. 425 */ 426 public boolean isParseIntegerOnly() { 427 return numberFormat.isParseIntegerOnly(); 428 } 429 430 /** 431 * Specifies whether only integer values are parsed or not. 432 * 433 * @param value if true only integer values will be parsed. 434 */ 435 public void setParseIntegerOnly(final boolean value) { 436 numberFormat.setParseIntegerOnly(value); 437 } 438 439 /** 440 * Obtains pattern to format values and unit together into a single string. 441 * {0} corresponds to the value, {1} corresponds to the unit part. 442 * 443 * @return pattern to format values and unit together. 444 */ 445 public String getValueAndUnitFormatPattern() { 446 return valueAndUnitFormatPattern; 447 } 448 449 /** 450 * Sets pattern to format values and unit together into a single string. 451 * {0} corresponds to the value, {1} corresponds to the unit part. 452 * 453 * @param valueAndUnitFormatPattern pattern to format values and unit 454 * together. 455 * @throws IllegalArgumentException if provided pattern is null. 456 */ 457 public void setValueAndUnitFormatPattern(final String valueAndUnitFormatPattern) { 458 if (valueAndUnitFormatPattern == null) { 459 throw new IllegalArgumentException(); 460 } 461 this.valueAndUnitFormatPattern = valueAndUnitFormatPattern; 462 } 463 464 /** 465 * Returns unit system this instance will use based on its assigned locale. 466 * Notice that if no locale was assigned, then the default system locale 467 * will be used. 468 * 469 * @return unit system this instance will use. 470 */ 471 public UnitSystem getUnitSystem() { 472 return UnitLocale.getFrom(locale); 473 } 474 475 /** 476 * Indicates whether provided string representation contains a valid 477 * measurement unit or not. 478 * 479 * @param source a string measurement representation to be checked. 480 * @return true if provided string has a valid (i.e. recognized) unit, false 481 * otherwise. 482 */ 483 public boolean isValidUnit(final String source) { 484 return findUnit(source) != null; 485 } 486 487 /** 488 * Indicates whether provided string representation is a valid measurement 489 * representation or not. 490 * 491 * @param source a string measurement representation to be checked. 492 * @return true if provided string representation is valid (contains valid 493 * value and unit), false otherwise. 494 */ 495 public boolean isValidMeasurement(final String source) { 496 try { 497 numberFormat.parse(source); 498 return isValidUnit(source); 499 } catch (final ParseException e) { 500 return false; 501 } 502 } 503 504 /** 505 * Indicates whether provided string representation of a measurement 506 * contains a metric system unit. 507 * 508 * @param source a measurement string representation to be checked. 509 * @return true if found unit is metric, false otherwise or if unit 510 * cannot be determined. 511 */ 512 public boolean isMetricUnit(final String source) { 513 return getUnitSystem(source) == UnitSystem.METRIC; 514 } 515 516 /** 517 * Indicates whether provided string representation of a measurement 518 * contains an imperial system unit. 519 * 520 * @param source a measurement string representation to be checked. 521 * @return true if found unit is imperial, false otherwise or if unit 522 * cannot be determined. 523 */ 524 public boolean isImperialUnit(final String source) { 525 return getUnitSystem(source) == UnitSystem.IMPERIAL; 526 } 527 528 /** 529 * Gets unit system for detected unit into provided string representation 530 * of a measurement. 531 * 532 * @param source a measurement string representation to be checked. 533 * @return a unit system (either metric or imperial) or null if unit 534 * cannot be determined. 535 */ 536 public abstract UnitSystem getUnitSystem(final String source); 537 538 /** 539 * Parses a string into a measure. 540 * 541 * @param source text to be parsed. 542 * @return a measure containing measure value and unit obtained from parsed 543 * text. 544 * @throws ParseException if parsing failed. 545 * @throws UnknownUnitException if unit cannot be determined. 546 */ 547 public abstract M parse(final String source) throws ParseException, UnknownUnitException; 548 549 /** 550 * Finds measure unit from within a measurement string representation. 551 * 552 * @param source a measurement string representation. 553 * @return a measure unit or null if none can be determined. 554 */ 555 public abstract U findUnit(final String source); 556 557 /** 558 * Obtains measure unit symbol. 559 * 560 * @param unit a measure unit. 561 * @return measure unit symbol. 562 */ 563 public abstract String getUnitSymbol(final U unit); 564 565 /** 566 * Internal method to parse a string into a measure. 567 * 568 * @param source text to be parsed. 569 * @param measure a measure to be initialized with parsed contents. 570 * @return provided measure. 571 * @throws ParseException if parsing failed. 572 * @throws UnknownUnitException if unit cannot be determined. 573 */ 574 M internalParse(final String source, final M measure) throws ParseException, UnknownUnitException { 575 measure.setValue(numberFormat.parse(source)); 576 try { 577 measure.setUnit(findUnit(source)); 578 } catch (final IllegalArgumentException e) { 579 throw new UnknownUnitException(e); 580 } 581 return measure; 582 } 583 584 /** 585 * Internal method to clone this measure formatter. 586 * 587 * @param copy an instantiated copy of a measure formatter that needs to be initialized. 588 * @return provided copy. 589 */ 590 MeasureFormatter<M, U> internalClone(final MeasureFormatter<M, U> copy) { 591 copy.numberFormat = (NumberFormat) numberFormat.clone(); 592 if (format != null) { 593 copy.format = (MessageFormat) format.clone(); 594 } 595 if (locale != null) { 596 copy.locale = (Locale) locale.clone(); 597 } 598 return copy; 599 } 600 601 @Override 602 protected MeasureFormatter<M , U> clone() throws CloneNotSupportedException { 603 //noinspection unchecked 604 return internalClone((MeasureFormatter<M , U>) super.clone()); 605 } 606 }