Commit 4b384b92 authored by Luc Maisonobe's avatar Luc Maisonobe

Merge branch 'issue-776' into develop

Conflicts:
	src/changes/changes.xml
parents 26d003b1 3380d717
Pipeline #1034 passed with stages
in 31 minutes and 48 seconds
......@@ -21,6 +21,9 @@
</properties>
<body>
<release version="11.0" date="TBD" description="TBD">
<action dev="luc" type="fix" issue="776">
Fixed associativity in units parsing.
</action>
<action dev="bryan" type="update" issue="773">
TimeStampedFieldAngularCoordinates now implements FieldTimeStamped.
</action>
......
......@@ -16,6 +16,7 @@
*/
package org.orekit.utils.units;
import org.hipparchus.fraction.Fraction;
import org.orekit.errors.OrekitException;
import org.orekit.errors.OrekitMessages;
......@@ -110,42 +111,90 @@ class Lexer {
}
if (current > start) {
final String identifier = unitSpecification.subSequence(start, current).toString();
return emit(current, TokenType.PREFIXED_UNIT, PrefixedUnit.valueOf(identifier), 0);
return emit(current, TokenType.PREFIXED_UNIT, PrefixedUnit.valueOf(identifier), 0, 1);
}
// look for power
if ((start < end - 1) &&
unitSpecification.charAt(start) == '*' &&
unitSpecification.charAt(start) == '*' &&
unitSpecification.charAt(start + 1) == '*') {
// power indicator as **
return emit(start + 2, TokenType.POWER, null, 0);
return emit(start + 2, TokenType.POWER, null, 0, 1);
} else if (unitSpecification.charAt(start) == '^') {
// power indicator as ^
return emit(start + 1, TokenType.POWER, null, 0);
return emit(start + 1, TokenType.POWER, null, 0, 1);
} else if (convertSuperscript(start) != ' ' &&
last != null &&
last.getType() != TokenType.POWER) {
// virtual power indicator as we switch to superscript characters
return emit(start, TokenType.POWER, null, 0);
return emit(start, TokenType.POWER, null, 0, 1);
}
// look for one character tokens
if (unitSpecification.charAt(start) == '*') {
return emit(start + 1, TokenType.MULTIPLICATION, null, 0);
return emit(start + 1, TokenType.MULTIPLICATION, null, 0, 1);
} else if (unitSpecification.charAt(start) == '×') {
return emit(start + 1, TokenType.MULTIPLICATION, null, 0);
return emit(start + 1, TokenType.MULTIPLICATION, null, 0, 1);
} else if (unitSpecification.charAt(start) == '.') {
return emit(start + 1, TokenType.MULTIPLICATION, null, 0);
return emit(start + 1, TokenType.MULTIPLICATION, null, 0, 1);
} else if (unitSpecification.charAt(start) == '/') {
return emit(start + 1, TokenType.DIVISION, null, 0);
return emit(start + 1, TokenType.DIVISION, null, 0, 1);
} else if (unitSpecification.charAt(start) == '⁄') {
return emit(start + 1, TokenType.DIVISION, null, 0);
return emit(start + 1, TokenType.DIVISION, null, 0, 1);
} else if (unitSpecification.charAt(start) == '(') {
return emit(start + 1, TokenType.OPEN, null, 0);
return emit(start + 1, TokenType.OPEN, null, 0, 1);
} else if (unitSpecification.charAt(start) == ')') {
return emit(start + 1, TokenType.CLOSE, null, 0);
return emit(start + 1, TokenType.CLOSE, null, 0, 1);
} else if (unitSpecification.charAt(start) == '√') {
return emit(start + 1, TokenType.SQUARE_ROOT, null, 0);
return emit(start + 1, TokenType.SQUARE_ROOT, null, 0, 1);
}
// look for special case "0.5" (used by CCSDS for square roots)
if ((start < end - 2) &&
unitSpecification.charAt(start) == '0' &&
unitSpecification.charAt(start + 1) == '.' &&
unitSpecification.charAt(start + 2) == '5') {
// ½ written as decimal number
return emit(start + 3, TokenType.FRACTION, null, 1, 2);
}
// look for unicode fractions
if (unitSpecification.charAt(start) == '¼') {
return emit(start + 1, TokenType.FRACTION, null, 1, 4);
} else if (unitSpecification.charAt(start) == '½') {
return emit(start + 1, TokenType.FRACTION, null, 1, 2);
} else if (unitSpecification.charAt(start) == '¾') {
return emit(start + 1, TokenType.FRACTION, null, 3, 4);
} else if (unitSpecification.charAt(start) == '⅐') {
return emit(start + 1, TokenType.FRACTION, null, 1, 7);
} else if (unitSpecification.charAt(start) == '⅑') {
return emit(start + 1, TokenType.FRACTION, null, 1, 9);
} else if (unitSpecification.charAt(start) == '⅒') {
return emit(start + 1, TokenType.FRACTION, null, 1, 10);
} else if (unitSpecification.charAt(start) == '⅓') {
return emit(start + 1, TokenType.FRACTION, null, 1, 3);
} else if (unitSpecification.charAt(start) == '⅔') {
return emit(start + 1, TokenType.FRACTION, null, 2, 3);
} else if (unitSpecification.charAt(start) == '⅕') {
return emit(start + 1, TokenType.FRACTION, null, 1, 5);
} else if (unitSpecification.charAt(start) == '⅖') {
return emit(start + 1, TokenType.FRACTION, null, 2, 5);
} else if (unitSpecification.charAt(start) == '⅗') {
return emit(start + 1, TokenType.FRACTION, null, 3, 5);
} else if (unitSpecification.charAt(start) == '⅘') {
return emit(start + 1, TokenType.FRACTION, null, 4, 5);
} else if (unitSpecification.charAt(start) == '⅙') {
return emit(start + 1, TokenType.FRACTION, null, 1, 6);
} else if (unitSpecification.charAt(start) == '⅚') {
return emit(start + 1, TokenType.FRACTION, null, 5, 6);
} else if (unitSpecification.charAt(start) == '⅛') {
return emit(start + 1, TokenType.FRACTION, null, 1, 8);
} else if (unitSpecification.charAt(start) == '⅜') {
return emit(start + 1, TokenType.FRACTION, null, 3, 8);
} else if (unitSpecification.charAt(start) == '⅝') {
return emit(start + 1, TokenType.FRACTION, null, 5, 8);
} else if (unitSpecification.charAt(start) == '⅞') {
return emit(start + 1, TokenType.FRACTION, null, 7, 8);
}
// it must be an integer, either as regular character or as superscript
......@@ -180,7 +229,7 @@ class Lexer {
}
if (current > numberStart) {
// there were some digits
return emit(current, TokenType.INTEGER, null, sign * value);
return emit(current, TokenType.INTEGER, null, sign * value, 1);
}
throw generateException();
......@@ -198,15 +247,17 @@ class Lexer {
* @param after index after token
* @param type token type
* @param unit prefixed unit value
* @param value integer value
* @param numerator value of the token numerator
* @param denominator value of the token denominator
* @return new token
*/
private Token emit(final int after, final TokenType type,
final PrefixedUnit unit, final int value) {
private Token emit(final int after, final TokenType type, final PrefixedUnit unit,
final int numerator, final int denominator) {
final CharSequence subString = unitSpecification.subSequence(start, after);
start = after;
nextToLast = last;
last = new Token(subString, type, unit, value);
last = new Token(subString, type, unit, numerator,
denominator == 1 ? null : new Fraction(numerator, denominator));
return last;
}
......
......@@ -22,21 +22,22 @@ import org.hipparchus.fraction.Fraction;
* <p>
* This fairly basic parser uses recursive descent with the following grammar,
* where '*' can in fact be either '*', '×' or '.', '/' can be either '/' or '⁄'
* and '^' can be either '^', "**" or implicit with switch to superscripts.
* and '^' can be either '^', "**" or implicit with switch to superscripts,
* and fraction are either unicode fractions like ½ or ⅞ or the decimal value 0.5.
* The special case "n/a" corresponds to {@link PredefinedUnit#NONE}.
* </p>
* <pre>
* unit → "n/a" | chain
* chain → operand operation
* operand → '√' simple | simple power
* operation → '*' chain | '/' chain | ε
* power → '^' exponent | ε
* exponent → integer | '(' integer denominator ')'
* denominator → '/' integer | ε
* simple → predefined | '(' chain ')'
* unit ::= "n/a" | chain
* chain ::= operand { ('*' | '/') operand }
* operand ::= '√' simple | simple power
* power ::= '^' exponent | ε
* exponent ::= 'fraction' | integer | '(' integer denominator ')'
* denominator ::= '/' integer | ε
* simple ::= predefined | '(' chain ')'
* </pre>
* <p>
* This parses correctly units like MHz, km/√d, kg.m.s⁻¹, µas^(2/5)/(h**(2)×m)³, km/√(kg.s), √kg*km** (3/2) /(µs^2*Ω⁻⁷).
* This parses correctly units like MHz, km/√d, kg.m.s⁻¹, µas^⅖/(h**(2)×m)³, km/√(kg.s),
* √kg*km** (3/2) /(µs^2*Ω⁻⁷), km**0.5/s
* Note that we don't accept both square root and power on the same operand, so km/√d³ is refused (but km/√(d³) is accepted).
* Note that "nd" does not stands for "not-defined" but for "nano-day"…
* </p>
......@@ -73,7 +74,18 @@ class Parser {
* @return chain unit
*/
private static Unit chain(final Lexer lexer) {
return operation(operand(lexer), lexer);
Unit chain = operand(lexer);
for (Token token = lexer.next(); token != null; token = lexer.next()) {
if (checkType(token, TokenType.MULTIPLICATION)) {
chain = chain.multiply(null, operand(lexer));
} else if (checkType(token, TokenType.DIVISION)) {
chain = chain.divide(null, operand(lexer));
} else {
lexer.pushBack();
break;
}
}
return chain;
}
/** Parse an operand.
......@@ -89,30 +101,15 @@ class Parser {
return simple(lexer).power(null, Fraction.ONE_HALF);
} else {
lexer.pushBack();
return simple(lexer).power(null, power(lexer));
}
}
/** Parse an operation.
* @param lhs left hand side unit
* @param lexer lexer providing tokens
* @return simple unit
*/
private static Unit operation(final Unit lhs, final Lexer lexer) {
final Token token = lexer.next();
if (checkType(token, TokenType.MULTIPLICATION)) {
return lhs.multiply(null, chain(lexer));
} else if (checkType(token, TokenType.DIVISION)) {
return lhs.divide(null, chain(lexer));
} else {
lexer.pushBack();
return lhs;
final Unit simple = simple(lexer);
final Fraction exponent = power(lexer);
return exponent == null ? simple : simple.power(null, exponent);
}
}
/** Parse a power operation.
* @param lexer lexer providing tokens
* @return exponent
* @return exponent, or null if no exponent
*/
private static Fraction power(final Lexer lexer) {
final Token token = lexer.next();
......@@ -120,7 +117,7 @@ class Parser {
return exponent(lexer);
} else {
lexer.pushBack();
return Fraction.ONE;
return null;
}
}
......@@ -130,12 +127,14 @@ class Parser {
*/
private static Fraction exponent(final Lexer lexer) {
final Token token = lexer.next();
if (checkType(token, TokenType.INTEGER)) {
return new Fraction(token.getValue());
if (checkType(token, TokenType.FRACTION)) {
return token.getFraction();
} else if (checkType(token, TokenType.INTEGER)) {
return new Fraction(token.getInt());
} else {
lexer.pushBack();
accept(lexer, TokenType.OPEN);
final int num = accept(lexer, TokenType.INTEGER).getValue();
final int num = accept(lexer, TokenType.INTEGER).getInt();
final int den = denominator(lexer);
accept(lexer, TokenType.CLOSE);
return new Fraction(num, den);
......@@ -149,7 +148,7 @@ class Parser {
private static int denominator(final Lexer lexer) {
final Token token = lexer.next();
if (checkType(token, TokenType.DIVISION)) {
return accept(lexer, TokenType.INTEGER).getValue();
return accept(lexer, TokenType.INTEGER).getInt();
} else {
lexer.pushBack();
return 1;
......
......@@ -16,6 +16,8 @@
*/
package org.orekit.utils.units;
import org.hipparchus.fraction.Fraction;
/** Unit token.
* @author Luc Maisonobe
* @since 11.0
......@@ -32,20 +34,25 @@ class Token {
private final PrefixedUnit unit;
/** Integer value. */
private final int value;
private final int integer;
/** Fraction value. */
private final Fraction fraction;
/** Build a token.
* @param subString substring corresponding to the token
* @param type token type
* @param unit prefixed unit value
* @param value integer value of the token
* @param integer integer value
* @param fraction fraction value
*/
Token(final CharSequence subString, final TokenType type,
final PrefixedUnit unit, final int value) {
final PrefixedUnit unit, final int integer, final Fraction fraction) {
this.subString = subString;
this.type = type;
this.unit = unit;
this.value = value;
this.integer = integer;
this.fraction = fraction;
}
/** Get the substring corresponding to the token.
......@@ -69,11 +76,18 @@ class Token {
return unit;
}
/** Get the integer value.
/** Get the integer value (numerator in case of fraction).
* @return integer value
*/
public int getValue() {
return value;
public int getInt() {
return integer;
}
/** Get the fraction value.
* @return fraction value
*/
public Fraction getFraction() {
return fraction;
}
}
......@@ -44,6 +44,9 @@ enum TokenType {
SQUARE_ROOT,
/** Integer. */
INTEGER;
INTEGER,
/** Fraction. */
FRACTION;
}
......@@ -414,10 +414,10 @@ public class Unit implements Serializable {
* <p>
* All the SI prefix (from "y", yocto, to "Y", Yotta) are accepted. Beware
* that some combinations are forbidden, for example "Pa" is Pascal, not
* peta-years, and "as" is arcsecond for us, not atto-seconds, because many people
* in the space field use mas for milli-arcseconds and µas for micro-arcseconds.
* Beware that prefixes are case-sensitive! Prefix and units must be joined
* without blanks.
* peta-years, and "as" is arcsecond for this parser, not atto-seconds, because
* many people in the space field use mas for milli-arcseconds and µas for
* micro-arcseconds. Beware that prefixes are case-sensitive! Prefix and units
* must be joined without blanks.
* </p>
* <ul>
* <li>multiplication can specified with either "*", "×" or "." as the operator</li>
......@@ -430,12 +430,22 @@ public class Unit implements Serializable {
* </li>
* </ul>
* <p>
* Fractional exponents are allowed, but only with regular characters (because unicode
* does not provide a superscript '/'). Negative exponents can be used too.
* Exponents can be specified in different ways:
* <ul>
* <li>as an integer, as in "m^-2" or "m⁻²"</li>
* <li>directly as unicode characters for the few fractions that unicode supports, as in "Ω^⅞"</li>
* <li>as the special decimal value 0.5 which is used by CCSDS, as in "km**0.5"</li>
* <li>as a pair of parentheses surrounding two integers separated by a '/', as in "Pa^(11/12)"</li>
* </ul>
* For integer exponents, the digits must be ASCII digits from the Basic Latin block from
* unicode if explicit exponent maker "**" or "^" was used, or using unicode superscript
* digits if implicit exponentiation (i.e. no markers at all) is used. Unicode superscripts
* are not allowed fractional exponents because unicode does not provide a superscript '/'.
* Negative exponents can be used too.
* </p>
* <p>
* These rules mean all the following (silly) examples are parsed properly:
* MHz, km/√d, kg.m.s⁻¹, µas^(2/5)/(h**(2)×m)³, km/√(kg.s)
* MHz, km/√d, kg.m.s⁻¹, µas^/(h**(2)×m)³, km/√(kg.s), km**0.5
* </p>
* @param unitSpecification unit specification to parse
* @return parsed unit
......
......@@ -16,6 +16,7 @@
*/
package org.orekit.utils.units;
import org.hipparchus.fraction.Fraction;
import org.junit.Assert;
import org.junit.Test;
import org.orekit.errors.OrekitException;
......@@ -31,84 +32,84 @@ public class LexerTest {
@Test
public void testAllTypes() {
final Lexer lexer = new Lexer("√kg*km** (3/2) /(µs^2*Ω⁻⁷)");
expect(lexer, "√", TokenType.SQUARE_ROOT, null, null, 0);
expect(lexer, "kg", TokenType.PREFIXED_UNIT, null, Unit.KILOGRAM, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "km", TokenType.PREFIXED_UNIT, Prefix.KILO, Unit.METRE, 0);
expect(lexer, "**", TokenType.POWER, null, null, 0);
expect(lexer, "(", TokenType.OPEN, null, null, 0);
expect(lexer, "3", TokenType.INTEGER, null, null, 3);
expect(lexer, "/", TokenType.DIVISION, null, null, 0);
expect(lexer, "2", TokenType.INTEGER, null, null, 2);
expect(lexer, ")", TokenType.CLOSE, null, null, 0);
expect(lexer, "/", TokenType.DIVISION, null, null, 0);
expect(lexer, "(", TokenType.OPEN, null, null, 0);
expect(lexer, "µs", TokenType.PREFIXED_UNIT, Prefix.MICRO, Unit.SECOND, 0);
expect(lexer, "^", TokenType.POWER, null, null, 0);
expect(lexer, "2", TokenType.INTEGER, null, null, 2);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "Ω", TokenType.PREFIXED_UNIT, null, Unit.OHM, 0);
expect(lexer, "", TokenType.POWER, null, null, 0);
expect(lexer, "⁻⁷", TokenType.INTEGER, null, null, -7);
expect(lexer, ")", TokenType.CLOSE, null, null, 0);
expect(lexer, "√", TokenType.SQUARE_ROOT, null, null, 0, 1);
expect(lexer, "kg", TokenType.PREFIXED_UNIT, null, Unit.KILOGRAM, 0, 1);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0, 1);
expect(lexer, "km", TokenType.PREFIXED_UNIT, Prefix.KILO, Unit.METRE, 0, 1);
expect(lexer, "**", TokenType.POWER, null, null, 0, 1);
expect(lexer, "(", TokenType.OPEN, null, null, 0, 1);
expect(lexer, "3", TokenType.INTEGER, null, null, 3, 1);
expect(lexer, "/", TokenType.DIVISION, null, null, 0, 1);
expect(lexer, "2", TokenType.INTEGER, null, null, 2, 1);
expect(lexer, ")", TokenType.CLOSE, null, null, 0, 1);
expect(lexer, "/", TokenType.DIVISION, null, null, 0, 1);
expect(lexer, "(", TokenType.OPEN, null, null, 0, 1);
expect(lexer, "µs", TokenType.PREFIXED_UNIT, Prefix.MICRO, Unit.SECOND, 0, 1);
expect(lexer, "^", TokenType.POWER, null, null, 0, 1);
expect(lexer, "2", TokenType.INTEGER, null, null, 2, 1);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0, 1);
expect(lexer, "Ω", TokenType.PREFIXED_UNIT, null, Unit.OHM, 0, 1);
expect(lexer, "", TokenType.POWER, null, null, 0, 1);
expect(lexer, "⁻⁷", TokenType.INTEGER, null, null, -7, 1);
expect(lexer, ")", TokenType.CLOSE, null, null, 0, 1);
Assert.assertNull(lexer.next());
}
@Test
public void testRegularExponent() {
final Lexer lexer = new Lexer("N^123450 × MHz^-98765");
expect(lexer, "N", TokenType.PREFIXED_UNIT, null, Unit.NEWTON, 0);
expect(lexer, "^", TokenType.POWER, null, null, 0);
expect(lexer, "123450", TokenType.INTEGER, null, null, 123450);
expect(lexer, "×", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "MHz", TokenType.PREFIXED_UNIT, Prefix.MEGA, Unit.HERTZ, 0);
expect(lexer, "^", TokenType.POWER, null, null, 0);
expect(lexer, "-98765", TokenType.INTEGER, null, null, -98765);
expect(lexer, "N", TokenType.PREFIXED_UNIT, null, Unit.NEWTON, 0, 1);
expect(lexer, "^", TokenType.POWER, null, null, 0, 1);
expect(lexer, "123450", TokenType.INTEGER, null, null, 123450, 1);
expect(lexer, "×", TokenType.MULTIPLICATION, null, null, 0, 1);
expect(lexer, "MHz", TokenType.PREFIXED_UNIT, Prefix.MEGA, Unit.HERTZ, 0, 1);
expect(lexer, "^", TokenType.POWER, null, null, 0, 1);
expect(lexer, "-98765", TokenType.INTEGER, null, null, -98765, 1);
Assert.assertNull(lexer.next());
}
@Test
public void testSuperscriptExponent() {
final Lexer lexer = new Lexer("SFU⁺¹²³⁴⁵⁰ ⁄ mas⁻⁹⁸⁷⁶⁵");
expect(lexer, "SFU", TokenType.PREFIXED_UNIT, null, Unit.SOLAR_FLUX_UNIT, 0);
expect(lexer, "", TokenType.POWER, null, null, 0);
expect(lexer, "⁺¹²³⁴⁵⁰", TokenType.INTEGER, null, null, 123450);
expect(lexer, "⁄", TokenType.DIVISION, null, null, 0);
expect(lexer, "mas", TokenType.PREFIXED_UNIT, Prefix.MILLI, Unit.ARC_SECOND, 0);
expect(lexer, "", TokenType.POWER, null, null, 0);
expect(lexer, "⁻⁹⁸⁷⁶⁵", TokenType.INTEGER, null, null, -98765);
expect(lexer, "SFU", TokenType.PREFIXED_UNIT, null, Unit.SOLAR_FLUX_UNIT, 0, 1);
expect(lexer, "", TokenType.POWER, null, null, 0, 1);
expect(lexer, "⁺¹²³⁴⁵⁰", TokenType.INTEGER, null, null, 123450, 1);
expect(lexer, "⁄", TokenType.DIVISION, null, null, 0, 1);
expect(lexer, "mas", TokenType.PREFIXED_UNIT, Prefix.MILLI, Unit.ARC_SECOND, 0, 1);
expect(lexer, "", TokenType.POWER, null, null, 0, 1);
expect(lexer, "⁻⁹⁸⁷⁶⁵", TokenType.INTEGER, null, null, -98765, 1);
Assert.assertNull(lexer.next());
}
@Test
public void testSignWithoutDigits() {
final Lexer lexer = new Lexer("Pa⁻");
expect(lexer, "Pa", TokenType.PREFIXED_UNIT, null, Unit.PASCAL, 0);
expect(lexer, "", TokenType.POWER, null, null, 0);
expect(lexer, "Pa", TokenType.PREFIXED_UNIT, null, Unit.PASCAL, 0, 1);
expect(lexer, "", TokenType.POWER, null, null, 0, 1);
expectFailure(lexer);
}
@Test
public void testMultipleSigns() {
final Lexer lexer = new Lexer("MJ⁻⁺²");
expect(lexer, "MJ", TokenType.PREFIXED_UNIT, Prefix.MEGA, Unit.JOULE, 0);
expect(lexer, "", TokenType.POWER, null, null, 0);
expect(lexer, "MJ", TokenType.PREFIXED_UNIT, Prefix.MEGA, Unit.JOULE, 0, 1);
expect(lexer, "", TokenType.POWER, null, null, 0, 1);
expectFailure(lexer);
}
@Test
public void testUnknownCharacter() {
final Lexer lexer = new Lexer("pW^2#");
expect(lexer, "pW", TokenType.PREFIXED_UNIT, Prefix.PICO, Unit.WATT, 0);
expect(lexer, "^", TokenType.POWER, null, null, 0);
expect(lexer, "2", TokenType.INTEGER, null, null, 2);
expect(lexer, "pW", TokenType.PREFIXED_UNIT, Prefix.PICO, Unit.WATT, 0, 1);
expect(lexer, "^", TokenType.POWER, null, null, 0, 1);
expect(lexer, "2", TokenType.INTEGER, null, null, 2, 1);
expectFailure(lexer);
}
@Test
public void testPercentageCharacter() {
final Lexer lexer = new Lexer("%");
expect(lexer, "%", TokenType.PREFIXED_UNIT, null, Unit.PERCENT, 0);
expect(lexer, "%", TokenType.PREFIXED_UNIT, null, Unit.PERCENT, 0, 1);
Assert.assertNull(lexer.next());
}
......@@ -127,102 +128,133 @@ public class LexerTest {
@Test
public void testStartWithSuperscript() {
final Lexer lexer = new Lexer("³");
expect(lexer, "³", TokenType.INTEGER, null, null, 3);
expect(lexer, "³", TokenType.INTEGER, null, null, 3, 1);
Assert.assertNull(lexer.next());
}
@Test
public void testCharacters() {
final Lexer lexer = new Lexer("d*°*min*◦*deg*′*hh*'*ad*″*a*\"*µ''");
expect(lexer, "d", TokenType.PREFIXED_UNIT, null, Unit.DAY, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "°", TokenType.PREFIXED_UNIT, null, Unit.DEGREE, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "min", TokenType.PREFIXED_UNIT, null, Unit.MINUTE, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "◦", TokenType.PREFIXED_UNIT, null, Unit.DEGREE, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "deg", TokenType.PREFIXED_UNIT, null, Unit.DEGREE, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "′", TokenType.PREFIXED_UNIT, null, Unit.ARC_MINUTE, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "hh", TokenType.PREFIXED_UNIT, Prefix.HECTO, Unit.HOUR, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "'", TokenType.PREFIXED_UNIT, null, Unit.ARC_MINUTE, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "ad", TokenType.PREFIXED_UNIT, Prefix.ATTO, Unit.DAY, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "″", TokenType.PREFIXED_UNIT, null, Unit.ARC_SECOND, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "a", TokenType.PREFIXED_UNIT, null, Unit.YEAR, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "\"", TokenType.PREFIXED_UNIT, null, Unit.ARC_SECOND, 0);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0);
expect(lexer, "µ''", TokenType.PREFIXED_UNIT, Prefix.MICRO, Unit.ARC_SECOND, 0);
expect(lexer, "d", TokenType.PREFIXED_UNIT, null, Unit.DAY, 0, 1);
expect(lexer, "*", TokenType.MULTIPLICATION, null, null, 0, 1);
expect(lexer, "°", TokenType.PREFIXED_UNIT, null, Unit.DEGREE