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

unitsofmeasurement / indriya / 2281

09 Feb 2025 03:47PM UTC coverage: 71.134% (+0.2%) from 70.912%
2281

push

circleci

keilw
LocalUnitFormat incorrecly formating mass units #434

https://github.com/unitsofmeasurement/indriya/issues/434

5 of 6 new or added lines in 1 file covered. (83.33%)

3721 of 5231 relevant lines covered (71.13%)

0.71 hits per line

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

54.11
/src/main/java/tech/units/indriya/format/LocalUnitFormat.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.ConverterFormatter.formatConverterLocal;
33
import static tech.units.indriya.format.FormatConstants.*;
34

35
import javax.measure.Quantity;
36
import javax.measure.Unit;
37
import javax.measure.UnitConverter;
38
import javax.measure.format.MeasurementParseException;
39
import tech.units.indriya.AbstractUnit;
40
import tech.units.indriya.internal.format.UnitFormatParser;
41
import tech.units.indriya.unit.AlternateUnit;
42
import tech.units.indriya.unit.AnnotatedUnit;
43
import tech.units.indriya.unit.BaseUnit;
44
import tech.units.indriya.unit.ProductUnit;
45
import tech.units.indriya.unit.TransformedUnit;
46

47
import static tech.units.indriya.unit.Units.CUBIC_METRE;
48
import static tech.units.indriya.unit.Units.GRAM;
49
import static tech.units.indriya.unit.Units.KILOGRAM;
50
import static tech.units.indriya.unit.Units.LITRE;
51

52
import java.io.IOException;
53
import java.io.StringReader;
54
import java.text.ParsePosition;
55
import java.util.Locale;
56
import java.util.Map;
57
import java.util.ResourceBundle;
58

59
/**
60
 * <p>
61
 * This class represents the locale sensitive format.
62
 * </p>
63
 *
64
 * <h3>Here is the grammar for CommonUnits in Extended Backus-Naur Form (EBNF)</h3>
65
 * <p>
66
 * Note that the grammar has been left-factored to be suitable for use by a top-down parser generator such as <a
67
 * href="https://javacc.dev.java.net/">JavaCC</a>
68
 * </p>
69
 * <table * align="center">
70
 * <tr>
71
 * <th colspan="3" align="left">Lexical Entities:</th>
72
 * </tr>
73
 * <tr valign="top">
74
 * <td>&lt;sign&gt;</td>
75
 * <td>:=</td>
76
 * <td>"+" | "-"</td>
77
 * </tr>
78
 * <tr valign="top">
79
 * <td>&lt;digit&gt;</td>
80
 * <td>:=</td>
81
 * <td>"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"</td>
82
 * </tr>
83
 * <tr valign="top">
84
 * <td>&lt;superscript_digit&gt;</td>
85
 * <td>:=</td>
86
 * <td>"⁰" | "¹" | "²" | "³" | "⁴" | "⁵" | "⁶" | "⁷" | "⁸" | "⁹"</td>
87
 * </tr>
88
 * <tr valign="top">
89
 * <td>&lt;integer&gt;</td>
90
 * <td>:=</td>
91
 * <td>(&lt;digit&gt;)+</td>
92
 * </tr>
93
 * <tr * valign="top">
94
 * <td>&lt;number&gt;</td>
95
 * <td>:=</td>
96
 * <td>(&lt;sign&gt;)? (&lt;digit&gt;)* (".")? (&lt;digit&gt;)+ (("e" | "E") (&lt;sign&gt;)? (&lt;digit&gt;)+)?</td>
97
 * </tr>
98
 * <tr valign="top">
99
 * <td>&lt;exponent&gt;</td>
100
 * <td>:=</td>
101
 * <td>( "^" ( &lt;sign&gt; )? &lt;integer&gt; ) <br>
102
 * | ( "^(" (&lt;sign&gt;)? &lt;integer&gt; ( "/" (&lt;sign&gt;)? &lt;integer&gt; )? ")" ) <br>
103
 * | ( &lt;superscript_digit&gt; )+</td>
104
 * </tr>
105
 * <tr valign="top">
106
 * <td>&lt;initial_char&gt;</td>
107
 * <td>:=</td>
108
 * <td>? Any Unicode character excluding the following: ASCII control & whitespace (&#92;u0000 - &#92;u0020), decimal digits '0'-'9', '('
109
 * (&#92;u0028), ')' (&#92;u0029), '*' (&#92;u002A), '+' (&#92;u002B), '-' (&#92;u002D), '.' (&#92;u002E), '/' (&#92;u005C), ':' (&#92;u003A), '^'
110
 * (&#92;u005E), '²' (&#92;u00B2), '³' (&#92;u00B3), '·' (&#92;u00B7), '¹' (&#92;u00B9), '⁰' (&#92;u2070), '⁴' (&#92;u2074), '⁵' (&#92;u2075), '⁶'
111
 * (&#92;u2076), '⁷' (&#92;u2077), '⁸' (&#92;u2078), '⁹' (&#92;u2079) ?</td>
112
 * </tr>
113
 * <tr valign="top">
114
 * <td>&lt;unit_identifier&gt;</td>
115
 * <td>:=</td>
116
 * <td>&lt;initial_char&gt; ( &lt;initial_char&gt; | &lt;digit&gt; )*</td>
117
 * </tr>
118
 * <tr>
119
 * <th colspan="3" align="left">Non-Terminals:</th>
120
 * </tr>
121
 * <tr * valign="top">
122
 * <td>&lt;unit_expr&gt;</td>
123
 * <td>:=</td>
124
 * <td>&lt;mix_expr&gt;</td>
125
 * </tr>
126
 * <tr valign="top">
127
 * <td>&lt;mix_expr&gt;</td>
128
 * <td>:=</td>
129
 * <td>&lt;add_expr&gt; ( ":" &lt;add_expr&gt; )*</td>
130
 * </tr>
131
 * <tr valign="top">
132
 * <td>&lt;add_expr&gt;</td>
133
 * <td>:=</td>
134
 * <td>( &lt;number&gt; &lt;sign&gt; )? &lt;mul_expr&gt; ( &lt;sign&gt; &lt;number&gt; )?</td>
135
 * </tr>
136
 * <tr valign="top">
137
 * <td>&lt;mul_expr&gt;</td>
138
 * <td>:=</td>
139
 * <td>&lt;exponent_expr&gt; ( ( ( "*" | "·" ) &lt;exponent_expr&gt; ) | ( "/" &lt;exponent_expr&gt; ) )*</td>
140
 * </tr>
141
 * <tr valign="top">
142
 * <td>&lt;exponent_expr&gt;</td>
143
 * <td>:=</td>
144
 * <td>( &lt;atomic_expr&gt; ( &lt;exponent&gt; )? ) <br>
145
 * | (&lt;integer&gt; "^" &lt;atomic_expr&gt;) <br>
146
 * | ( ( "log" ( &lt;integer&gt; )? ) | "ln" ) "(" &lt;add_expr&gt; ")" )</td>
147
 * </tr>
148
 * <tr valign="top">
149
 * <td>&lt;atomic_expr&gt;</td>
150
 * <td>:=</td>
151
 * <td>&lt;number&gt; <br>
152
 * | &lt;unit_identifier&gt; <br>
153
 * | ( "(" &lt;add_expr&gt; ")" )</td>
154
 * </tr>
155
 * </table>
156
 *
157
 * @author <a href="mailto:eric-r@northwestern.edu">Eric Russell</a>
158
 * @author <a href="mailto:werner@units.tech">Werner Keil</a>
159
 * @version 1.7, February 9, 2025
160
 * @since 1.0
161
 */
162
public class LocalUnitFormat extends AbstractUnitFormat {
163

164
  //////////////////////////////////////////////////////
165
  // Class variables                                  //
166
  //////////////////////////////////////////////////////
167
  /**
168
   * DefaultQuantityFactory locale instance. If the default locale is changed after the class is initialized, this instance will no longer be used.
169
   */
170
  private static final LocalUnitFormat DEFAULT_INSTANCE = new LocalUnitFormat(SymbolMap.of(ResourceBundle.getBundle(LocalUnitFormat.class
1✔
171
      .getPackage().getName() + ".messages")));
1✔
172

173
  // /////////////////
174
  // Class methods //
175
  // /////////////////
176
  /**
177
   * Returns the instance for the current default locale (non-ascii characters are allowed)
178
   */
179
  public static LocalUnitFormat getInstance() {
180
    return DEFAULT_INSTANCE;
1✔
181
  }
182

183
  /**
184
   * Returns an instance for the given locale.
185
   * 
186
   * @param locale the locale to use
187
   */
188
  public static LocalUnitFormat getInstance(Locale locale) {
189
    return new LocalUnitFormat(SymbolMap.of(ResourceBundle.getBundle(LocalUnitFormat.class.getPackage().getName() + ".messages", locale)));
1✔
190
  }
191

192
  /** Returns an instance for the given symbol map. */
193
  public static LocalUnitFormat getInstance(SymbolMap symbols) {
194
    return new LocalUnitFormat(symbols);
×
195
  }
196

197
  // //////////////////////
198
  // Instance variables //
199
  // //////////////////////
200
  /**
201
   * The symbol map used by this instance to map between {@link Unit Unit}s and <code>String</code>s, etc...
202
   */
203
  private final transient SymbolMap symbolMap;
204

205
  // ////////////////
206
  // Constructors //
207
  // ////////////////
208
  /**
209
   * Base constructor.
210
   *
211
   * @param symbols
212
   *          the symbol mapping.
213
   */
214
  private LocalUnitFormat(SymbolMap symbols) {
1✔
215
    symbolMap = symbols;
1✔
216
  }
1✔
217

218
  ////////////////////////
219
  // Instance methods //
220
  ////////////////////////
221
  /**
222
   * Get the symbol map used by this instance to map between {@link AbstractUnit Unit}s and <code>String</code>s, etc...
223
   * 
224
   * @return SymbolMap the current symbol map
225
   */
226
  protected SymbolMap getSymbols() {
227
    return symbolMap;
×
228
  }
229
  
230
  @Override
231
  public String toString() {
232
    return getClass().getSimpleName();
1✔
233
  }
234

235
  ////////////////
236
  // Formatting //
237
  ////////////////
238
  @Override
239
  public Appendable format(Unit<?> unit, Appendable appendable) throws IOException {
240
    if (!(unit instanceof AbstractUnit)) {
1✔
241
      return appendable.append(unit.toString()); // Unknown unit (use
×
242
      // intrinsic toString()
243
      // method)
244
    }
245
    formatInternal(unit, appendable);
1✔
246
    return appendable;
1✔
247
  }
248

249
  public boolean isLocaleSensitive() {
250
    return true;
1✔
251
  }
252

253
  protected Unit<?> parse(CharSequence csq, int index) throws MeasurementParseException {
254
    return parse(csq, new ParsePosition(index));
×
255
  }
256

257
  public Unit<?> parse(CharSequence csq, ParsePosition cursor) throws MeasurementParseException {
258
    // Parsing reads the whole character sequence from the parse position.
259
    int start = cursor.getIndex();
1✔
260
    int end = csq.length();
1✔
261
    if (end <= start) {
1✔
262
      return AbstractUnit.ONE;
×
263
    }
264
    String source = csq.subSequence(start, end).toString().trim();
1✔
265
    if (source.length() == 0) {
1✔
266
      return AbstractUnit.ONE;
×
267
    }
268
    try {
269
      UnitFormatParser parser = new UnitFormatParser(symbolMap, new StringReader(source));
1✔
270
      Unit<?> result = parser.parseUnit();
1✔
271
      cursor.setIndex(end);
1✔
272
      return result;
1✔
273
    } catch (TokenException e) {
×
274
      if (e.currentToken != null) {
×
275
        cursor.setErrorIndex(start + e.currentToken.endColumn);
×
276
      } else {
277
        cursor.setErrorIndex(start);
×
278
      }
279
      throw new IllegalArgumentException(e); // TODO should we throw
×
280
      // ParserException here,
281
      // too?
282
    } catch (TokenMgrError e) {
×
283
      cursor.setErrorIndex(start);
×
284
      throw new MeasurementParseException(e);
×
285
    }
286
  }
287

288
  @Override
289
  public Unit<? extends Quantity<?>> parse(CharSequence csq) throws MeasurementParseException {
290
    return parse(csq, new ParsePosition(0));
1✔
291
  }
292

293
  /**
294
   * Format the given unit to the given StringBuilder, then return the operator precedence of the outermost operator in the unit expression that was
295
   * formatted. See {@link ConverterFormat} for the constants that define the various precedence values.
296
   * 
297
   * @param unit
298
   *          the unit to be formatted
299
   * @param buffer
300
   *          the <code>StringBuilder</code> to be written to
301
   * @return the operator precedence of the outermost operator in the unit expression that was output
302
   */
303
  @SuppressWarnings({ "rawtypes", "unchecked" })
304
  private int formatInternal(Unit<?> unit, Appendable buffer) throws IOException {
305
    if (unit instanceof AnnotatedUnit<?>) {
1✔
306
      unit = ((AnnotatedUnit<?>) unit).getActualUnit();
×
307
      // } else if (unit instanceof ProductUnit<?>) {
308
      // ProductUnit<?> p = (ProductUnit<?>)unit;
309
    }
310
    // TODO is the behavior similar to EBNFUnitFormat for AnnotatedUnit?
311
    String symbol = symbolMap.getSymbol((AbstractUnit<?>) unit);
1✔
312
    if (symbol != null) {
1✔
313
      buffer.append(symbol);
1✔
314
      return NOOP_PRECEDENCE;
1✔
315
    } else if (unit instanceof ProductUnit<?> && unit.getBaseUnits() != null) {
1✔
316
      Map<Unit<?>, Integer> productUnits = (Map<Unit<?>, Integer>) unit.getBaseUnits();
1✔
317
      int negativeExponentCount = 0;
1✔
318
      // Write positive exponents first...
319
      boolean start = true;
1✔
320
      for (Map.Entry<Unit<?>, Integer> e : productUnits.entrySet()) {
1✔
321
        int pow = e.getValue();
1✔
322
        if (pow >= 0) {
1✔
323
          formatExponent(e.getKey(), pow, 1, !start, buffer);
1✔
324
          start = false;
1✔
325
        } else {
326
          negativeExponentCount += 1;
1✔
327
        }
328
      }
1✔
329
      // ..then write negative exponents.
330
      if (negativeExponentCount > 0) {
1✔
331
        if (start) {
1✔
332
          buffer.append('1');
×
333
        }
334
        buffer.append('/');
1✔
335
        if (negativeExponentCount > 1) {
1✔
336
          buffer.append('(');
×
337
        }
338
        start = true;
1✔
339
        for (Map.Entry<Unit<?>, Integer> e : productUnits.entrySet()) {
1✔
340
          int pow = e.getValue();
1✔
341
          if (pow < 0) {
1✔
342
            formatExponent(e.getKey(), -pow, 1, !start, buffer);
1✔
343
            start = false;
1✔
344
          }
345
        }
1✔
346
        if (negativeExponentCount > 1) {
1✔
347
          buffer.append(')');
×
348
        }
349
      }
350
      return PRODUCT_PRECEDENCE;
1✔
351
    } else if (unit instanceof BaseUnit<?>) {
1✔
352
      buffer.append(((BaseUnit<?>) unit).getSymbol());
×
353
      return NOOP_PRECEDENCE;
×
354
    } else if (unit instanceof AlternateUnit<?>) { // unit.getSymbol() !=
1✔
355
      // null) { // Alternate
356
      // unit.
357
      buffer.append(unit.getSymbol());
×
358
      return NOOP_PRECEDENCE;
×
359
    } else { // A transformed unit or new unit type!
360
      UnitConverter converter = null;
1✔
361
      boolean printSeparator = false;
1✔
362
      StringBuilder temp = new StringBuilder();
1✔
363
      int unitPrecedence = NOOP_PRECEDENCE;
1✔
364
      Unit<?> parentUnit = unit.getSystemUnit();
1✔
365
      converter = ((AbstractUnit<?>) unit).getSystemConverter();
1✔
366
      if (KILOGRAM.equals(parentUnit)) {
1✔
367
        // More special-case hackery to work around gram/kilogram
368
        // incosistency
369
        if (unit.equals(GRAM)) {
×
370
          buffer.append(symbolMap.getSymbol(GRAM));
×
371
          return NOOP_PRECEDENCE;
×
372
        }
373
        parentUnit = GRAM;
×
374
        if (unit instanceof TransformedUnit<?>) {
×
375
          converter = ((TransformedUnit<?>) unit).getConverter();
×
376
        } else {
377
          converter = unit.getConverterTo((Unit) GRAM);
×
378
        }
379
      } else if (CUBIC_METRE.equals(parentUnit)) {
1✔
380
        if (converter != null) {
×
381
          parentUnit = LITRE;
×
382
        }
383
      }
384

385
      if (unit instanceof TransformedUnit) {
1✔
386
          TransformedUnit<?> transUnit = (TransformedUnit<?>) unit;
1✔
387
          if (parentUnit == null) {
1✔
NEW
388
                  parentUnit = transUnit.getParentUnit();
×
389
          } else {
390
                  if (transUnit.getParentUnit() != null) {
1✔
391
                          parentUnit =  transUnit.getParentUnit();
1✔
392
                  }
393
          }
394
          converter = transUnit.getConverter();
1✔
395
      }
396

397
      unitPrecedence = formatInternal(parentUnit, temp);
1✔
398
      printSeparator = !parentUnit.equals(AbstractUnit.ONE);
1✔
399
      int result = formatConverterLocal(converter, printSeparator, unitPrecedence, temp, symbolMap);
1✔
400
      buffer.append(temp);
1✔
401
      return result;
1✔
402
    }
403
  }
404

405
  /**
406
   * Format the given unit raised to the given fractional power to the given <code>StringBuffer</code>.
407
   * 
408
   * @param unit
409
   *          Unit the unit to be formatted
410
   * @param pow
411
   *          int the numerator of the fractional power
412
   * @param root
413
   *          int the denominator of the fractional power
414
   * @param continued
415
   *          boolean <code>true</code> if the converter expression should begin with an operator, otherwise <code>false</code>. This will always be
416
   *          true unless the unit being modified is equal to Unit.ONE.
417
   * @param buffer
418
   *          StringBuffer the buffer to append to. No assumptions should be made about its content.
419
   */
420
  private void formatExponent(Unit<?> unit, int pow, int root, boolean continued, Appendable buffer) throws IOException {
421
    if (continued) {
1✔
422
      buffer.append(MIDDLE_DOT);
×
423
    }
424
    StringBuffer temp = new StringBuffer();
1✔
425
    int unitPrecedence = formatInternal(unit, temp);
1✔
426
    if (unitPrecedence < PRODUCT_PRECEDENCE) {
1✔
427
      temp.insert(0, '(');
×
428
      temp.append(')');
×
429
    }
430
    buffer.append(temp);
1✔
431
    if ((root == 1) && (pow == 1)) {
1✔
432
      // do nothing
433
    } else if ((root == 1) && (pow > 1)) {
×
434
      String powStr = Integer.toString(pow);
×
435
      for (int i = 0; i < powStr.length(); i += 1) {
×
436
        char c = powStr.charAt(i);
×
437
        switch (c) {
×
438
          case '0':
439
            buffer.append('\u2070');
×
440
            break;
×
441
          case '1':
442
            buffer.append('\u00b9');
×
443
            break;
×
444
          case '2':
445
            buffer.append('\u00b2');
×
446
            break;
×
447
          case '3':
448
            buffer.append('\u00b3');
×
449
            break;
×
450
          case '4':
451
            buffer.append('\u2074');
×
452
            break;
×
453
          case '5':
454
            buffer.append('\u2075');
×
455
            break;
×
456
          case '6':
457
            buffer.append('\u2076');
×
458
            break;
×
459
          case '7':
460
            buffer.append('\u2077');
×
461
            break;
×
462
          case '8':
463
            buffer.append('\u2078');
×
464
            break;
×
465
          case '9':
466
            buffer.append('\u2079');
×
467
            break;
468
        }
469
      }
470
    } else if (root == 1) {
×
471
      buffer.append("^");
×
472
      buffer.append(String.valueOf(pow));
×
473
    } else {
474
      buffer.append("^(");
×
475
      buffer.append(String.valueOf(pow));
×
476
      buffer.append('/');
×
477
      buffer.append(String.valueOf(root));
×
478
      buffer.append(')');
×
479
    }
480
  }
1✔
481
}
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