Skip to content

Commit e916e4d

Browse files
authored
Improve IntConverter performance (#559)
* Replace `Long.toString()` by call to `NumbersCache`. * Improved performance of integer parsing ... for integers shorter than Integer.MAX_VALUE Heuristic: since we have no range check in our parseInt() we only parse values which have at least one digit less than Integer.MAX_VALUE and leave longer Strings to Integer.parseInt(). NB: we must not simply reject strings longer than MAX_VALUE since they could possibly include an arbitrary number of leading zeros. * Added check for negative numbers to NumbersCache.get() * Check explicitly for integer between 0 and 9. As per Jussi Virtanen's review comment. #559 (comment)
1 parent 1d7d239 commit e916e4d

File tree

4 files changed

+105
-18
lines changed

4 files changed

+105
-18
lines changed

quickfixj-core/src/main/java/quickfix/NumbersCache.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,21 @@ public final class NumbersCache {
3232

3333
static {
3434
LITTLE_NUMBERS = new ArrayList<>(LITTLE_NUMBERS_LENGTH);
35-
for (int i = 0; i < LITTLE_NUMBERS_LENGTH; i++)
35+
for (int i = 0; i < LITTLE_NUMBERS_LENGTH; i++) {
3636
LITTLE_NUMBERS.add(Integer.toString(i));
37+
}
3738
}
3839

3940
/**
4041
* Get the String representing the given number
4142
*
42-
* @param i the long to convert
43-
* @return the String representing the long
43+
* @param i the int to convert
44+
* @return the String representing the integer
4445
*/
4546
public static String get(int i) {
46-
if (i < LITTLE_NUMBERS_LENGTH)
47+
if (i < LITTLE_NUMBERS_LENGTH && i >= 0) {
4748
return LITTLE_NUMBERS.get(i);
49+
}
4850
return String.valueOf(i);
4951
}
5052

@@ -56,10 +58,11 @@ public static String get(int i) {
5658
* @return the String representing the double or null if the double is not an integer
5759
*/
5860
public static String get(double d) {
59-
long l = (long)d;
60-
if (d == (double)l)
61+
long l = (long) d;
62+
if (d == (double) l) {
6163
return get(l);
64+
}
6265
return null;
63-
}
66+
}
6467

6568
}

quickfixj-core/src/main/java/quickfix/field/converter/AbstractDateTimeConverter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ protected static void assertLength(String value, String type, int... lengths) th
4747
protected static void assertDigitSequence(String value, int i, int j, String type)
4848
throws FieldConvertError {
4949
for (int offset = i; offset < j; offset++) {
50-
if (!Character.isDigit(value.charAt(offset))) {
50+
if (!IntConverter.isDigit(value.charAt(offset))) {
5151
throwFieldConvertError(value, type);
5252
}
5353
}

quickfixj-core/src/main/java/quickfix/field/converter/IntConverter.java

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,24 @@
2020
package quickfix.field.converter;
2121

2222
import quickfix.FieldConvertError;
23+
import quickfix.NumbersCache;
2324

2425
/**
2526
* Convert between an integer and a String
2627
*/
2728
public final class IntConverter {
2829

30+
private static final String INT_MAX_STRING = String.valueOf(Integer.MAX_VALUE);
31+
2932
/**
3033
* Convert an integer to a String
3134
*
3235
* @param i the integer to convert
3336
* @return the String representing the integer
34-
* @see java.lang.Long#toString(long)
37+
* @see NumbersCache#get(int)
3538
*/
3639
public static String convert(int i) {
37-
return Long.toString(i);
40+
return NumbersCache.get(i);
3841
}
3942

4043
/**
@@ -43,22 +46,68 @@ public static String convert(int i) {
4346
* @param value the String to convert
4447
* @return the converted integer
4548
* @throws FieldConvertError raised if the String does not represent a valid
46-
* integer
49+
* FIX integer, i.e. optional negative sign and rest are digits.
4750
* @see java.lang.Integer#parseInt(String)
4851
*/
4952
public static int convert(String value) throws FieldConvertError {
50-
try {
51-
for (int i = 0; i < value.length(); i++) {
52-
if (!Character.isDigit(value.charAt(i)) && !(i == 0 && value.charAt(i) == '-')) {
53-
throw new FieldConvertError("invalid integral value: " + value);
53+
54+
if (!value.isEmpty()) {
55+
final char firstChar = value.charAt(0);
56+
boolean isNegative = (firstChar == '-');
57+
if (!isDigit(firstChar) && !isNegative) {
58+
throw new FieldConvertError("invalid integral value: " + value);
59+
}
60+
int minLength = (isNegative ? 2 : 1);
61+
if (value.length() < minLength) {
62+
throw new FieldConvertError("invalid integral value: " + value);
63+
}
64+
65+
// Heuristic: since we have no range check in our parseInt() we only parse
66+
// values which have at least one digit less than Integer.MAX_VALUE and
67+
// leave longer Strings to Integer.parseInt().
68+
// NB: we must not simply reject strings longer than MAX_VALUE since
69+
// they could possibly include an arbitrary number of leading zeros.
70+
int maxLength = (isNegative ? INT_MAX_STRING.length() : INT_MAX_STRING.length() - 1);
71+
if (value.length() <= maxLength) {
72+
return parseInt(value, isNegative);
73+
} else {
74+
try {
75+
return Integer.parseInt(value);
76+
} catch (NumberFormatException e) {
77+
throw new FieldConvertError("invalid integral value: " + value + ": " + e);
5478
}
5579
}
56-
return Integer.parseInt(value);
57-
} catch (NumberFormatException e) {
58-
throw new FieldConvertError("invalid integral value: " + value + ": " + e);
80+
} else {
81+
throw new FieldConvertError("invalid integral value: empty string");
5982
}
6083
}
6184

85+
/**
86+
* Please note that input needs to be validated first, otherwise unexpected
87+
* results may occur. Please also note that this method has no range or
88+
* overflow check, so please only use it when you are sure that no overflow
89+
* might occur (e.g. for parsing seconds or smaller integers).
90+
*
91+
* This method does however check if the contained characters are digits.
92+
*
93+
* @param value the String to convert
94+
* @param isNegative if passed String is negative, first character will
95+
* be skipped since it is assumed that it contains the negative sign
96+
* @return the converted int
97+
*/
98+
private static int parseInt(String value, boolean isNegative) throws FieldConvertError {
99+
int num = 0;
100+
int firstIndex = (isNegative ? 1 : 0);
101+
for (int i = firstIndex; i < value.length(); i++) {
102+
if (isDigit(value.charAt(i))) {
103+
num = (num * 10) + (value.charAt(i) - '0');
104+
} else {
105+
throw new FieldConvertError("invalid integral value: " + value);
106+
}
107+
}
108+
return isNegative ? -num : num;
109+
}
110+
62111
/**
63112
* Please note that input needs to be validated first, otherwise unexpected
64113
* results may occur. Please also note that this method has no range or overflow
@@ -106,4 +155,15 @@ static long parseLong(String value) {
106155
}
107156
return negative ? -num : num;
108157
}
158+
159+
/**
160+
* Check if a character is a digit, i.e. in the range between 0 and 9.
161+
*
162+
* @param character character to check
163+
* @return true if character is a digit between 0 and 9
164+
*/
165+
static boolean isDigit(char character) {
166+
return (character >= '0' && character <= '9');
167+
}
168+
109169
}

quickfixj-core/src/test/java/quickfix/FieldConvertersTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ public class FieldConvertersTest {
5555
public void testIntegerConversion() throws Exception {
5656
String intMaxValuePlus3 = "2147483650";
5757
String intMinValueMinus3 = "-2147483651";
58+
String intLargeValue = "999999999";
59+
String intLargeValue2 = "2000000000";
60+
assertEquals(String.valueOf(Integer.MAX_VALUE), IntConverter.convert(Integer.MAX_VALUE));
61+
assertEquals(String.valueOf(Integer.MIN_VALUE), IntConverter.convert(Integer.MIN_VALUE));
62+
assertEquals(999999999, IntConverter.convert(intLargeValue));
63+
assertEquals(2000000000, IntConverter.convert(intLargeValue2));
5864
assertEquals("123", IntConverter.convert(123));
5965
assertEquals(123, IntConverter.convert("123"));
6066
assertEquals(-1, IntConverter.convert("-1"));
@@ -91,6 +97,24 @@ public void testIntegerConversion() throws Exception {
9197
} catch (FieldConvertError e) {
9298
// expected
9399
}
100+
try {
101+
IntConverter.convert("");
102+
fail();
103+
} catch (FieldConvertError e) {
104+
// expected
105+
}
106+
try {
107+
IntConverter.convert("-");
108+
fail();
109+
} catch (FieldConvertError e) {
110+
// expected
111+
}
112+
try {
113+
IntConverter.convert("+");
114+
fail();
115+
} catch (FieldConvertError e) {
116+
// expected
117+
}
94118
}
95119

96120
@Test

0 commit comments

Comments
 (0)