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