aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/expr/Value.java10
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java129
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java3
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java23
4 files changed, 95 insertions, 70 deletions
diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/Value.java b/src/main/java/com/healthmarketscience/jackcess/expr/Value.java
index 04844ae..ad0e587 100644
--- a/src/main/java/com/healthmarketscience/jackcess/expr/Value.java
+++ b/src/main/java/com/healthmarketscience/jackcess/expr/Value.java
@@ -46,6 +46,16 @@ public interface Value
return((ordinal() <= DOUBLE.ordinal()) ? DOUBLE : BIG_DEC);
}
+ public Type getPreferredNumericType() {
+ if(isNumeric()) {
+ return this;
+ }
+ if(isTemporal()) {
+ return ((this == DATE) ? LONG : DOUBLE);
+ }
+ return null;
+ }
+
private boolean inRange(Type start, Type end) {
return ((start.ordinal() <= ordinal()) && (ordinal() <= end.ordinal()));
}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java
index 227b3c8..5a3d7b5 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java
@@ -52,6 +52,19 @@ public class BuiltinOperators
public static final Value FALSE_VAL = new LongValue(0L);
public static final Value EMPTY_STR_VAL = new StringValue("");
+ private enum CoercionType {
+ SIMPLE(true, true), GENERAL(false, true), COMPARE(false, false);
+
+ final boolean _preferTemporal;
+ final boolean _allowCoerceStringToNum;
+
+ private CoercionType(boolean preferTemporal,
+ boolean allowCoerceStringToNum) {
+ _preferTemporal = preferTemporal;
+ _allowCoerceStringToNum = allowCoerceStringToNum;
+ }
+ }
+
private BuiltinOperators() {}
// null propagation rules:
@@ -98,7 +111,8 @@ public class BuiltinOperators
return NULL_VAL;
}
- Value.Type mathType = getSimpleMathTypePrecedence(param1, param2);
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.SIMPLE);
switch(mathType) {
case STRING:
@@ -127,7 +141,8 @@ public class BuiltinOperators
return NULL_VAL;
}
- Value.Type mathType = getSimpleMathTypePrecedence(param1, param2);
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.SIMPLE);
switch(mathType) {
// case STRING: break; unsupported
@@ -154,9 +169,8 @@ public class BuiltinOperators
return NULL_VAL;
}
- // FIXME, string will convert to number if one is number and one is a string parseable as a number!!!
-
- Value.Type mathType = getGeneralMathTypePrecedence(param1, param2);
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.GENERAL);
switch(mathType) {
// case STRING: break; unsupported
@@ -180,7 +194,8 @@ public class BuiltinOperators
return NULL_VAL;
}
- Value.Type mathType = getGeneralMathTypePrecedence(param1, param2);
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.GENERAL);
switch(mathType) {
// case STRING: break; unsupported
@@ -214,7 +229,8 @@ public class BuiltinOperators
return NULL_VAL;
}
- Value.Type mathType = getGeneralMathTypePrecedence(param1, param2);
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.GENERAL);
boolean wasDouble = false;
switch(mathType) {
@@ -242,7 +258,8 @@ public class BuiltinOperators
return NULL_VAL;
}
- Value.Type mathType = getGeneralMathTypePrecedence(param1, param2);
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.GENERAL);
// jdk only supports general pow() as doubles, let's go with that
double result = Math.pow(param1.getAsDouble(), param2.getAsDouble());
@@ -262,7 +279,8 @@ public class BuiltinOperators
return NULL_VAL;
}
- Value.Type mathType = getGeneralMathTypePrecedence(param1, param2);
+ Value.Type mathType = getMathTypePrecedence(param1, param2,
+ CoercionType.GENERAL);
boolean wasDouble = false;
switch(mathType) {
@@ -544,7 +562,8 @@ public class BuiltinOperators
Value param1, Value param2)
{
// note that comparison does not do string to num coercion
- Value.Type compareType = getGeneralMathTypePrecedence(param1, param2, false);
+ Value.Type compareType = getMathTypePrecedence(param1, param2,
+ CoercionType.COMPARE);
switch(compareType) {
case STRING:
@@ -631,56 +650,8 @@ public class BuiltinOperators
}
}
- private static Value.Type getSimpleMathTypePrecedence(
- Value param1, Value param2)
- {
- Value.Type t1 = param1.getType();
- Value.Type t2 = param2.getType();
-
- if(t1 == t2) {
- return t1;
- }
-
- if((t1 == Value.Type.STRING) || (t2 == Value.Type.STRING)) {
-
- // see if this is mixed string/numeric and the string can be coerced to
- // a number
- Value.Type numericType = coerceStringToNumeric(param1, param2);
- if(numericType != null) {
- // string can be coerced to number
- return numericType;
- }
-
- // string always wins
- return Value.Type.STRING;
- }
-
- // for "simple" math, keep as date/times
- if(t1.isTemporal() || t2.isTemporal()) {
- return (t1.isTemporal() ?
- (t2.isTemporal() ?
- // for mixed temporal types, always go to date/time
- Value.Type.DATE_TIME : t1) :
- t2);
- }
-
- // if both types are integral, choose "largest"
- if(t1.isIntegral() && t2.isIntegral()) {
- return max(t1, t2);
- }
-
- // choose largest relevant floating-point type
- return max(t1.getPreferredFPType(), t2.getPreferredFPType());
- }
-
- private static Value.Type getGeneralMathTypePrecedence(
- Value param1, Value param2)
- {
- return getGeneralMathTypePrecedence(param1, param2, true);
- }
-
- private static Value.Type getGeneralMathTypePrecedence(
- Value param1, Value param2, boolean allowCoerceToNum)
+ private static Value.Type getMathTypePrecedence(
+ Value param1, Value param2, CoercionType cType)
{
Value.Type t1 = param1.getType();
Value.Type t2 = param2.getType();
@@ -689,8 +660,8 @@ public class BuiltinOperators
if(t1 == t2) {
- if(t1.isTemporal()) {
- return Value.Type.DOUBLE;
+ if(!cType._preferTemporal && t1.isTemporal()) {
+ return t1.getPreferredNumericType();
}
return t1;
@@ -698,10 +669,10 @@ public class BuiltinOperators
if((t1 == Value.Type.STRING) || (t2 == Value.Type.STRING)) {
- if(allowCoerceToNum) {
+ if(cType._allowCoerceStringToNum) {
// see if this is mixed string/numeric and the string can be coerced
// to a number
- Value.Type numericType = coerceStringToNumeric(param1, param2);
+ Value.Type numericType = coerceStringToNumeric(param1, param2, cType);
if(numericType != null) {
// string can be coerced to number
return numericType;
@@ -712,6 +683,19 @@ public class BuiltinOperators
return Value.Type.STRING;
}
+ // for "simple" math, keep as date/times
+ if(cType._preferTemporal &&
+ (t1.isTemporal() || t2.isTemporal())) {
+ return (t1.isTemporal() ?
+ (t2.isTemporal() ?
+ // for mixed temporal types, always go to date/time
+ Value.Type.DATE_TIME : t1) :
+ t2);
+ }
+
+ t1 = t1.getPreferredNumericType();
+ t2 = t2.getPreferredNumericType();
+
// if both types are integral, choose "largest"
if(t1.isIntegral() && t2.isIntegral()) {
return max(t1, t2);
@@ -721,17 +705,24 @@ public class BuiltinOperators
return max(t1.getPreferredFPType(), t2.getPreferredFPType());
}
- private static Value.Type coerceStringToNumeric(Value param1, Value param2) {
+ private static Value.Type coerceStringToNumeric(
+ Value param1, Value param2, CoercionType cType) {
Value.Type t1 = param1.getType();
Value.Type t2 = param2.getType();
- Value.Type numericType = null;
+ Value.Type prefType = null;
Value strParam = null;
if(t1.isNumeric()) {
- numericType = t1;
+ prefType = t1;
strParam = param2;
} else if(t2.isNumeric()) {
- numericType = t2;
+ prefType = t2;
+ strParam = param1;
+ } else if(t1.isTemporal()) {
+ prefType = (cType._preferTemporal ? t1 : t1.getPreferredNumericType());
+ strParam = param2;
+ } else if(t2.isTemporal()) {
+ prefType = (cType._preferTemporal ? t2 : t2.getPreferredNumericType());
strParam = param1;
} else {
// no numeric type involved
@@ -741,7 +732,7 @@ public class BuiltinOperators
try {
// see if string can be coerced to a number
strParam.getAsBigDecimal();
- return numericType;
+ return prefType;
} catch(NumberFormatException ignored) {
// not a number
}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java
index d078d44..596b3f0 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java
@@ -350,7 +350,8 @@ class ExpressionTokenizer
}
try {
- return new Token(TokenType.LITERAL, sdf.parse(dateStr), dateStr, valType);
+ return new Token(TokenType.LITERAL, sdf.parse(dateStr), dateStr, valType,
+ sdf);
} catch(ParseException pe) {
throw new IllegalArgumentException(
"Invalid date time literal " + dateStr + " " + buf, pe);
diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java
index b284c25..b760b16 100644
--- a/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java
+++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java
@@ -17,6 +17,7 @@ limitations under the License.
package com.healthmarketscience.jackcess.impl.expr;
import java.text.SimpleDateFormat;
+import java.util.Date;
import com.healthmarketscience.jackcess.DatabaseBuilder;
import com.healthmarketscience.jackcess.TestUtil;
@@ -252,6 +253,28 @@ public class ExpressionatorTest extends TestCase
}
+ public void testTypeCoercion() throws Exception
+ {
+ assertEquals("foobar", eval("=\"foo\" + \"bar\""));
+
+ assertEquals("12foo", eval("=12 + \"foo\""));
+ assertEquals("foo12", eval("=\"foo\" + 12"));
+
+ assertEquals(37L, eval("=\"25\" + 12"));
+ assertEquals(37L, eval("=12 + \"25\""));
+
+ evalFail(("=12 - \"foo\""), RuntimeException.class);
+ evalFail(("=\"foo\" - 12"), RuntimeException.class);
+
+ assertEquals("foo1225", eval("=\"foo\" + 12 + 25"));
+ assertEquals("37foo", eval("=12 + 25 + \"foo\""));
+ assertEquals("foo37", eval("=\"foo\" + (12 + 25)"));
+ assertEquals("25foo12", eval("=\"25foo\" + 12"));
+
+ assertEquals(new Date(1485579600000L), eval("=#1/1/2017# + 27"));
+ assertEquals(128208L, eval("=#1/1/2017# * 3"));
+ }
+
private static void validateExpr(String exprStr, String debugStr) {
validateExpr(exprStr, debugStr, exprStr);
}