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

unitsofmeasurement / indriya / 2332

12 Jun 2025 09:48PM UTC coverage: 71.282% (+0.005%) from 71.277%
2332

push

circleci

keilw
Merge branch 'master' of https://github.com/unitsofmeasurement/indriya.git

8 of 8 new or added lines in 1 file covered. (100.0%)

91 existing lines in 2 files now uncovered.

3753 of 5265 relevant lines covered (71.28%)

0.71 hits per line

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

80.15
/src/main/java/tech/units/indriya/format/SimpleUnitFormat.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 java.io.IOException;
33
import java.text.FieldPosition;
34
import java.text.ParsePosition;
35
import java.util.HashMap;
36
import java.util.Map;
37
import java.util.stream.Collectors;
38
import java.util.stream.Stream;
39

40
import javax.measure.BinaryPrefix;
41
import javax.measure.MeasurementError;
42
import javax.measure.MetricPrefix;
43
import javax.measure.Prefix;
44
import javax.measure.Quantity;
45
import javax.measure.Unit;
46
import javax.measure.UnitConverter;
47
import javax.measure.format.MeasurementParseException;
48
import javax.measure.format.UnitFormat;
49

50
import static javax.measure.MetricPrefix.MICRO;
51

52
import tech.units.indriya.AbstractUnit;
53
import tech.units.indriya.function.AddConverter;
54
import tech.units.indriya.function.MultiplyConverter;
55
import tech.units.indriya.function.RationalNumber;
56
import tech.units.indriya.unit.AlternateUnit;
57
import tech.units.indriya.unit.AnnotatedUnit;
58
import tech.units.indriya.unit.BaseUnit;
59
import tech.units.indriya.unit.ProductUnit;
60
import tech.units.indriya.unit.TransformedUnit;
61
import tech.units.indriya.unit.Units;
62

63
import static tech.units.indriya.format.FormatConstants.MIDDLE_DOT;
64

65
/**
66
 * <p>
67
 * This class implements the {@link UnitFormat} interface for formatting and parsing {@link Unit units}.
68
 * </p>
69
 *
70
 * <p>
71
 * For all SI units, the <b>24 SI prefixes</b> used to form decimal multiples and sub-multiples are recognized. As well as the <b>8 binary prefixes</b>.<br>
72
 * {@link Units} are directly recognized. For example:<br>
73
 * <code>
74
 *        UnitFormat format = SimpleUnitFormat.getInstance();<br>
75
 *        format.parse("m°C").equals(MetricPrefix.MILLI(Units.CELSIUS));<br>
76
 *        format.parse("kW").equals(MetricPrefix.KILO(Units.WATT));<br>
77
 *        format.parse("ft").equals(Units.METRE.multiply(0.3048))</code>
78
 * </p>
79
 *
80
 * @author <a href="mailto:jean-marie@dautelle.com">Jean-Marie Dautelle</a>
81
 * @author <a href="mailto:werner@units.tech">Werner Keil</a>
82
 * @author Eric Russell
83
 * @author Andi Huber
84
 * @version 2.20, June 12, 2025
85
 * @since 1.0
86
 */
87
public abstract class SimpleUnitFormat extends AbstractUnitFormat {
88
    // private static final long serialVersionUID = 4149424034841739785L;
89

90
    /**
91
     * Flavor of this format
92
     *
93
     * @author Werner
94
     *
95
     */
96
    public static enum Flavor {
1✔
97
        /** @deprecated use DEFAULT */
98
         Default, 
1✔
99
         /** The default format flavor */
100
         DEFAULT, 
1✔
101
         /** The ASCII_INSTANCE format flavor */
102
         ASCII
1✔
103
    }
104

105
    private static final String MU = "\u03bc";
106

107
    /**
108
     * Holds the default format instance.
109
     */
110
    private static final DefaultFormat DEFAULT_INSTANCE = new DefaultFormat().init();
1✔
111

112
    /**
113
     * Holds the ASCII_INSTANCE format instance.
114
     */
115
    private static final ASCIIFormat ASCII_INSTANCE = new ASCIIFormat().init();
1✔
116

117
    /**
118
     * Returns the globally shared unit format instance (used by {@link AbstractUnit#parse(CharSequence) AbstractUnit.parse()} and
119
     * {@link AbstractUnit#toString() AbstractUnit.toString()}).
120
     *
121
     * @return the default unit format.
122
     */
123
    public static SimpleUnitFormat getInstance() {
124
        return getInstance(Flavor.DEFAULT);
1✔
125
    }
126

127
    /**
128
     * Returns the {@link SimpleUnitFormat} in the desired {@link Flavor}
129
     *
130
     * @return the instance for the given {@link Flavor}.
131
     */
132
    public static SimpleUnitFormat getInstance(Flavor flavor) {
133
        switch (flavor) {
1✔
134
        case ASCII:
135
            return SimpleUnitFormat.ASCII_INSTANCE;
1✔
136
        default:
137
            return DEFAULT_INSTANCE;
1✔
138
        }
139
    }
140

141
    /**
142
     * Similar to {@link #getInstance()}, but returns a new, non-shared unit format instance,
143
     * instead of a shared singleton instance.
144
     *
145
     * @return a new instance of the default unit format.
146
     * @see #getInstance()
147
     * @since 2.7
148
     */
149
    public static SimpleUnitFormat getNewInstance() {
150
        return getNewInstance(Flavor.DEFAULT);
1✔
151
    }
152

153
    /**
154
     * Similar to {@link #getInstance(Flavor)}, but returns a new {@link SimpleUnitFormat} instance in the desired
155
     * {@link Flavor}, instead of a shared singleton instance.
156
     *
157
     * @return a new instance for the given {@link Flavor}.
158
     * @see #getInstance(Flavor)
159
     * @since 2.7 
160
     */
161
    public static SimpleUnitFormat getNewInstance(Flavor flavor) {
162
        switch (flavor) {
1✔
163
        case ASCII:
164
            return new ASCIIFormat().init();
×
165
        default:
166
            return new DefaultFormat().init();
1✔
167
        }
168
    }
169

170
    /**
171
     * Base constructor.
172
     */
173
    protected SimpleUnitFormat() {
1✔
174
    }
1✔
175

176
    /**
177
     * Formats the specified unit.
178
     *
179
     * @param unit
180
     *          the unit to format.
181
     * @param appendable
182
     *          the appendable destination.
183
     * @throws IOException
184
     *           if an error occurs.
185
     */
186
    public abstract Appendable format(Unit<?> unit, Appendable appendable) throws IOException;
187

188
    /**
189
     * Parses a sequence of character to produce a unit or a rational product of unit.
190
     *
191
     * @param csq
192
     *          the <code>CharSequence</code> to parse.
193
     * @param pos
194
     *          an object holding the parsing index and error position.
195
     * @return an {@link Unit} parsed from the character sequence.
196
     * @throws IllegalArgumentException
197
     *           if the character sequence contains an illegal syntax.
198
     */
199
    @SuppressWarnings("rawtypes")
200
    public abstract Unit<? extends Quantity> parseProductUnit(CharSequence csq, ParsePosition pos) throws MeasurementParseException;
201

202
    /**
203
     * Parses a sequence of character to produce a single unit.
204
     *
205
     * @param csq
206
     *          the <code>CharSequence</code> to parse.
207
     * @param pos
208
     *          an object holding the parsing index and error position.
209
     * @return an {@link Unit} parsed from the character sequence.
210
     * @throws IllegalArgumentException
211
     *           if the character sequence does not contain a valid unit identifier.
212
     */
213
    @SuppressWarnings("rawtypes")
214
    public abstract Unit<? extends Quantity> parseSingleUnit(CharSequence csq, ParsePosition pos) throws MeasurementParseException;
215

216
    /**
217
     * Attaches a system-wide label to the specified unit. For example: <code>SimpleUnitFormat.getInstance().label(DAY.multiply(365), "year");
218
     * SimpleUnitFormat.getInstance().label(METER.multiply(0.3048), "ft");</code> If the specified label is already associated to an unit the previous
219
     * association is discarded or ignored.
220
     * <p>
221
     * If you set a different label without calling {@link #removeLabel(Unit)}), {@link #removeAlias(Unit, String)}), using the old label, or {@link #removeAliases(Unit)}) on the given unit, the old label is overwritten for <b>labeling/<b> purposes, but it remains like an <b>alias</b> (it still works for parsing). 
222
     * </p>
223
     * @param unit
224
     *          the unit being labeled.
225
     * @param label
226
     *          the new label for this unit.
227
     * @throws IllegalArgumentException
228
     *           if the label is not a {@link SimpleUnitFormat#isValidIdentifier(String)} valid identifier.
229
     */
230
    public abstract void label(Unit<?> unit, String label);
231

232
        /**
233
         * Removes the system-wide label (added by {@link #label(Unit, String)}) and all system-wide aliases (added by {@link #alias(Unit, String)}) for this unit.
234
         *
235
         * @param unit
236
         *          the unit for which label shall be removed.
237
         */
238
        public abstract void removeLabel(Unit<?> unit);
239

240
        /**
241
     * Attaches a system-wide alias to this unit. Multiple aliases may be attached to the same unit. Aliases are used during parsing to recognize
242
     * different variants of the same unit. For example: <code> SimpleUnitFormat.getInstance().alias(METER.multiply(0.3048), "foot");
243
     * SimpleUnitFormat.getInstance().alias(METRE.multiply(0.3048), "feet"); SimpleUnitFormat.getInstance().alias(METER, "meter");
244
     * SimpleUnitFormat.getInstance().alias(METRE, "meter"); </code> If the specified alias is already associated to a unit or applied as a label, the association is
245
     * replaced by the new one.
246
     *
247
     * @param unit
248
     *          the unit being aliased.
249
     * @param alias
250
     *          the alias attached to this unit.
251
     * @throws IllegalArgumentException
252
     *           if the label is not a {@link SimpleUnitFormat#isValidIdentifier(String)} valid identifier.
253
     */
254
    public abstract void alias(Unit<?> unit, String alias);
255

256
        /**
257
         * Removes the given system-wide alias (added by {@link #alias(Unit, String)}) for this unit and keeps the label (added by {@link #label(Unit, String)})
258
         *
259
         * @param unit
260
         *          the unit for which alias shall be removed.
261
         *          
262
         * @param alias
263
         *          the alias to be removed.          
264
         */
265
        public abstract void removeAlias(Unit<?> unit, String alias);
266
    
267
        /**
268
         * Removes all system-wide aliases (added by {@link #alias(Unit, String)}) for this unit and keeps the label (added by {@link #label(Unit, String)})
269
         *
270
         * @param unit
271
         *          the unit for which aliases shall be removed.
272
         */
273
        public abstract void removeAliases(Unit<?> unit);
274

275
    /**
276
     * Indicates if the specified name can be used as unit identifier.
277
     *
278
     * @param name
279
     *          the identifier to be tested.
280
     * @return <code>true</code> if the name specified can be used as label or alias for this format;<code>false</code> otherwise.
281
     */
282
    protected abstract boolean isValidIdentifier(String name);
283

284
    /**
285
     * Formats an unit and appends the resulting text to a given string buffer (implements <code>java.text.Format</code>).
286
     *
287
     * @param unit
288
     *          the unit to format.
289
     * @param toAppendTo
290
     *          where the text is to be appended
291
     * @param pos
292
     *          the field position (not used).
293
     * @return <code>toAppendTo</code>
294
     */
295
    public final StringBuffer format(Object unit, final StringBuffer toAppendTo, FieldPosition pos) {
296
        try {
297
            final Object dest = toAppendTo;
×
298
            if (dest instanceof Appendable) {
×
299
                format((Unit<?>) unit, (Appendable) dest);
×
300
            } else { // When retroweaver is used to produce 1.4 binaries. TODO is this still relevant?
301
                format((Unit<?>) unit, new Appendable() {
×
302
                    public Appendable append(char arg0) throws IOException {
303
                        toAppendTo.append(arg0);
×
304
                        return null;
×
305
                    }
306
                    public Appendable append(CharSequence arg0) throws IOException {
307
                        toAppendTo.append(arg0);
×
308
                        return null;
×
309
                    }
310
                    public Appendable append(CharSequence arg0, int arg1, int arg2) throws IOException {
311
                        toAppendTo.append(arg0.subSequence(arg1, arg2));
×
312
                        return null;
×
313
                    }
314
                });
315
            }
316
            return toAppendTo;
×
317
        } catch (IOException e) {
×
318
            throw new MeasurementError(e); // Should never happen.
×
319
        }
320
    }
321

322
    /**
323
     * Parses the text from a string to produce an object (implements <code>java.text.Format</code>).
324
     *
325
     * @param source
326
     *          the string source, part of which should be parsed.
327
     * @param pos
328
     *          the cursor position.
329
     * @return the corresponding unit or <code>null</code> if the string cannot be parsed.
330
     */
331
    public final Unit<?> parseObject(String source, ParsePosition pos) throws MeasurementParseException {
332
        return parseProductUnit(source, pos);
1✔
333
    }
334

335
    /**
336
     * This class represents an exponent with both a power (numerator) and a root (denominator).
337
     */
338
    private static class Exponent {
339
        public final int pow;
340
        public final int root;
341

342
        public Exponent(int pow, int root) {
1✔
343
            this.pow = pow;
1✔
344
            this.root = root;
1✔
345
        }
1✔
346
    }
347

348
    /**
349
     * This class represents the default (Unicode) format.
350
     * internal class, please extend either SimpleUnitFormat or AbstractUnitFormat
351
     */
352
    static class DefaultFormat extends SimpleUnitFormat {
353

354
        // Initializes the standard unit databases.
355

356
        static final Unit<?>[] METRIC_UNITS = { Units.AMPERE, Units.BECQUEREL, Units.CANDELA, Units.COULOMB, Units.FARAD, Units.GRAY, Units.HENRY,
1✔
357
                Units.HERTZ, Units.JOULE, Units.KATAL, Units.KELVIN, Units.LUMEN, Units.LUX, Units.METRE, Units.MOLE, Units.NEWTON, Units.OHM, Units.PASCAL,
358
                Units.RADIAN, Units.SECOND, Units.SIEMENS, Units.SIEVERT, Units.STERADIAN, Units.TESLA, Units.VOLT, Units.WATT, Units.WEBER };
359

360
        static final String[] METRIC_PREFIX_SYMBOLS =
361
                Stream.of(MetricPrefix.values())
1✔
362
                .map(Prefix::getSymbol)
1✔
363
                .collect(Collectors.toList())
1✔
364
                .toArray(new String[] {});
1✔
365

366
        // TODO try to consolidate those
367
        static final UnitConverter[] METRIC_PREFIX_CONVERTERS =
368
                Stream.of(MetricPrefix.values())
1✔
369
                .map(MultiplyConverter::ofPrefix)
1✔
370
                .collect(Collectors.toList())
1✔
371
                .toArray(new UnitConverter[] {});
1✔
372

373
        static final String[] BINARY_PREFIX_SYMBOLS =
374
                Stream.of(BinaryPrefix.values())
1✔
375
                .map(Prefix::getSymbol)
1✔
376
                .collect(Collectors.toList())
1✔
377
                .toArray(new String[] {});
1✔
378

379
        static final UnitConverter[] BINARY_PREFIX_CONVERTERS =
1✔
380
                Stream.of(BinaryPrefix.values())
1✔
381
                .map(MultiplyConverter::ofPrefix)
1✔
382
                .collect(Collectors.toList())
1✔
383
                .toArray(new UnitConverter[] {});
1✔
384

385
        /**
386
         * Holds the unique symbols collection (base units or alternate units).
387
         */
388
        private final Map<String, Unit<?>> symbolToUnit = new HashMap<>();
1✔
389

390
        private static enum Token { EOF, IDENTIFIER, OPEN_PAREN, CLOSE_PAREN, EXPONENT, MULTIPLY, DIVIDE,
1✔
391
            PLUS, INTEGER, FLOAT };
1✔
392

393

394
        DefaultFormat() {
1✔
395
            // Hack, somehow µg is not found.
396
            symbolToUnit.put(MetricPrefix.MICRO.getSymbol() + "g", MICRO(Units.GRAM));
1✔
397
            symbolToUnit.put("μg", MICRO(Units.GRAM));
1✔
398
            symbolToUnit.put(MU + "g", MICRO(Units.GRAM));
1✔
399
        }
1✔
400

401
        private DefaultFormat init() {
402

403
            for (int i = 0; i < METRIC_UNITS.length; i++) {
1✔
404
                Unit<?> si = METRIC_UNITS[i];
1✔
405
                String symbol = (si instanceof BaseUnit) ? ((BaseUnit<?>) si).getSymbol() : ((AlternateUnit<?>) si).getSymbol();
1✔
406
                label(si, symbol);
1✔
407
                for (int j = 0; j < METRIC_PREFIX_SYMBOLS.length; j++) {
1✔
408
                    Unit<?> u = si.prefix(MetricPrefix.values()[j]);
1✔
409
                    label(u, METRIC_PREFIX_SYMBOLS[j] + symbol);
1✔
410
                    if ( "µ".equals(METRIC_PREFIX_SYMBOLS[j]) ) {
1✔
411
                        label(u, MU + symbol);
1✔
412
                    }
413
                } // TODO what about BINARY_PREFIX here?
414
            }
415

416
            // -- GRAM/KILOGRAM
417

418
            label(Units.GRAM, "g");
1✔
419
            for(MetricPrefix prefix : MetricPrefix.values()) {
1✔
420
                switch (prefix) {
1✔
421
                case KILO:
422
                    label(Units.KILOGRAM, "kg");
1✔
423
                    break;
1✔
424
                case MICRO:
425
                    label(Units.GRAM.prefix(prefix), prefix.getSymbol()+"g");
1✔
426
                    break;
1✔
427
                default:
428
                    label(Units.GRAM.prefix(prefix), prefix.getSymbol()+"g");
1✔
429
                    break;
430
                }
431
            }
432

433
            label(MICRO(Units.GRAM), MetricPrefix.MICRO.getSymbol() + "g");
1✔
434

435
            // Alias in ASCIIFormat for Ohm
436
            aliasWithPrefixes(Units.OHM, "Ohm");
1✔
437

438
            // Special case for DEGREE_CELSIUS.
439
            labelWithPrefixes(Units.CELSIUS, "℃");
1✔
440
            aliasWithPrefixes(Units.CELSIUS, "°C");
1✔
441
            
442
            // Additional cases and aliases
443
            label(AbstractUnit.ONE, "one");
1✔
444
            label(Units.PERCENT, "%");
1✔
445
            
446
            // https://en.wikipedia.org/wiki/Non-SI_units_mentioned_in_the_SI#Units_officially_accepted_for_use_with_the_SI
447
            // The SI prefixes can be used with several of these units, but not, for example, with the non-SI units of time.
448
            // Also see https://github.com/unitsofmeasurement/indriya/issues/433 
449
            label(Units.MINUTE, "min");
1✔
450
            label(Units.HOUR, "h");
1✔
451
            label(Units.DAY, "d");
1✔
452
            alias(Units.DAY, "day");
1✔
453
            label(Units.WEEK, "wk");
1✔
454
            alias(Units.WEEK, "week");
1✔
455
            label(Units.YEAR, "yr");
1✔
456
            alias(Units.YEAR, "y"); 
1✔
457
            alias(Units.YEAR, "year");
1✔
458
            alias(Units.YEAR, "days365");
1✔
459
            alias(Units.YEAR, "a");
1✔
460
            label(Units.MONTH, "mo");
1✔
461
            alias(Units.MONTH, "mon");
1✔
462
            alias(Units.MONTH, "month");
1✔
463
            label(Units.KILOMETRE_PER_HOUR, "km/h");
1✔
464
            labelWithPrefixes(Units.SQUARE_METRE, "m\u00B2");
1✔
465
            aliasWithPrefixes(Units.SQUARE_METRE, "\u33A1");
1✔
466
            aliasWithPrefixes(Units.SQUARE_METRE, "m2");
1✔
467
            labelWithPrefixes(Units.CUBIC_METRE, "m\u00B3");
1✔
468
            aliasWithPrefixes(Units.CUBIC_METRE, "\u33A5");
1✔
469
            aliasWithPrefixes(Units.CUBIC_METRE, "m3");
1✔
470
            labelWithPrefixes(Units.LITRE, "l");
1✔
471

472
            return this;
1✔
473
        }
474

475
        /**
476
         * Holds the name to unit mapping.
477
         */
478
        protected final Map<String, Unit<?>> nameToUnit = new HashMap<>();
1✔
479

480
        /**
481
         * Holds the unit to name mapping.
482
         */
483
        protected final Map<Unit<?>, String> unitToName = new HashMap<>();
1✔
484

485
        @Override
486
        public String toString() {
487
            return SimpleUnitFormat.class.getSimpleName();
1✔
488
        }
489

490
        @Override
491
        public void label(Unit<?> unit, String label) {
492
            if (!isValidIdentifier(label))
1✔
493
                throw new IllegalArgumentException("Label: " + label + " is not a valid identifier.");
×
494
            synchronized (this) {
1✔
495
                nameToUnit.put(label, unit);
1✔
496
                unitToName.put(unit, label);
1✔
497
            }
1✔
498
        }
1✔
499
        
500
            @Override
501
                public void removeLabel(Unit<?> unit) {
502
                        unitToName.remove(unit);
1✔
503
                        nameToUnit.entrySet().removeIf(e -> e.getValue().equals(unit));
1✔
504
                }
1✔
505

506
        @Override
507
        public void alias(Unit<?> unit, String alias) {
508
            if (!isValidIdentifier(alias))
1✔
509
                throw new IllegalArgumentException("Alias: " + alias + " is not a valid identifier.");
×
510
            synchronized (this) {
1✔
511
                nameToUnit.put(alias, unit);
1✔
512
            }
1✔
513
        }
1✔
514

515
        @Override
516
                public void removeAlias(Unit<?> unit, String alias) {
517
                        nameToUnit.remove(alias);
1✔
518
                }
1✔
519
        
520
                @Override
521
                public void removeAliases(Unit<?> unit) {
522
                        final String alias = unitToName.get(unit);
1✔
523
                        nameToUnit.entrySet().removeIf(e -> e.getValue().equals(unit) && !e.getKey().equals(alias));
1✔
524
                }
1✔
525

526
        @Override
527
        protected boolean isValidIdentifier(String name) {
528
            if ((name == null) || (name.length() == 0))
1✔
529
                return false;
×
530
            return isUnitIdentifierPart(name.charAt(0));
1✔
531
        }
532
        
533
        /**
534
         * Applies {@link #label(Unit, String)} for this unit and all standard prefixes.
535
         * 
536
         * @param unit a unit
537
         * @param label a label
538
         */
539
        private void labelWithPrefixes(Unit<?> unit, String label) {
540
                label(unit, label);
1✔
541
                // TODO try to optimize this
542
            for (int i = 0; i < METRIC_PREFIX_SYMBOLS.length; i++) {
1✔
543
                    label(unit.prefix(MetricPrefix.values()[i]), METRIC_PREFIX_SYMBOLS[i] + label);
1✔
544
            }
545
            for (int i = 0; i < BINARY_PREFIX_SYMBOLS.length; i++) {
1✔
546
                    label(unit.prefix(BinaryPrefix.values()[i]), BINARY_PREFIX_SYMBOLS[i] + label);
1✔
547
            }
548
        }
1✔
549
        
550
        /**
551
         * Applies {@link #alias(Unit, String)} for this unit and all standard prefixes.
552
         * 
553
         * @param unit a unit
554
         * @param alias an alias
555
         */
556
        private void aliasWithPrefixes(Unit<?> unit, String alias) {
557
                alias(unit, alias);
1✔
558
                // TODO try to optimize this
559
            for (int i = 0; i < METRIC_PREFIX_SYMBOLS.length; i++) {
1✔
560
                alias(unit.prefix(MetricPrefix.values()[i]), METRIC_PREFIX_SYMBOLS[i] + alias);
1✔
561
            }
562
            for (int i = 0; i < BINARY_PREFIX_SYMBOLS.length; i++) {
1✔
563
                    alias(unit.prefix(BinaryPrefix.values()[i]), BINARY_PREFIX_SYMBOLS[i] + alias);
1✔
564
            }
565

566
        }
1✔
567

568
        protected static boolean isUnitIdentifierPart(char ch) {
569
            return Character.isLetter(ch)
1✔
570
                    || (!Character.isWhitespace(ch) && !Character.isDigit(ch) && (ch != MIDDLE_DOT) && (ch != '*') && (ch != '/') && (ch != '(') && (ch != ')')
1✔
571
                            && (ch != '[') && (ch != ']') && (ch != '\u00b9') && (ch != '\u00b2') && (ch != '\u00b3') && (ch != '^') && (ch != '+') && (ch != '-'));
572
        }
573

574
        // Returns the name for the specified unit or null if product unit.
575
        protected String nameFor(Unit<?> unit) {
576
            // Searches label database.
577
            String label = unitToName.get(unit);
1✔
578
            if (label != null)
1✔
579
                return label;
1✔
580
            if (unit instanceof BaseUnit)
1✔
581
                return ((BaseUnit<?>) unit).getSymbol();
1✔
582
            if (unit instanceof AlternateUnit)
1✔
583
                return ((AlternateUnit<?>) unit).getSymbol();
1✔
584
            if (unit instanceof TransformedUnit) {
1✔
585
                TransformedUnit<?> tfmUnit = (TransformedUnit<?>) unit;
1✔
586
                if (tfmUnit.getSymbol() != null) {
1✔
587
                    return tfmUnit.getSymbol();
1✔
588
                }
589
                Unit<?> baseUnit = tfmUnit.getParentUnit();
1✔
590
                UnitConverter cvtr = tfmUnit.getConverter(); // tfmUnit.getSystemConverter();
1✔
591
                StringBuilder result = new StringBuilder();
1✔
592
                String baseUnitName = baseUnit.toString();
1✔
593
                String prefix = prefixFor(cvtr);
1✔
594
                if ((baseUnitName.indexOf(MIDDLE_DOT) >= 0) || (baseUnitName.indexOf('*') >= 0) || (baseUnitName.indexOf('/') >= 0)) {
1✔
595
                    // We could use parentheses whenever baseUnits is an
596
                    // instanceof ProductUnit, but most ProductUnits have
597
                    // aliases,
598
                    // so we'd end up with a lot of unnecessary parentheses.
599
                    result.append('(');
1✔
600
                    result.append(baseUnitName);
1✔
601
                    result.append(')');
1✔
602
                } else {
603
                    result.append(baseUnitName);
1✔
604
                }
605
                if (prefix != null) {
1✔
606
                    result.insert(0, prefix);
1✔
607
                } else {
608
                    if (cvtr instanceof AddConverter) {
1✔
609
                        result.append('+');
1✔
610
                        result.append(((AddConverter) cvtr).getOffset());
1✔
611
                    } else if (cvtr instanceof MultiplyConverter) {
1✔
612
                        Number scaleFactor = ((MultiplyConverter) cvtr).getFactor();
1✔
613
                        if(scaleFactor instanceof RationalNumber) {
1✔
614

615
                            RationalNumber rational = (RationalNumber)scaleFactor;
1✔
616
                            RationalNumber reciprocal = rational.reciprocal();
1✔
617
                            if(reciprocal.isInteger()) {
1✔
618
                                result.append('/');
1✔
619
                                result.append(reciprocal.toString()); // renders as integer
1✔
620
                            } else {
621
                                result.append('*');
1✔
622
                                result.append(scaleFactor);
1✔
623
                            }
624

625
                        } else {
1✔
626
                            result.append('*');
×
627
                            result.append(scaleFactor);
×
628
                        }
629

630
                    } else { // Other converters.
1✔
631
                        return "[" + baseUnit + "?]";
1✔
632
                    }
633
                }
634
                return result.toString();
1✔
635
            }
636
            if (unit instanceof AnnotatedUnit<?>) {
1✔
637
                AnnotatedUnit<?> annotatedUnit = (AnnotatedUnit<?>) unit;
1✔
638
                final StringBuilder annotable = new StringBuilder(nameFor(annotatedUnit.getActualUnit()));
1✔
639
                if (annotatedUnit.getAnnotation() != null) {
1✔
640
                    annotable.append('{'); // TODO maybe also configure this one similar to mix delimiter
1✔
641
                    annotable.append(annotatedUnit.getAnnotation());
1✔
642
                    annotable.append('}');
1✔
643
                }
644
                return annotable.toString();
1✔
645
            }
646
            return null; // Product unit.
1✔
647
        }
648

649
        // Returns the prefix for the specified unit converter.
650
        protected String prefixFor(UnitConverter converter) {
651
            for (int i = 0; i < METRIC_PREFIX_CONVERTERS.length; i++) {
1✔
652
                if (METRIC_PREFIX_CONVERTERS[i].equals(converter)) {
1✔
653
                    return METRIC_PREFIX_SYMBOLS[i];
1✔
654
                }
655
            }
656
            for (int j = 0; j < BINARY_PREFIX_CONVERTERS.length; j++) {
1✔
657
                if (BINARY_PREFIX_CONVERTERS[j].equals(converter)) {
1✔
658
                    return BINARY_PREFIX_SYMBOLS[j];
1✔
659
                }
660
            }
661
            return null; // TODO or return blank?
1✔
662
        }
663

664
        // Returns the unit for the specified name.
665
        protected Unit<?> unitFor(String name) {
666
            Unit<?> unit = nameToUnit.get(name);
1✔
667
            if (unit != null) {
1✔
668
                return unit;
1✔
669
            } else {
670
                unit = symbolToUnit.get(name);
1✔
671
            }
672
            return unit;
1✔
673
        }
674

675
        // //////////////////////////
676
        // Parsing.
677
        @SuppressWarnings({ "rawtypes", "unchecked" })
678
        public Unit<? extends Quantity> parseSingleUnit(CharSequence csq, ParsePosition pos) throws MeasurementParseException {
679
            int startIndex = pos.getIndex();
1✔
680
            String name = readIdentifier(csq, pos);
1✔
681
            Unit unit = unitFor(name);
1✔
682
            check(unit != null, name + " not recognized", csq, startIndex);
1✔
683
            return unit;
1✔
684
        }
685

686
        @SuppressWarnings({ "rawtypes", "unchecked" })
687
        @Override
688
        public Unit<? extends Quantity> parseProductUnit(CharSequence csq, ParsePosition pos) throws MeasurementParseException {
689
                Unit result = null;
1✔
690
                if (csq == null) {
1✔
691
                    throw new MeasurementParseException("Cannot parse null", csq, pos.getIndex());
×
692
            } else {
693
                    result = unitFor(csq.toString());
1✔
694
                    if (result != null)
1✔
695
                            return result;
1✔
696
            }
697
                result = AbstractUnit.ONE;
1✔
698
            Token token = nextToken(csq, pos);
1✔
699
            switch (token) {
1✔
700
            case IDENTIFIER:
701
                result = parseSingleUnit(csq, pos);
1✔
702
                break;
1✔
703
            case OPEN_PAREN:
704
                pos.setIndex(pos.getIndex() + 1);
×
705
                result = parseProductUnit(csq, pos);
×
706
                token = nextToken(csq, pos);
×
707
                check(token == Token.CLOSE_PAREN, "')' expected", csq, pos.getIndex());
×
708
                pos.setIndex(pos.getIndex() + 1);
×
709
                break;
×
710
            default:
711
                break;
712
            }
713
            token = nextToken(csq, pos);
1✔
714
            while (true) {
715
                switch (token) {
1✔
716
                case EXPONENT:
717
                    Exponent e = readExponent(csq, pos);
1✔
718
                    if (e.pow != 1) {
1✔
719
                        result = result.pow(e.pow);
1✔
720
                    }
721
                    if (e.root != 1) {
1✔
722
                        result = result.root(e.root);
1✔
723
                    }
724
                    break;
725
                case MULTIPLY:
726
                    pos.setIndex(pos.getIndex() + 1);
×
727
                    token = nextToken(csq, pos);
×
728
                    if (token == Token.INTEGER) {
×
729
                        long n = readLong(csq, pos);
×
730
                        if (n != 1) {
×
731
                            result = result.multiply(n);
×
732
                        }
733
                    } else if (token == Token.FLOAT) {
×
734
                        double d = readDouble(csq, pos);
×
735
                        if (d != 1.0) {
×
736
                            result = result.multiply(d);
×
737
                        }
738
                    } else {
×
739
                        result = result.multiply(parseProductUnit(csq, pos));
×
740
                    }
741
                    break;
×
742
                case DIVIDE:
743
                    pos.setIndex(pos.getIndex() + 1);
1✔
744
                    token = nextToken(csq, pos);
1✔
745
                    if (token == Token.INTEGER) {
1✔
746
                        long n = readLong(csq, pos);
×
747
                        if (n != 1) {
×
748
                            result = result.divide(n);
×
749
                        }
750
                    } else if (token == Token.FLOAT) {
1✔
751
                        double d = readDouble(csq, pos);
×
752
                        if (d != 1.0) {
×
753
                            result = result.divide(d);
×
754
                        }
755
                    } else {
×
756
                        result = result.divide(parseProductUnit(csq, pos));
1✔
757
                    }
758
                    break;
1✔
759
                case PLUS:
760
                    pos.setIndex(pos.getIndex() + 1);
×
761
                    token = nextToken(csq, pos);
×
762
                    if (token == Token.INTEGER) {
×
763
                        long n = readLong(csq, pos);
×
764
                        if (n != 1) {
×
765
                            result = result.shift(n);
×
766
                        }
767
                    } else if (token == Token.FLOAT) {
×
768
                        double d = readDouble(csq, pos);
×
769
                        if (d != 1.0) {
×
770
                            result = result.shift(d);
×
771
                        }
772
                    } else {
×
773
                        throw new MeasurementParseException("not a number", csq, pos.getIndex());
×
774
                    }
775
                    break;
776
                case EOF:
777
                case CLOSE_PAREN:
778
                    return result;
1✔
779
                default:
780
                    throw new MeasurementParseException(String.format("unexpected token %s", token),
1✔
781
                                    csq, pos.getIndex());
1✔
782
                }
783
                token = nextToken(csq, pos);
1✔
784
            }
785
        }
786

787
        private static Token nextToken(CharSequence csq, ParsePosition pos) {
788
            final int length = csq.length();
1✔
789
            while (pos.getIndex() < length) {
1✔
790
                char c = csq.charAt(pos.getIndex());
1✔
791
                if (isUnitIdentifierPart(c)) {
1✔
792
                    return Token.IDENTIFIER;
1✔
793
                } else if (c == '(') {
1✔
UNCOV
794
                    return Token.OPEN_PAREN;
×
795
                } else if (c == ')') {
1✔
UNCOV
796
                    return Token.CLOSE_PAREN;
×
797
                } else if ((c == '^') || (c == '\u00b9') || (c == '\u00b2') || (c == '\u00b3')) {
1✔
798
                    return Token.EXPONENT;
1✔
799
                } else if (c == '*') {
1✔
800
                    if (csq.length() == pos.getIndex() + 1) {
×
UNCOV
801
                        throw new MeasurementParseException("unexpected token " + Token.EOF, csq, pos.getIndex()); // return ;
×
802
                    }
803
                    char c2 = csq.charAt(pos.getIndex() + 1);
×
UNCOV
804
                    return c2 == '*' ? Token.EXPONENT : Token.MULTIPLY;
×
805
                } else if (c == MIDDLE_DOT) {
1✔
UNCOV
806
                    return Token.MULTIPLY;
×
807
                } else if (c == '/') {
1✔
808
                    return Token.DIVIDE;
1✔
809
                } else if (c == '+') {
1✔
UNCOV
810
                    return Token.PLUS;
×
811
                } else if ((c == '-') || Character.isDigit(c)) {
1✔
812
                    int index = pos.getIndex() + 1;
1✔
813
                    while ((index < length) && (Character.isDigit(c) || (c == '-') || (c == '.') || (c == 'E'))) {
1✔
814
                        c = csq.charAt(index++);
1✔
815
                        if (c == '.') {
1✔
UNCOV
816
                            return Token.FLOAT;
×
817
                        }
818
                    }
819
                    return Token.INTEGER;
1✔
820
                }
821
                pos.setIndex(pos.getIndex() + 1);
1✔
822
            }
1✔
823
            return Token.EOF;
1✔
824
        }
825

826
        private static void check(boolean expr, String message, CharSequence csq, int index) throws MeasurementParseException {
827
            if (!expr) {
1✔
828
                throw new MeasurementParseException(message, csq, index);
1✔
829
            }
830
        }
1✔
831

832
        private static Exponent readExponent(CharSequence csq, ParsePosition pos) {
833
            char c = csq.charAt(pos.getIndex());
1✔
834
            if (c == '^') {
1✔
835
                pos.setIndex(pos.getIndex() + 1);
1✔
836
            } else if (c == '*') {
×
UNCOV
837
                pos.setIndex(pos.getIndex() + 2);
×
838
            }
839
            final int length = csq.length();
1✔
840
            int pow = 0;
1✔
841
            boolean isPowNegative = false;
1✔
842
            boolean parseRoot = false;
1✔
843

844
            POWERLOOP: while (pos.getIndex() < length) {
1✔
845
                c = csq.charAt(pos.getIndex());
1✔
846
                switch(c) {
1✔
847
                case '-': isPowNegative = true; break;
×
848
                case '\u00b9': pow = pow * 10 + 1; break;
×
849
                case '\u00b2': pow = pow * 10 + 2; break;
×
UNCOV
850
                case '\u00b3': pow = pow * 10 + 3; break;
×
851
                case ':': parseRoot = true; break POWERLOOP;
1✔
852
                default:
853
                    if (c >= '0' && c <= '9') pow = pow * 10 + (c - '0');
1✔
854
                    else break POWERLOOP;
855
                }
856
                pos.setIndex(pos.getIndex() + 1);
1✔
857
            }
858
            if (pow == 0) pow = 1;
1✔
859

860
            int root = 0;
1✔
861
            boolean isRootNegative = false;
1✔
862
            if (parseRoot) {
1✔
863
                pos.setIndex(pos.getIndex() + 1);
1✔
864
                ROOTLOOP: while (pos.getIndex() < length) {
1✔
865
                    c = csq.charAt(pos.getIndex());
1✔
866
                    switch(c) {
1✔
867
                    case '-': isRootNegative = true; break;
×
868
                    case '\u00b9': root = root * 10 + 1; break;
×
869
                    case '\u00b2': root = root * 10 + 2; break;
×
UNCOV
870
                    case '\u00b3': root = root * 10 + 3; break;
×
871
                    default:
872
                        if (c >= '0' && c <= '9') root = root * 10 + (c - '0');
1✔
873
                        else break ROOTLOOP;
874
                    }
875
                    pos.setIndex(pos.getIndex() + 1);
1✔
876
                }
877
            }
878
            if (root == 0) root = 1;
1✔
879

880
            return new Exponent(isPowNegative ? -pow : pow, isRootNegative ? -root : root);
1✔
881
        }
882

883
        private static long readLong(CharSequence csq, ParsePosition pos) {
884
            final int length = csq.length();
×
885
            int result = 0;
×
886
            boolean isNegative = false;
×
887
            while (pos.getIndex() < length) {
×
888
                char c = csq.charAt(pos.getIndex());
×
889
                if (c == '-') {
×
890
                    isNegative = true;
×
891
                } else if ((c >= '0') && (c <= '9')) {
×
UNCOV
892
                    result = result * 10 + (c - '0');
×
893
                } else {
894
                    break;
895
                }
896
                pos.setIndex(pos.getIndex() + 1);
×
897
            }
×
UNCOV
898
            return isNegative ? -result : result;
×
899
        }
900

901
        private static double readDouble(CharSequence csq, ParsePosition pos) {
902
            final int length = csq.length();
×
903
            int start = pos.getIndex();
×
904
            int end = start + 1;
×
905
            while (end < length) {
×
906
                if ("0123456789+-.E".indexOf(csq.charAt(end)) < 0) {
×
UNCOV
907
                    break;
×
908
                }
UNCOV
909
                end += 1;
×
910
            }
911
            pos.setIndex(end + 1);
×
UNCOV
912
            return Double.parseDouble(csq.subSequence(start, end).toString());
×
913
        }
914

915
        private static String readIdentifier(CharSequence csq, ParsePosition pos) {
916
            final int length = csq.length();
1✔
917
            int start = pos.getIndex();
1✔
918
            int i = start;
1✔
919
            while ((++i < length) && isUnitIdentifierPart(csq.charAt(i))) {
1✔
920
            }
921
            pos.setIndex(i);
1✔
922
            return csq.subSequence(start, i).toString();
1✔
923
        }
924

925
        // //////////////////////////
926
        // Formatting.
927

928
        @Override
929
        public Appendable format(Unit<?> unit, Appendable appendable) throws IOException {
930
            String name = nameFor(unit);
1✔
931
            if (name != null) {
1✔
932
                return appendable.append(name);
1✔
933
            }
934
            if (!(unit instanceof ProductUnit)) {
1✔
UNCOV
935
                throw new IllegalArgumentException("Cannot format given Object as a Unit");
×
936
            }
937

938
            // Product unit.
939
            ProductUnit<?> productUnit = (ProductUnit<?>) unit;
1✔
940

941
            // Special case: self-powered product unit
942
            if (productUnit.getUnitCount() == 1 && productUnit.getUnit(0) instanceof ProductUnit) {
1✔
UNCOV
943
                final ProductUnit<?> powerUnit = (ProductUnit<?>) productUnit.getUnit(0);
×
944
                // is the sub-unit known under a given label?
UNCOV
945
                if (nameFor(powerUnit) == null)
×
946
                    // apply the power to the sub-units and format those instead
UNCOV
947
                    return format(ProductUnit.ofPow(powerUnit, productUnit.getUnitPow(0)), appendable);
×
948
            }
949

950
            int invNbr = 0;
1✔
951

952
            // Write positive exponents first.
953
            boolean start = true;
1✔
954
            for (int i = 0; i < productUnit.getUnitCount(); i++) {
1✔
955
                int pow = productUnit.getUnitPow(i);
1✔
956
                if (pow >= 0) {
1✔
957
                    if (!start) {
1✔
958
                        appendable.append(MIDDLE_DOT); // Separator.
1✔
959
                    }
960
                    name = nameFor(productUnit.getUnit(i));
1✔
961
                    int root = productUnit.getUnitRoot(i);
1✔
962
                    append(appendable, name, pow, root);
1✔
963
                    start = false;
1✔
964
                } else {
1✔
965
                    invNbr++;
1✔
966
                }
967
            }
968

969
            // Write negative exponents.
970
            if (invNbr != 0) {
1✔
971
                if (start) {
1✔
972
                    appendable.append('1'); // e.g. 1/s
1✔
973
                }
974
                appendable.append('/');
1✔
975
                if (invNbr > 1) {
1✔
976
                    appendable.append('(');
1✔
977
                }
978
                start = true;
1✔
979
                for (int i = 0; i < productUnit.getUnitCount(); i++) {
1✔
980
                    int pow = productUnit.getUnitPow(i);
1✔
981
                    if (pow < 0) {
1✔
982
                        name = nameFor(productUnit.getUnit(i));
1✔
983
                        int root = productUnit.getUnitRoot(i);
1✔
984
                        if (!start) {
1✔
985
                            appendable.append(MIDDLE_DOT); // Separator.
1✔
986
                        }
987
                        append(appendable, name, -pow, root);
1✔
988
                        start = false;
1✔
989
                    }
990
                }
991
                if (invNbr > 1) {
1✔
992
                    appendable.append(')');
1✔
993
                }
994
            }
995
            return appendable;
1✔
996
        }
997

998
        private static void append(Appendable appendable, CharSequence symbol, int pow, int root) throws IOException {
999
            appendable.append(symbol);
1✔
1000
            if ((pow != 1) || (root != 1)) {
1✔
1001
                // Write exponent.
1002
                if ((pow == 2) && (root == 1)) {
1✔
1003
                    appendable.append('\u00b2'); // Square
1✔
1004
                } else if ((pow == 3) && (root == 1)) {
1✔
1005
                    appendable.append('\u00b3'); // Cubic
1✔
1006
                } else {
1007
                    // Use general exponent form.
1008
                    appendable.append('^');
1✔
1009
                    appendable.append(String.valueOf(pow));
1✔
1010
                    if (root != 1) {
1✔
1011
                        appendable.append(':');
1✔
1012
                        appendable.append(String.valueOf(root));
1✔
1013
                    }
1014
                }
1015
            }
1016
        }
1✔
1017

1018
        // private static final long serialVersionUID = 1L;
1019

1020
        @Override
1021
        public Unit<?> parse(CharSequence csq) throws MeasurementParseException {
1022
            return parse(csq, 0);
1✔
1023
        }
1024

1025
        protected Unit<?> parse(CharSequence csq, int index) throws IllegalArgumentException {
1026
            return parse(csq, new ParsePosition(index));
1✔
1027
        }
1028

1029
        @Override
1030
        public Unit<?> parse(CharSequence csq, ParsePosition cursor) throws IllegalArgumentException {
1031
            return parseObject(csq.toString(), cursor);
1✔
1032
        }
1033
    }
1034

1035
    /**
1036
     * This class represents the ASCII_INSTANCE format.
1037
     */
1038
    private static final class ASCIIFormat extends DefaultFormat {
1039

1040
        private ASCIIFormat() {
1041
            super();
1042
        }
1043

1044
        private ASCIIFormat init() {
1045

1046
            // ASCII_INSTANCE
1047
            for (int i = 0; i < METRIC_UNITS.length; i++) {
1✔
1048
                Unit<?> si = METRIC_UNITS[i];
1✔
1049
                String symbol = (si instanceof BaseUnit) ? ((BaseUnit<?>) si).getSymbol() : ((AlternateUnit<?>) si).getSymbol();
1✔
1050
                if (isAllAscii(symbol))
1✔
1051
                    label(si, symbol);
1✔
1052
                for (int j = 0; j < METRIC_PREFIX_SYMBOLS.length; j++) {
1✔
1053
                    Unit<?> u = si.prefix(MetricPrefix.values()[j]);
1✔
1054
                    if ( "µ".equals(METRIC_PREFIX_SYMBOLS[j]) ) {
1✔
1055
                        label(u, "micro" + asciiSymbol(symbol));
1✔
1056
                    }
1057
                } // TODO what about BINARY_PREFIX here?
1058
            }
1059

1060
            // -- GRAM/KILOGRAM
1061

1062
            label(Units.GRAM, "g");
1✔
1063
            for(MetricPrefix prefix : MetricPrefix.values()) {
1✔
1064
                switch (prefix) {
1✔
1065
                case KILO:
1066
                    label(Units.KILOGRAM, "kg");
1✔
1067
                    break;
1✔
1068
                case MICRO:
1069
                    label(MICRO(Units.GRAM), "microg"); // instead of 'µg' -> 'microg'
1✔
1070
                    break;
1✔
1071
                default:
1072
                    label(Units.GRAM.prefix(prefix), prefix.getSymbol()+"g");
1✔
1073
                    break;
1074
                }
1075
            }
1076

1077
            // ASCIIFormat for Ohm
1078
            labelWithAsciiPrefixes(Units.OHM, "Ohm");
1✔
1079

1080
            // Special case for DEGREE_CELSIUS.
1081
            labelWithAsciiPrefixes(Units.CELSIUS, "Celsius");
1✔
1082
            aliasWithAsciiPrefixes(Units.CELSIUS, "Cel");
1✔
1083

1084
            label(Units.METRE, "m");
1✔
1085
            label(Units.SECOND, "s");
1✔
1086
            label(Units.KILOMETRE_PER_HOUR, "km/h");
1✔
1087
            alias(Units.SQUARE_METRE, "m2");
1✔
1088
            alias(Units.CUBIC_METRE, "m3");
1✔
1089

1090
            // -- LITRE
1091

1092
            label(Units.LITRE, "l");
1✔
1093
            for(Prefix prefix : MetricPrefix.values()) {
1✔
1094
                if(prefix==MICRO) {
1✔
1095
                    label(MICRO(Units.LITRE), "microL"); // instead of 'µL' -> 'microL'
1✔
1096
                } else {
1097
                    label(Units.LITRE.prefix(prefix), prefix.getSymbol()+"L");
1✔
1098
                }
1099
            }
1100
            label(Units.NEWTON, "N");
1✔
1101
            label(Units.RADIAN, "rad");
1✔
1102

1103
            label(AbstractUnit.ONE, "one");
1✔
1104

1105
            return this;
1✔
1106
        }
1107

1108

1109
        @Override
1110
        protected String nameFor(Unit<?> unit) {
1111
            // First search if specific ASCII_INSTANCE name should be used.
1112
            String name = unitToName.get(unit);
1✔
1113
            if (name != null)
1✔
1114
                return name;
1✔
1115
            // Else returns default name.
1116
            return DEFAULT_INSTANCE.nameFor(unit);
1✔
1117
        }
1118

1119
        @Override
1120
        protected Unit<?> unitFor(String name) {
1121
            // First search if specific ASCII_INSTANCE name.
1122
            Unit<?> unit = nameToUnit.get(name);
1✔
1123
            if (unit != null)
1✔
1124
                return unit;
1✔
1125
            // Else returns default mapping.
1126
            return DEFAULT_INSTANCE.unitFor(name);
1✔
1127
        }
1128

1129
        @Override
1130
        public String toString() {
1131
            return "SimpleUnitFormat - ASCII";
1✔
1132
        }
1133

1134
        @Override
1135
        public Appendable format(Unit<?> unit, Appendable appendable) throws IOException {
1136
            String name = nameFor(unit);
1✔
1137
            if (name != null)
1✔
1138
                return appendable.append(name);
1✔
1139
            if (!(unit instanceof ProductUnit))
1✔
UNCOV
1140
                throw new IllegalArgumentException("Cannot format given Object as a Unit");
×
1141

1142
            ProductUnit<?> productUnit = (ProductUnit<?>) unit;
1✔
1143
            for (int i = 0; i < productUnit.getUnitCount(); i++) {
1✔
1144
                if (i != 0) {
1✔
1145
                    appendable.append('*'); // Separator.
1✔
1146
                }
1147
                name = nameFor(productUnit.getUnit(i));
1✔
1148
                int pow = productUnit.getUnitPow(i);
1✔
1149
                int root = productUnit.getUnitRoot(i);
1✔
1150
                appendable.append(name);
1✔
1151
                if ((pow != 1) || (root != 1)) {
1✔
1152
                    // Use general exponent form.
1153
                    appendable.append('^');
1✔
1154
                    appendable.append(String.valueOf(pow));
1✔
1155
                    if (root != 1) {
1✔
1156
                        appendable.append(':');
1✔
1157
                        appendable.append(String.valueOf(root));
1✔
1158
                    }
1159
                }
1160
            }
1161
            return appendable;
1✔
1162
        }
1163

1164
        @Override
1165
        protected boolean isValidIdentifier(String name) {
1166
            if ((name == null) || (name.length() == 0))
1✔
UNCOV
1167
                return false;
×
1168
            // label must not begin with a digit or mathematical operator
1169
            return isUnitIdentifierPart(name.charAt(0)) && isAllAscii(name);
1✔
1170
            /*
1171
             * for (int i = 0; i < name.length(); i++) { if
1172
             * (!isAsciiCharacter(name.charAt(i))) return false; } return true;
1173
             */
1174
        }
1175
        
1176
        /**
1177
         * Applies {@link #alias(Unit, String)} for this unit and all standard prefixes, if the alias contains only ASCII characters.
1178
         * 
1179
         * @param unit a unit
1180
         * @param alias an alias
1181
         */
1182
        private void aliasWithAsciiPrefixes(Unit<?> unit, String alias) {
1183
                if (isValidIdentifier(alias)) {
1✔
1184
                        alias(unit, alias);
1✔
1185
                    for (int i = 0; i < METRIC_PREFIX_SYMBOLS.length; i++) {
1✔
1186
                        alias(unit.prefix(MetricPrefix.values()[i]), asciiPrefix(METRIC_PREFIX_SYMBOLS[i]) + alias);
1✔
1187
                    }
1188
                    for (int i = 0; i < BINARY_PREFIX_SYMBOLS.length; i++) {
1✔
1189
                            alias(unit.prefix(BinaryPrefix.values()[i]), asciiPrefix(BINARY_PREFIX_SYMBOLS[i]) + alias);
1✔
1190
                    }
1191
                }
1192
        }
1✔
1193
        
1194
        /**
1195
         * Applies {@link #label(Unit, String)} for this unit and all standard prefixes, if the label contains only ASCII characters.
1196
         * 
1197
         * @param unit a unit
1198
         * @param alias an label
1199
         */
1200
        private void labelWithAsciiPrefixes(Unit<?> unit, String label) {
1201
                if (isValidIdentifier(label)) {
1✔
1202
                        label(unit, label);
1✔
1203
                    for (int i = 0; i < METRIC_PREFIX_SYMBOLS.length; i++) {
1✔
1204
                            label(unit.prefix(MetricPrefix.values()[i]), asciiPrefix(METRIC_PREFIX_SYMBOLS[i]) + label);
1✔
1205
                    }
1206
                    for (int i = 0; i < BINARY_PREFIX_SYMBOLS.length; i++) {
1✔
1207
                            label(unit.prefix(BinaryPrefix.values()[i]), asciiPrefix(BINARY_PREFIX_SYMBOLS[i]) + label);
1✔
1208
                    }
1209
                }
1210
        }
1✔
1211
        
1212
        private static String asciiPrefix(String prefix) {
1213
            return "µ".equals(prefix) ? "micro" : prefix;
1✔
1214
        }
1215

1216
        private static String asciiSymbol(String s) {
1217
            return "Ω".equals(s) ? "Ohm" : s;
1✔
1218
        }
1219

1220
        /** to check if a string only contains US-ASCII_INSTANCE characters */
1221
        private static boolean isAllAscii(String input) {
1222
            boolean isASCII = true;
1✔
1223
            for (int i = 0; i < input.length(); i++) {
1✔
1224
                int c = input.charAt(i);
1✔
1225
                if (c > 0x7F) {
1✔
1226
                    isASCII = false;
1✔
1227
                    break;
1✔
1228
                }
1229
            }
1230
            return isASCII;
1✔
1231
        }
1232
    }
1233
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc