• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

unitsofmeasurement / indriya / 2356

07 Jul 2025 11:14AM UTC coverage: 71.2% (-0.08%) from 71.282%
2356

push

circleci

keilw
444: Formatting Quantities: maxFractionDigits are ignored

Task-Url: https://github.com/unitsofmeasurement/indriya/issues/444

4 of 5 new or added lines in 1 file covered. (80.0%)

3738 of 5250 relevant lines covered (71.2%)

0.71 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.17
/src/main/java/tech/units/indriya/format/NumberDelimiterQuantityFormat.java
1
/*
2
 * Units of Measurement Reference Implementation
3
 * Copyright (c) 2005-2025, Jean-Marie Dautelle, Werner Keil, Otavio Santana.
4
 *
5
 * All rights reserved.
6
 *
7
 * Redistribution and use in source and binary forms, with or without modification,
8
 * are permitted provided that the following conditions are met:
9
 *
10
 * 1. Redistributions of source code must retain the above copyright notice,
11
 *    this list of conditions and the following disclaimer.
12
 *
13
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions
14
 *    and the following disclaimer in the documentation and/or other materials provided with the distribution.
15
 *
16
 * 3. Neither the name of JSR-385, Indriya nor the names of their contributors may be used to endorse or promote products
17
 *    derived from this software without specific prior written permission.
18
 *
19
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
21
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
28
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
 */
30
package tech.units.indriya.format;
31

32
import static tech.units.indriya.format.FormatBehavior.LOCALE_NEUTRAL;
33
import static tech.units.indriya.format.CommonFormatter.parseMixedAsLeading;
34
import static tech.units.indriya.format.CommonFormatter.parseMixedAsPrimary;
35

36
import java.io.IOException;
37
import java.text.NumberFormat;
38
import java.text.ParsePosition;
39
import java.util.Locale;
40
import java.util.Objects;
41

42
import javax.measure.Quantity;
43
import javax.measure.Unit;
44
import javax.measure.format.MeasurementParseException;
45
import javax.measure.format.UnitFormat;
46

47
import tech.units.indriya.AbstractUnit;
48
import tech.units.indriya.quantity.MixedQuantity;
49
import tech.units.indriya.quantity.Quantities;
50

51
/**
52
 * An implementation of {@link javax.measure.format.QuantityFormat QuantityFormat} combining {@linkplain NumberFormat} and {@link UnitFormat}
53
 * separated by a delimiter.
54
 *
55
 * @author <a href="mailto:werner@units.tech">Werner Keil</a>
56
 * @author <a href="mailto:thodoris.bais@gmail.com">Thodoris Bais</a>
57
 *
58
 * @version 3.0, $Date: 2025-07-07 $
59
 * @since 2.0
60
 */
61
@SuppressWarnings({ "rawtypes", "unchecked" })
62
public class NumberDelimiterQuantityFormat extends AbstractQuantityFormat {
63

64
    /**
65
     * Holds the default format instance (SimpleUnitFormat).
66
     */
67
    private static final NumberDelimiterQuantityFormat SIMPLE_INSTANCE = new NumberDelimiterQuantityFormat.Builder()
1✔
68
            .setNumberFormat(NumberFormat.getInstance(Locale.ROOT)).setUnitFormat(SimpleUnitFormat.getInstance()).build();
1✔
69

70
    /**
71
     * Holds the localized format instance.
72
     */
73
    private static final NumberDelimiterQuantityFormat LOCAL_INSTANCE = new NumberDelimiterQuantityFormat.Builder()
1✔
74
            .setNumberFormat(NumberFormat.getInstance())
1✔
75
            .setUnitFormat(LocalUnitFormat.getInstance())
1✔
76
            .setLocaleSensitive(true).build();
1✔
77

78
    /**
79
     *
80
     */
81
    private static final long serialVersionUID = 3546952599885869402L;
82

83
    private transient NumberFormat numberFormat;
84
    private transient UnitFormat unitFormat;
85
    private transient Unit primaryUnit;
86
    private String delimiter;
87
    private String mixDelimiter;
88
    private boolean localeSensitive;
89

90
    /** private constructor */
91
    private NumberDelimiterQuantityFormat() { }
92

93
    /**
94
     * A fluent Builder to easily create new instances of <code>NumberDelimiterQuantityFormat</code>.
95
     */
96
    public static class Builder {
1✔
97

98
        private transient NumberFormat numberFormat;
99
        private transient UnitFormat unitFormat;
100
        private transient Unit primaryUnit;
101
        private transient String delimiter = DEFAULT_DELIMITER;
1✔
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);
1✔
113
            this.numberFormat = numberFormat;
1✔
114
            return this;
1✔
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);
1✔
125
            this.unitFormat = unitFormat;
1✔
126
            this.localeSensitive = unitFormat.isLocaleSensitive(); // adjusting localeSensitive based on UnitFormat
1✔
127
            return this;
1✔
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);
1✔
138
            this.primaryUnit = primary;
1✔
139
            return this;
1✔
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);
1✔
150
            this.delimiter = delimiter;
1✔
151
            return this;
1✔
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);
1✔
162
            this.mixedRadixDelimiter = radixPartsDelimiter;
1✔
163
            return this;
1✔
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;
1✔
174
            return this;
1✔
175
        }
176

177
        public NumberDelimiterQuantityFormat build() {
178
            NumberDelimiterQuantityFormat quantityFormat = new NumberDelimiterQuantityFormat();
1✔
179
            quantityFormat.numberFormat = this.numberFormat;
1✔
180
            quantityFormat.unitFormat = this.unitFormat;
1✔
181
            quantityFormat.primaryUnit = this.primaryUnit;
1✔
182
            quantityFormat.delimiter = this.delimiter;
1✔
183
            quantityFormat.mixDelimiter = this.mixedRadixDelimiter;
1✔
184
            quantityFormat.localeSensitive = this.localeSensitive;
1✔
185
            return quantityFormat;
1✔
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) {
1✔
201
                        case LOCALE_SENSITIVE:
202
                                return LOCAL_INSTANCE;
1✔
203
            case LOCALE_NEUTRAL:
204
            default:
205
                return SIMPLE_INSTANCE;
1✔
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();
1✔
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);
1✔
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();
1✔
257
    }
258

259
    @Override
260
    public Appendable format(Quantity<?> quantity, Appendable dest) throws IOException {
261
            dest.append(numberFormat.format(quantity.getValue()));
1✔
262
            if (quantity.getUnit().equals(AbstractUnit.ONE))
1✔
NEW
263
                return dest;
×
264
            dest.append(delimiter);
1✔
265
            return unitFormat.format(quantity.getUnit(), dest);
1✔
266
    }
267

268
    @Override
269
    public Quantity<?> parse(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException, MeasurementParseException {
270
        final String str = csq.toString();
1✔
271
        final int index = cursor.getIndex();
1✔
272
        if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) {
1✔
273
            if (primaryUnit != null) {
1✔
274
                return parseMixedAsPrimary(str, numberFormat, unitFormat, primaryUnit, delimiter, mixDelimiter, index);
×
275
            } else {
276
                return parseMixedAsLeading(str, numberFormat, unitFormat, delimiter, mixDelimiter, index);
1✔
277
            }
278
        } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) {
1✔
279
            if (primaryUnit != null) {
1✔
280
                return parseMixedAsPrimary(str, numberFormat, unitFormat, primaryUnit, delimiter, index);
1✔
281
            } else {
282
                return parseMixedAsLeading(str, numberFormat, unitFormat, delimiter, index);
1✔
283
            }
284
        }
285
        final Number number = numberFormat.parse(str, cursor);
1✔
286
        if (number == null)
1✔
287
            throw new IllegalArgumentException("Number cannot be parsed");
1✔
288
        final String[] parts = str.substring(index).split(delimiter);
1✔
289
        if (parts.length < 2) {
1✔
290
            throw new IllegalArgumentException("No Unit found");
1✔
291
        }
292
        final Unit unit = unitFormat.parse(parts[1]);
1✔
293
        return Quantities.getQuantity(number, unit);
1✔
294
    }
295

296
    @Override
297
    protected Quantity<?> parse(CharSequence csq, int index) throws IllegalArgumentException, MeasurementParseException {
298
        return parse(csq, new ParsePosition(index));
1✔
299
    }
300

301
    @Override
302
    public Quantity<?> parse(CharSequence csq) throws IllegalArgumentException, MeasurementParseException {
303
        return parse(csq, 0);
1✔
304
    }
305

306
    @Override
307
    public String toString() {
308
        return getClass().getSimpleName();
1✔
309
    }
310

311
    @Override
312
    public boolean isLocaleSensitive() {
313
        return localeSensitive;
1✔
314
    }
315

316
    @Override
317
    protected StringBuffer formatMixed(MixedQuantity<?> comp, StringBuffer dest) {
318
        final StringBuffer sb = new StringBuffer();
1✔
319
        int i = 0;
1✔
320
        for (Quantity<?> q : comp.getQuantities()) {
1✔
321
            sb.append(format(q));
1✔
322
            if (i < comp.getQuantities().size() - 1 ) {
1✔
323
                sb.append((mixDelimiter != null ? mixDelimiter : DEFAULT_DELIMITER)); // we need null for parsing but not
1✔
324
            }
325
            i++;
1✔
326
        }
1✔
327
        return sb;
1✔
328
    }
329

330
    public MixedQuantity<?> parseMixed(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException, MeasurementParseException {
331
        final String str = csq.toString();
1✔
332
        final int index = cursor.getIndex();
1✔
333
        if (mixDelimiter != null && !mixDelimiter.equals(delimiter)) {
1✔
334
                return CommonFormatter.parseMixed(str, numberFormat, unitFormat, delimiter, mixDelimiter, index);
1✔
335
        } else if (mixDelimiter != null && mixDelimiter.equals(delimiter)) {
1✔
336
                return CommonFormatter.parseMixed(str, numberFormat, unitFormat, delimiter, index);
1✔
337
        }
338
        final Number number = numberFormat.parse(str, cursor);
×
339
        if (number == null)
×
340
            throw new IllegalArgumentException("Number cannot be parsed");
×
341
        final String[] parts = str.substring(index).split(delimiter);
×
342
        if (parts.length < 2) {
×
343
            throw new IllegalArgumentException("No Unit found");
×
344
        }
345
        final Unit unit = unitFormat.parse(parts[1]);
×
346
        return MixedQuantity.of(Quantities.getQuantity(number, unit));
×
347
    }
348

349
    protected MixedQuantity<?> parseMixed(CharSequence csq, int index) throws IllegalArgumentException, MeasurementParseException {
350
        return parseMixed(csq, new ParsePosition(index));
1✔
351
    }
352

353
    public MixedQuantity<?> parseMixed(CharSequence csq) throws IllegalArgumentException, MeasurementParseException {
354
        return parseMixed(csq, 0);
1✔
355
    }
356
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc