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