New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Coerce decimal strings for whole number types by truncating the decimal part #25835
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -312,13 +312,7 @@ public List<Field> createFields(String name, Number value, | |
DOUBLE("double", NumericType.DOUBLE) { | ||
@Override | ||
Double parse(Object value, boolean coerce) { | ||
if (value instanceof Number) { | ||
return ((Number) value).doubleValue(); | ||
} | ||
if (value instanceof BytesRef) { | ||
value = ((BytesRef) value).utf8ToString(); | ||
} | ||
return Double.parseDouble(value.toString()); | ||
return objectToDouble(value); | ||
} | ||
|
||
@Override | ||
|
@@ -389,20 +383,20 @@ public List<Field> createFields(String name, Number value, | |
BYTE("byte", NumericType.BYTE) { | ||
@Override | ||
Byte parse(Object value, boolean coerce) { | ||
double doubleValue = objectToDouble(value); | ||
|
||
if (doubleValue < Byte.MIN_VALUE || doubleValue > Byte.MAX_VALUE) { | ||
throw new IllegalArgumentException("Value [" + value + "] is out of range for a byte"); | ||
} | ||
if (!coerce && doubleValue % 1 != 0) { | ||
throw new IllegalArgumentException("Value [" + value + "] has a decimal part"); | ||
} | ||
|
||
if (value instanceof Number) { | ||
double doubleValue = ((Number) value).doubleValue(); | ||
if (doubleValue < Byte.MIN_VALUE || doubleValue > Byte.MAX_VALUE) { | ||
throw new IllegalArgumentException("Value [" + value + "] is out of range for a byte"); | ||
} | ||
if (!coerce && doubleValue % 1 != 0) { | ||
throw new IllegalArgumentException("Value [" + value + "] has a decimal part"); | ||
} | ||
return ((Number) value).byteValue(); | ||
} | ||
if (value instanceof BytesRef) { | ||
value = ((BytesRef) value).utf8ToString(); | ||
} | ||
return Byte.parseByte(value.toString()); | ||
|
||
return (byte) doubleValue; | ||
} | ||
|
||
@Override | ||
|
@@ -445,29 +439,25 @@ Number valueForSearch(Number value) { | |
SHORT("short", NumericType.SHORT) { | ||
@Override | ||
Short parse(Object value, boolean coerce) { | ||
double doubleValue = objectToDouble(value); | ||
|
||
if (doubleValue < Short.MIN_VALUE || doubleValue > Short.MAX_VALUE) { | ||
throw new IllegalArgumentException("Value [" + value + "] is out of range for a short"); | ||
} | ||
if (!coerce && doubleValue % 1 != 0) { | ||
throw new IllegalArgumentException("Value [" + value + "] has a decimal part"); | ||
} | ||
|
||
if (value instanceof Number) { | ||
double doubleValue = ((Number) value).doubleValue(); | ||
if (doubleValue < Short.MIN_VALUE || doubleValue > Short.MAX_VALUE) { | ||
throw new IllegalArgumentException("Value [" + value + "] is out of range for a short"); | ||
} | ||
if (!coerce && doubleValue % 1 != 0) { | ||
throw new IllegalArgumentException("Value [" + value + "] has a decimal part"); | ||
} | ||
return ((Number) value).shortValue(); | ||
} | ||
if (value instanceof BytesRef) { | ||
value = ((BytesRef) value).utf8ToString(); | ||
} | ||
return Short.parseShort(value.toString()); | ||
|
||
return (short) doubleValue; | ||
} | ||
|
||
@Override | ||
Short parse(XContentParser parser, boolean coerce) throws IOException { | ||
int value = parser.intValue(coerce); | ||
if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { | ||
throw new IllegalArgumentException("Value [" + value + "] is out of range for a short"); | ||
} | ||
return (short) value; | ||
return parser.shortValue(coerce); | ||
} | ||
|
||
@Override | ||
|
@@ -501,20 +491,20 @@ Number valueForSearch(Number value) { | |
INTEGER("integer", NumericType.INT) { | ||
@Override | ||
Integer parse(Object value, boolean coerce) { | ||
double doubleValue = objectToDouble(value); | ||
|
||
if (doubleValue < Integer.MIN_VALUE || doubleValue > Integer.MAX_VALUE) { | ||
throw new IllegalArgumentException("Value [" + value + "] is out of range for an integer"); | ||
} | ||
if (!coerce && doubleValue % 1 != 0) { | ||
throw new IllegalArgumentException("Value [" + value + "] has a decimal part"); | ||
} | ||
|
||
if (value instanceof Number) { | ||
double doubleValue = ((Number) value).doubleValue(); | ||
if (doubleValue < Integer.MIN_VALUE || doubleValue > Integer.MAX_VALUE) { | ||
throw new IllegalArgumentException("Value [" + value + "] is out of range for an integer"); | ||
} | ||
if (!coerce && doubleValue % 1 != 0) { | ||
throw new IllegalArgumentException("Value [" + value + "] has a decimal part"); | ||
} | ||
return ((Number) value).intValue(); | ||
} | ||
if (value instanceof BytesRef) { | ||
value = ((BytesRef) value).utf8ToString(); | ||
} | ||
return Integer.parseInt(value.toString()); | ||
|
||
return (int) doubleValue; | ||
} | ||
|
||
@Override | ||
|
@@ -612,20 +602,27 @@ public List<Field> createFields(String name, Number value, | |
LONG("long", NumericType.LONG) { | ||
@Override | ||
Long parse(Object value, boolean coerce) { | ||
double doubleValue = objectToDouble(value); | ||
|
||
if (doubleValue < Long.MIN_VALUE || doubleValue > Long.MAX_VALUE) { | ||
throw new IllegalArgumentException("Value [" + value + "] is out of range for a long"); | ||
} | ||
if (!coerce && doubleValue % 1 != 0) { | ||
throw new IllegalArgumentException("Value [" + value + "] has a decimal part"); | ||
} | ||
|
||
if (value instanceof Number) { | ||
double doubleValue = ((Number) value).doubleValue(); | ||
if (doubleValue < Long.MIN_VALUE || doubleValue > Long.MAX_VALUE) { | ||
throw new IllegalArgumentException("Value [" + value + "] is out of range for a long"); | ||
} | ||
if (!coerce && doubleValue % 1 != 0) { | ||
throw new IllegalArgumentException("Value [" + value + "] has a decimal part"); | ||
} | ||
return ((Number) value).longValue(); | ||
} | ||
if (value instanceof BytesRef) { | ||
value = ((BytesRef) value).utf8ToString(); | ||
|
||
// longs need special handling so we don't lose precision while parsing | ||
String stringValue = (value instanceof BytesRef) ? ((BytesRef) value).utf8ToString() : value.toString(); | ||
|
||
try { | ||
return Long.parseLong(stringValue); | ||
} catch (NumberFormatException e) { | ||
return (long) Double.parseDouble(stringValue); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does not preserve the precision of large numbers that have a decimal part and exceed the fractional bits of double but are within min/max Long. For example, while both Do we want to try to do something a bit more clever to handle this edge case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'd just put a comment that we might not fail in all cases, but I don't think I would try to address it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean a comment in the code or in the documentation? (i.e. a warning here https://www.elastic.co/guide/en/elasticsearch/reference/current/coerce.html) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean in the code but just noticed there was one already |
||
} | ||
return Long.parseLong(value.toString()); | ||
} | ||
|
||
@Override | ||
|
@@ -781,6 +778,23 @@ boolean hasDecimalPart(Object number) { | |
return Math.signum(Double.parseDouble(value.toString())); | ||
} | ||
|
||
/** | ||
* Converts an Object to a double by checking it against known types first | ||
*/ | ||
private static double objectToDouble(Object value) { | ||
double doubleValue; | ||
|
||
if (value instanceof Number) { | ||
doubleValue = ((Number) value).doubleValue(); | ||
} else if (value instanceof BytesRef) { | ||
doubleValue = Double.parseDouble(((BytesRef) value).utf8ToString()); | ||
} else { | ||
doubleValue = Double.parseDouble(value.toString()); | ||
} | ||
|
||
return doubleValue; | ||
} | ||
|
||
} | ||
|
||
public static final class NumberFieldType extends MappedFieldType { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I deleted your comment by mistake, so I am adding it back:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed it should be changed to be consistent, but let's do it in a separate PR? Thinking more about it, I'm wondering whether we should remove the
coerce
option instead. I opened #25861.