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