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 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.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.2, $Date: 2020-09-27 $
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 NumberDelimiterQuantityFormat() {
091        /* private constructor */ }
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            return this;
127        }
128
129        /**
130         * Sets the primary unit parameter for multiple {@link CompoundQuantity mixed quantities} to the given {@code Unit}.
131         * @param primary the primary {@link Unit}
132         * @throws NullPointerException if {@code primary} is {@code null}
133         * @return this {@code NumberDelimiterQuantityFormat.Builder}
134         */
135        public Builder setPrimaryUnit(final Unit primary) {
136            Objects.requireNonNull(primary);
137            this.primaryUnit = primary;
138            return this;
139        }
140
141        /**
142         * Sets the delimiter between a {@code NumberFormat} and {@code UnitFormat}.
143         * @param delimiter the delimiter to use
144         * @throws NullPointerException if {@code delimiter} is {@code null}
145         * @return this {@code NumberDelimiterQuantityFormat.Builder}
146         */
147        public Builder setDelimiter(String delimiter) {
148                Objects.requireNonNull(delimiter);
149            this.delimiter = delimiter;
150            return this;
151        }
152
153        /**
154         * Sets the radix delimiter between multiple {@link CompoundQuantity mixed quantities}.
155         * @param radixPartsDelimiter the delimiter to use
156         * @throws NullPointerException if {@code radixPartsDelimiter} is {@code null}
157         * @return this {@code NumberDelimiterQuantityFormat.Builder}
158         */
159        public Builder setRadixPartsDelimiter(String radixPartsDelimiter) {
160            Objects.requireNonNull(radixPartsDelimiter);
161            this.mixedRadixDelimiter = radixPartsDelimiter;
162            return this;
163        }
164
165        /**
166         * Sets the {@code localeSensitive} flag.
167         * @param localeSensitive the flag, if the {@code NumberDelimiterQuantityFormat} to be built will depend on a {@code Locale} to perform its tasks.
168         * @return this {@code NumberDelimiterQuantityFormat.Builder}
169         * @see UnitFormat#isLocaleSensitive()
170         */
171        public Builder setLocaleSensitive(boolean localeSensitive) {
172            this.localeSensitive = localeSensitive;
173            return this;
174        }
175
176        public NumberDelimiterQuantityFormat build() {
177            NumberDelimiterQuantityFormat quantityFormat = new NumberDelimiterQuantityFormat();
178            quantityFormat.numberFormat = this.numberFormat;
179            quantityFormat.unitFormat = this.unitFormat;
180            quantityFormat.primaryUnit = this.primaryUnit;
181            quantityFormat.delimiter = this.delimiter;
182            quantityFormat.mixDelimiter = this.mixedRadixDelimiter;
183            quantityFormat.localeSensitive = this.localeSensitive;
184            return quantityFormat;
185        }
186    }
187
188    /**
189     * Returns an instance of {@link NumberDelimiterQuantityFormat} with a particular {@link FormatBehavior}, either locale-sensitive or locale-neutral.
190     * For example: <code>NumberDelimiterQuantityFormat.getInstance(LOCALE_NEUTRAL))</code> returns<br>
191     * <code>new NumberDelimiterQuantityFormat.Builder()
192            .setNumberFormat(NumberFormat.getInstance(Locale.ROOT)).setUnitFormat(SimpleUnitFormat.getInstance()).build();</code>
193     *
194     * @param behavior
195     *            the format behavior to apply.
196     * @return <code>NumberDelimiterQuantityFormat.getInstance(NumberFormat.getInstance(), UnitFormat.getInstance())</code>
197     */
198    public static NumberDelimiterQuantityFormat getInstance(final FormatBehavior behavior) {
199        switch (behavior) {
200                        case LOCALE_SENSITIVE:
201                                return LOCAL_INSTANCE;
202            case LOCALE_NEUTRAL:
203            default:
204                return SIMPLE_INSTANCE;
205        }
206    }
207
208    /**
209     * Returns a new instance of {@link Builder}.
210     *
211     * @return a new {@link Builder}.
212     */
213    public static final Builder builder() {
214        return new Builder();
215    }
216
217    /**
218     * Returns the default format.
219     *
220     * @return the desired format.
221     */
222    public static NumberDelimiterQuantityFormat getInstance() {
223        return getInstance(LOCALE_NEUTRAL);
224    }
225
226    /**
227     * Returns the quantity format using the specified number format and unit format (the number and unit are separated by one space).
228     *
229     * @param numberFormat
230     *            the number format.
231     * @param unitFormat
232     *            the unit format.
233     * @return the corresponding format.
234     */
235    public static NumberDelimiterQuantityFormat getInstance(NumberFormat numberFormat, UnitFormat unitFormat) {
236        return new NumberDelimiterQuantityFormat.Builder().setNumberFormat(numberFormat).setUnitFormat(unitFormat).build();
237    }
238
239    @Override
240    public Appendable format(Quantity<?> quantity, Appendable dest) throws IOException {
241        int fract = 0;
242        /*
243        if (quantity instanceof MixedQuantity) {
244            final MixedQuantity<?> compQuant = (MixedQuantity<?>) quantity;
245            if (compQuant.getUnit() instanceof MixedUnit) {
246                final MixedUnit<?> compUnit = (MixedUnit<?>) compQuant.getUnit();
247                final Number[] values = compQuant.getValues();
248                if (values.length == compUnit.getUnits().size()) {
249                    final StringBuffer sb = new StringBuffer(); // we use StringBuffer here because of java.text.Format compatibility
250                    for (int i = 0; i < values.length; i++) {
251                        if (values[i] != null) {
252                            fract = getFractionDigitsCount(values[i].doubleValue());
253                        } else {
254                            fract = 0;
255                        }
256                        if (fract > 1) {
257                            numberFormat.setMaximumFractionDigits(fract + 1);
258                        }
259                        sb.append(numberFormat.format(values[i]));
260                        sb.append(delimiter);
261                        sb.append(unitFormat.format(compUnit.getUnits().get(i)));
262                        if (i < values.length - 1) {
263                            sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not
264                                                                                                            // formatting
265                        }
266                    }
267                    return sb;
268                } else {
269                    throw new IllegalArgumentException(
270                            String.format("%s values don't match %s in mixed unit", values.length, compUnit.getUnits().size()));
271                }
272            } else {
273                throw new MeasurementException("A mixed quantity must contain a mixed unit");
274            }
275        } else {
276        */
277            if (quantity != null && quantity.getValue() != null) {
278                fract = getFractionDigitsCount(quantity.getValue().doubleValue());
279            }
280            if (fract > 1) {
281                numberFormat.setMaximumFractionDigits(fract + 1);
282            }
283            dest.append(numberFormat.format(quantity.getValue()));
284            if (quantity.getUnit().equals(AbstractUnit.ONE))
285                return dest;
286            dest.append(delimiter);
287            return unitFormat.format(quantity.getUnit(), dest);
288        //}
289    }
290
291    @Override
292    public Quantity<?> parse(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException, MeasurementParseException {
293        final String str = csq.toString();
294        final int index = cursor.getIndex();
295        if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) {
296            if (primaryUnit != null) {
297                return parseCompoundAsPrimary(str, numberFormat, unitFormat, primaryUnit, delimiter, mixDelimiter, index);
298            } else {
299                return parseCompoundAsLeading(str, numberFormat, unitFormat, delimiter, mixDelimiter, index);
300            }
301        } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) {
302            if (primaryUnit != null) {
303                return parseCompoundAsPrimary(str, numberFormat, unitFormat, primaryUnit, delimiter, index);
304            } else {
305                return parseCompoundAsLeading(str, numberFormat, unitFormat, delimiter, index);
306            }
307        }
308        final Number number = numberFormat.parse(str, cursor);
309        if (number == null)
310            throw new IllegalArgumentException("Number cannot be parsed");
311        final String[] parts = str.substring(index).split(delimiter);
312        if (parts.length < 2) {
313            throw new IllegalArgumentException("No Unit found");
314        }
315        final Unit unit = unitFormat.parse(parts[1]);
316        return Quantities.getQuantity(number, unit);
317    }
318
319    @Override
320    protected Quantity<?> parse(CharSequence csq, int index) throws IllegalArgumentException, MeasurementParseException {
321        return parse(csq, new ParsePosition(index));
322    }
323
324    @Override
325    public Quantity<?> parse(CharSequence csq) throws IllegalArgumentException, MeasurementParseException {
326        return parse(csq, 0);
327    }
328
329    @Override
330    public String toString() {
331        return getClass().getSimpleName();
332    }
333
334    @Override
335    public boolean isLocaleSensitive() {
336        return localeSensitive;
337    }
338
339    @Override
340    protected StringBuffer formatCompound(CompoundQuantity<?> comp, StringBuffer dest) {
341        final StringBuffer sb = new StringBuffer();
342        int i = 0;
343        for (Quantity<?> q : comp.getQuantities()) {
344            sb.append(format(q));
345            if (i < comp.getQuantities().size() - 1 ) {
346                sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not
347            }
348            i++;
349        }
350        return sb;
351    }
352
353    public CompoundQuantity<?> parseCompound(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException, MeasurementParseException {
354        final String str = csq.toString();
355        final int index = cursor.getIndex();
356        if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) {
357                return CommonFormatter.parseCompound(str, numberFormat, unitFormat, delimiter, mixDelimiter, index);
358        } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) {
359                return CommonFormatter.parseCompound(str, numberFormat, unitFormat, delimiter, index);
360        }
361        final Number number = numberFormat.parse(str, cursor);
362        if (number == null)
363            throw new IllegalArgumentException("Number cannot be parsed");
364        final String[] parts = str.substring(index).split(delimiter);
365        if (parts.length < 2) {
366            throw new IllegalArgumentException("No Unit found");
367        }
368        final Unit unit = unitFormat.parse(parts[1]);
369        return CompoundQuantity.of(Quantities.getQuantity(number, unit));
370    }
371
372    protected CompoundQuantity<?> parseCompound(CharSequence csq, int index) throws IllegalArgumentException, MeasurementParseException {
373        return parseCompound(csq, new ParsePosition(index));
374    }
375
376    public CompoundQuantity<?> parseCompound(CharSequence csq) throws IllegalArgumentException, MeasurementParseException {
377        return parseCompound(csq, 0);
378    }
379
380    // Private helper methods
381
382    private static int getFractionDigitsCount(double d) {
383        if (d >= 1) { // we only need the fraction digits
384            d = d - (long) d;
385        }
386        if (d == 0) { // nothing to count
387            return 0;
388        }
389        d *= 10; // shifts 1 digit to left
390        int count = 1;
391        while (d - (long) d != 0) { // keeps shifting until there are no more
392            // fractions
393            d *= 10;
394            count++;
395        }
396        return count;
397    }
398
399}