View Javadoc
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 }