From: James Ahlborn Date: Fri, 9 Dec 2016 14:48:33 +0000 (+0000) Subject: checkpoint reworking expression classes and implementing many basic operations X-Git-Tag: jackcess-2.2.0~24^2~60 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=8cab4c878d609244e30e6961c08493a8f896f436;p=jackcess.git checkpoint reworking expression classes and implementing many basic operations git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/exprs@1067 f203690c-595d-4dc9-a70b-905162fa7fd2 --- diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/Value.java b/src/main/java/com/healthmarketscience/jackcess/expr/Value.java index 5c1d13f..c9a2a22 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/Value.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/Value.java @@ -16,6 +16,10 @@ limitations under the License. package com.healthmarketscience.jackcess.expr; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + /** * * @author James Ahlborn @@ -24,16 +28,26 @@ public interface Value { public enum Type { + // FIXME, ditch boolean type -> -1,0 NULL, BOOLEAN, STRING, DATE, TIME, DATE_TIME, LONG, DOUBLE, BIG_INT, BIG_DEC; public boolean isNumeric() { return inRange(LONG, BIG_DEC); } + public boolean isIntegral() { + // note when BOOLEAN is converted to number, it is integral + return ((this == LONG) || (this == BIG_INT) || (this == BOOLEAN)); + } + public boolean isTemporal() { return inRange(DATE, DATE_TIME); } + public Type getPreferredFPType() { + return((ordinal() <= DOUBLE.ordinal()) ? DOUBLE : BIG_DEC); + } + private boolean inRange(Type start, Type end) { return ((start.ordinal() <= ordinal()) && (ordinal() <= end.ordinal())); } @@ -41,5 +55,22 @@ public interface Value public Type getType(); + public Object get(); + + public Boolean getAsBoolean(); + + public String getAsString(); + + public Date getAsDateTime(); + + public Long getAsLong(); + + public Double getAsDouble(); + + public BigInteger getAsBigInteger(); + + public BigDecimal getAsBigDecimal(); + + public Value toNumericValue(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index d61bab8..203ad82 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -795,9 +795,27 @@ public class ColumnImpl implements Column, Comparable { * @usage _advanced_method_ */ public long fromDateDouble(double value) + { + return fromDateDouble(value, getCalendar()); + } + + /** + * Returns a java long time value converted from an access date double. + * @usage _advanced_method_ + */ + public static long fromDateDouble(double value, DatabaseImpl db) + { + return fromDateDouble(value, db.getCalendar()); + } + + /** + * Returns a java long time value converted from an access date double. + * @usage _advanced_method_ + */ + public static long fromDateDouble(double value, Calendar c) { long localTime = fromLocalDateDouble(value); - return localTime - getFromLocalTimeZoneOffset(localTime); + return localTime - getFromLocalTimeZoneOffset(localTime, c); } static long fromLocalDateDouble(double value) @@ -842,11 +860,31 @@ public class ColumnImpl implements Column, Comparable { * @usage _advanced_method_ */ public double toDateDouble(Object value) + { + return toDateDouble(value, getCalendar()); + } + + /** + * Returns an access date double converted from a java Date/Calendar/Number + * time value. + * @usage _advanced_method_ + */ + public static double toDateDouble(Object value, DatabaseImpl db) + { + return toDateDouble(value, db.getCalendar()); + } + + /** + * Returns an access date double converted from a java Date/Calendar/Number + * time value. + * @usage _advanced_method_ + */ + public static double toDateDouble(Object value, Calendar c) { // seems access stores dates in the local timezone. guess you just // hope you read it in the same timezone in which it was written! long time = toDateLong(value); - time += getToLocalTimeZoneOffset(time); + time += getToLocalTimeZoneOffset(time, c); return toLocalDateDouble(time); } @@ -881,9 +919,8 @@ public class ColumnImpl implements Column, Comparable { * Gets the timezone offset from UTC to local time for the given time * (including DST). */ - private long getToLocalTimeZoneOffset(long time) + private static long getToLocalTimeZoneOffset(long time, Calendar c) { - Calendar c = getCalendar(); c.setTimeInMillis(time); return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); } @@ -892,11 +929,10 @@ public class ColumnImpl implements Column, Comparable { * Gets the timezone offset from local time to UTC for the given time * (including DST). */ - private long getFromLocalTimeZoneOffset(long time) + private static long getFromLocalTimeZoneOffset(long time, Calendar c) { // getting from local time back to UTC is a little wonky (and not // guaranteed to get you back to where you started) - Calendar c = getCalendar(); c.setTimeInMillis(time); // apply the zone offset first to get us closer to the original time c.setTimeInMillis(time - c.get(Calendar.ZONE_OFFSET)); @@ -1414,7 +1450,17 @@ public class ColumnImpl implements Column, Comparable { * null is returned as 0 and Numbers are converted * using their double representation. */ - static BigDecimal toBigDecimal(Object value) + BigDecimal toBigDecimal(Object value) + { + return toBigDecimal(value, getDatabase()); + } + + /** + * @return an appropriate BigDecimal representation of the given object. + * null is returned as 0 and Numbers are converted + * using their double representation. + */ + static BigDecimal toBigDecimal(Object value, DatabaseImpl db) { if(value == null) { return BigDecimal.ZERO; @@ -1424,6 +1470,11 @@ public class ColumnImpl implements Column, Comparable { return new BigDecimal((BigInteger)value); } else if(value instanceof Number) { return new BigDecimal(((Number)value).doubleValue()); + } else if(value instanceof Boolean) { + // access seems to like -1 for true and 0 for false + return ((Boolean)value) ? BigDecimal.valueOf(-1) : BigDecimal.ZERO; + } else if(value instanceof Date) { + return new BigDecimal(toDateDouble(value, db)); } return new BigDecimal(value.toString()); } @@ -1433,12 +1484,27 @@ public class ColumnImpl implements Column, Comparable { * null is returned as 0 and Strings are parsed as * Doubles. */ - private static Number toNumber(Object value) + private Number toNumber(Object value) + { + return toNumber(value, getDatabase()); + } + + /** + * @return an appropriate Number representation of the given object. + * null is returned as 0 and Strings are parsed as + * Doubles. + */ + private static Number toNumber(Object value, DatabaseImpl db) { if(value == null) { return BigDecimal.ZERO; } else if(value instanceof Number) { return (Number)value; + } else if(value instanceof Boolean) { + // access seems to like -1 for true and 0 for false + return ((Boolean)value) ? -1 : 0; + } else if(value instanceof Date) { + return toDateDouble(value, db); } return Double.valueOf(value.toString()); } @@ -1532,7 +1598,7 @@ public class ColumnImpl implements Column, Comparable { if(obj instanceof BigInteger) { return (((BigInteger)obj).compareTo(BigInteger.ZERO) != 0); } - return (((Number)obj).doubleValue() != 0.0); + return (((Number)obj).doubleValue() != 0.0d); } return Boolean.parseBoolean(obj.toString()); } @@ -1549,11 +1615,11 @@ public class ColumnImpl implements Column, Comparable { } /** - * Treat booleans as integers (C-style). + * Treat booleans as integers (access-style). */ protected static Object booleanToInteger(Object obj) { if (obj instanceof Boolean) { - obj = ((Boolean) obj) ? 1 : 0; + obj = ((Boolean) obj) ? -1 : 0; } return obj; } @@ -1770,7 +1836,8 @@ public class ColumnImpl implements Column, Comparable { * Converts the given value to the "internal" representation for the given * data type. */ - public static Object toInternalValue(DataType dataType, Object value) + public static Object toInternalValue(DataType dataType, Object value, + DatabaseImpl db) throws IOException { if(value == null) { @@ -1781,21 +1848,21 @@ public class ColumnImpl implements Column, Comparable { case BOOLEAN: return ((value instanceof Boolean) ? value : toBooleanValue(value)); case BYTE: - return ((value instanceof Byte) ? value : toNumber(value).byteValue()); + return ((value instanceof Byte) ? value : toNumber(value, db).byteValue()); case INT: return ((value instanceof Short) ? value : - toNumber(value).shortValue()); + toNumber(value, db).shortValue()); case LONG: return ((value instanceof Integer) ? value : - toNumber(value).intValue()); + toNumber(value, db).intValue()); case MONEY: - return toBigDecimal(value); + return toBigDecimal(value, db); case FLOAT: return ((value instanceof Float) ? value : - toNumber(value).floatValue()); + toNumber(value, db).floatValue()); case DOUBLE: return ((value instanceof Double) ? value : - toNumber(value).doubleValue()); + toNumber(value, db).doubleValue()); case SHORT_DATE_TIME: return ((value instanceof DateExt) ? value : new Date(toDateLong(value))); @@ -1805,7 +1872,7 @@ public class ColumnImpl implements Column, Comparable { return ((value instanceof String) ? value : toCharSequence(value).toString()); case NUMERIC: - return toBigDecimal(value); + return toBigDecimal(value, db); case COMPLEX_TYPE: // leave alone for now? return value; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDateValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDateValue.java new file mode 100644 index 0000000..8a3ff8f --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDateValue.java @@ -0,0 +1,85 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl.expr; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.DateFormat; +import java.util.Date; + +import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.ColumnImpl; + +/** + * + * @author James Ahlborn + */ +public abstract class BaseDateValue extends BaseValue +{ + private final Date _val; + private final DateFormat _fmt; + + public BaseDateValue(Date val, DateFormat fmt) + { + _val = val; + _fmt = fmt; + } + + public Object get() { + return _val; + } + + protected Double getNumber() { + return ColumnImpl.toDateDouble(_val, _fmt.getCalendar()); + } + + @Override + public Boolean getAsBoolean() { + // ms access seems to treat dates/times as "true" + return Boolean.TRUE; + } + + @Override + public String getAsString() { + return _fmt.format(_val); + } + + @Override + public Long getAsLong() { + return getNumber().longValue(); + } + + @Override + public Double getAsDouble() { + return getNumber(); + } + + @Override + public BigInteger getAsBigInteger() { + return getAsBigDecimal().toBigInteger(); + } + + @Override + public BigDecimal getAsBigDecimal() { + return BigDecimal.valueOf(getNumber()); + } + + @Override + public Value toNumericValue() { + return new DoubleValue(getNumber()); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java new file mode 100644 index 0000000..8849390 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java @@ -0,0 +1,70 @@ +// Copyright (c) 2016 Dell Boomi, Inc. + +package com.healthmarketscience.jackcess.impl.expr; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + +import com.healthmarketscience.jackcess.expr.Value; + +/** + * + * @author James Ahlborn + */ +public abstract class BaseDelayedValue implements Value +{ + private Value _val; + + protected BaseDelayedValue() { + } + + private Value getDelegate() { + if(_val == null) { + _val = eval(); + } + return _val; + } + + public Value.Type getType() { + return getDelegate().getType(); + } + + public Object get() { + return getDelegate().get(); + } + + public Boolean getAsBoolean() { + return getDelegate().getAsBoolean(); + } + + public String getAsString() { + return getDelegate().getAsString(); + } + + public Date getAsDateTime() { + return getDelegate().getAsDateTime(); + } + + public Long getAsLong() { + return getDelegate().getAsLong(); + } + + public Double getAsDouble() { + return getDelegate().getAsDouble(); + } + + public BigInteger getAsBigInteger() { + return getDelegate().getAsBigInteger(); + } + + public BigDecimal getAsBigDecimal() { + return getDelegate().getAsBigDecimal(); + } + + public Value toNumericValue() { + return getDelegate().toNumericValue(); + } + + protected abstract Value eval(); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java new file mode 100644 index 0000000..ab6b24b --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java @@ -0,0 +1,39 @@ +// Copyright (c) 2016 Dell Boomi, Inc. + +package com.healthmarketscience.jackcess.impl.expr; + +import com.healthmarketscience.jackcess.expr.Value; + +/** + * + * @author James Ahlborn + */ +public abstract class BaseNumericValue extends BaseValue +{ + + protected BaseNumericValue() + { + } + + @Override + public String getAsString() { + return getNumber().toString(); + } + + @Override + public Long getAsLong() { + return getNumber().longValue(); + } + + @Override + public Double getAsDouble() { + return getNumber().doubleValue(); + } + + @Override + public Value toNumericValue() { + return this; + } + + protected abstract Number getNumber(); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java new file mode 100644 index 0000000..d75e671 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java @@ -0,0 +1,72 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl.expr; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; + +import com.healthmarketscience.jackcess.expr.Value; + +/** + * + * @author James Ahlborn + */ +public abstract class BaseValue implements Value +{ + public Boolean getAsBoolean() { + throw invalidConversion(Value.Type.BOOLEAN); + } + + public String getAsString() { + throw invalidConversion(Value.Type.STRING); + } + + public Date getAsDateTime() { + throw invalidConversion(Value.Type.DATE_TIME); + } + + public Long getAsLong() { + throw invalidConversion(Value.Type.LONG); + } + + public Double getAsDouble() { + throw invalidConversion(Value.Type.DOUBLE); + } + + public BigInteger getAsBigInteger() { + throw invalidConversion(Value.Type.BIG_INT); + } + + public BigDecimal getAsBigDecimal() { + throw invalidConversion(Value.Type.BIG_DEC); + } + + public Value toNumericValue() { + throw invalidConversion(Value.Type.LONG); + } + + private UnsupportedOperationException invalidConversion(Value.Type newType) { + return new UnsupportedOperationException( + getType() + " value cannot be converted to " + newType); + } + + @Override + public String toString() { + return "Value[" + getType() + "] '" + get() + "'"; + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java new file mode 100644 index 0000000..386eb79 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java @@ -0,0 +1,67 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl.expr; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * + * @author James Ahlborn + */ +public class BigDecimalValue extends BaseNumericValue +{ + private final BigDecimal _val; + + public BigDecimalValue(BigDecimal val) + { + _val = val; + } + + public Type getType() { + return Type.BIG_DEC; + } + + public Object get() { + return _val; + } + + @Override + protected Number getNumber() { + return _val; + } + + @Override + public Boolean getAsBoolean() { + return (_val.compareTo(BigDecimal.ZERO) != 0L); + } + + @Override + public String getAsString() { + return _val.toPlainString(); + } + + @Override + public BigInteger getAsBigInteger() { + return _val.toBigInteger(); + } + + @Override + public BigDecimal getAsBigDecimal() { + return _val; + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigIntegerValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigIntegerValue.java new file mode 100644 index 0000000..9aa787f --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BigIntegerValue.java @@ -0,0 +1,62 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl.expr; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * + * @author James Ahlborn + */ +public class BigIntegerValue extends BaseNumericValue +{ + private final BigInteger _val; + + public BigIntegerValue(BigInteger val) + { + _val = val; + } + + public Type getType() { + return Type.BIG_INT; + } + + public Object get() { + return _val; + } + + @Override + protected Number getNumber() { + return _val; + } + + @Override + public Boolean getAsBoolean() { + return (_val.compareTo(BigInteger.ZERO) != 0L); + } + + @Override + public BigInteger getAsBigInteger() { + return _val; + } + + @Override + public BigDecimal getAsBigDecimal() { + return new BigDecimal(_val); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BooleanValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BooleanValue.java new file mode 100644 index 0000000..cc31617 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BooleanValue.java @@ -0,0 +1,89 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl.expr; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import com.healthmarketscience.jackcess.expr.Value; + +/** + * + * @author James Ahlborn + */ +public class BooleanValue extends BaseValue +{ + private final Boolean _val; + + public BooleanValue(Boolean val) + { + _val = val; + } + + public Type getType() { + return Type.BOOLEAN; + } + + public Object get() { + return _val; + } + + @Override + public Boolean getAsBoolean() { + return _val; + } + + @Override + public String getAsString() { + // access seems to like -1 for true and 0 for false + return (_val ? "-1" : "0"); + } + + @Override + public Long getAsLong() { + // access seems to like -1 for true and 0 for false + return numericBoolean(_val); + } + + @Override + public Double getAsDouble() { + // access seems to like -1 for true and 0 for false + return (_val ? -1d : 0d); + } + + @Override + public BigInteger getAsBigInteger() { + // access seems to like -1 for true and 0 for false + return (_val ? BigInteger.valueOf(-1) : BigInteger.ZERO); + } + + @Override + public BigDecimal getAsBigDecimal() { + // access seems to like -1 for true and 0 for false + return (_val ? BigDecimal.valueOf(-1) : BigDecimal.ZERO); + } + + @Override + public Value toNumericValue() { + return new LongValue(getAsLong()); + } + + protected static long numericBoolean(Boolean b) { + // access seems to like -1 for true and 0 for false + return (b ? -1L : 0L); + } +} 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 aa58b97..2ef3c23 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java @@ -16,19 +16,15 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; -import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.text.DateFormat; +import java.text.Format; import java.util.Date; -import java.util.Map; import java.util.regex.Pattern; -import com.healthmarketscience.jackcess.RuntimeIOException; -import com.healthmarketscience.jackcess.impl.ColumnImpl; -import com.healthmarketscience.jackcess.expr.Expression; import com.healthmarketscience.jackcess.expr.Value; -import com.healthmarketscience.jackcess.expr.Function; -import com.healthmarketscience.jackcess.expr.RowContext; +import com.healthmarketscience.jackcess.impl.ColumnImpl; /** @@ -38,31 +34,17 @@ import com.healthmarketscience.jackcess.expr.RowContext; public class BuiltinOperators { - public static final Value NULL_VAL = - new SimpleValue(Value.Type.NULL, null); - public static final Value TRUE_VAL = - new SimpleValue(Value.Type.BOOLEAN, Boolean.TRUE); - public static final Value FALSE_VAL = - new SimpleValue(Value.Type.BOOLEAN, Boolean.FALSE); - - public static class SimpleValue implements Value - { - private final Value.Type _type; - private final Object _val; - - public SimpleValue(Value.Type type, Object val) { - _type = type; - _val = val; - } - - public Value.Type getType() { - return _type; + public static final Value NULL_VAL = new BaseValue() { + public Type getType() { + return Type.NULL; } - public Object get() { - return _val; + return null; } - } + }; + public static final Value TRUE_VAL = new BooleanValue(Boolean.TRUE); + public static final Value FALSE_VAL = new BooleanValue(Boolean.FALSE); + public static final Value EMPTY_STR_VAL = new StringValue(""); private BuiltinOperators() {} @@ -79,51 +61,275 @@ public class BuiltinOperators // FIXME, Imp operator? public static Value negate(Value param1) { - // FIXME - return null; + if(paramIsNull(param1)) { + // null propagation + return NULL_VAL; + } + + Value.Type mathType = param1.getType(); + + switch(mathType) { + case BOOLEAN: + return toValue(-getAsNumericBoolean(param1)); + // case STRING: break; unsupported + case DATE: + case TIME: + case DATE_TIME: + // dates/times get converted to date doubles for arithmetic + double result = -param1.getAsDouble(); + return toDateValue(mathType, result, param1, null); + case LONG: + return toValue(-param1.getAsLong()); + case DOUBLE: + return toValue(-param1.getAsDouble()); + case BIG_INT: + return toValue(param1.getAsBigInteger().negate()); + case BIG_DEC: + return toValue(param1.getAsBigDecimal().negate()); + default: + throw new RuntimeException("Unexpected type " + mathType); + } } public static Value add(Value param1, Value param2) { - // FIXME - return null; + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + Value.Type mathType = getSimpleMathTypePrecedence(param1, param2); + + switch(mathType) { + case BOOLEAN: + return toValue(getAsNumericBoolean(param1) + getAsNumericBoolean(param2)); + case STRING: + // string '+' is a null-propagation (handled above) concat + return nonNullConcat(param1, param2); + case DATE: + case TIME: + case DATE_TIME: + // dates/times get converted to date doubles for arithmetic + double result = param1.getAsDouble() + param2.getAsDouble(); + return toDateValue(mathType, result, param1, param2); + case LONG: + return toValue(param1.getAsLong() + param2.getAsLong()); + case DOUBLE: + return toValue(param1.getAsDouble() + param2.getAsDouble()); + case BIG_INT: + return toValue(param1.getAsBigInteger().add(param2.getAsBigInteger())); + case BIG_DEC: + return toValue(param1.getAsBigDecimal().add(param2.getAsBigDecimal())); + default: + throw new RuntimeException("Unexpected type " + mathType); + } } public static Value subtract(Value param1, Value param2) { - // FIXME - return null; + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + Value.Type mathType = getSimpleMathTypePrecedence(param1, param2); + + switch(mathType) { + case BOOLEAN: + return toValue(getAsNumericBoolean(param1) - getAsNumericBoolean(param2)); + // case STRING: break; unsupported + case DATE: + case TIME: + case DATE_TIME: + // dates/times get converted to date doubles for arithmetic + double result = param1.getAsDouble() - param2.getAsDouble(); + return toDateValue(mathType, result, param1, param2); + case LONG: + return toValue(param1.getAsLong() - param2.getAsLong()); + case DOUBLE: + return toValue(param1.getAsDouble() - param2.getAsDouble()); + case BIG_INT: + return toValue(param1.getAsBigInteger().subtract(param2.getAsBigInteger())); + case BIG_DEC: + return toValue(param1.getAsBigDecimal().subtract(param2.getAsBigDecimal())); + default: + throw new RuntimeException("Unexpected type " + mathType); + } } public static Value multiply(Value param1, Value param2) { - // FIXME - return null; + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + Value.Type mathType = getGeneralMathTypePrecedence(param1, param2); + + switch(mathType) { + case BOOLEAN: + return toValue(getAsNumericBoolean(param1) * getAsNumericBoolean(param2)); + // case STRING: break; unsupported + // case DATE: break; promoted to double + // case TIME: break; promoted to double + // case DATE_TIME: break; promoted to double + case LONG: + return toValue(param1.getAsLong() * param2.getAsLong()); + case DOUBLE: + return toValue(param1.getAsDouble() * param2.getAsDouble()); + case BIG_INT: + return toValue(param1.getAsBigInteger().multiply(param2.getAsBigInteger())); + case BIG_DEC: + return toValue(param1.getAsBigDecimal().multiply(param2.getAsBigDecimal())); + default: + throw new RuntimeException("Unexpected type " + mathType); + } } public static Value divide(Value param1, Value param2) { - // FIXME - return null; + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + Value.Type mathType = getGeneralMathTypePrecedence(param1, param2); + + switch(mathType) { + case BOOLEAN: + return toValue(getAsNumericBoolean(param1) / getAsNumericBoolean(param2)); + // case STRING: break; unsupported + // case DATE: break; promoted to double + // case TIME: break; promoted to double + // case DATE_TIME: break; promoted to double + case LONG: + long lp1 = param1.getAsLong(); + long lp2 = param2.getAsLong(); + if((lp1 % lp2) == 0) { + return toValue(lp1 / lp2); + } + return toValue((double)lp1 / (double)lp2); + case DOUBLE: + return toValue(param1.getAsDouble() / param2.getAsDouble()); + case BIG_INT: + BigInteger bip1 = param1.getAsBigInteger(); + BigInteger bip2 = param2.getAsBigInteger(); + BigInteger[] res = bip1.divideAndRemainder(bip2); + if(res[1].compareTo(BigInteger.ZERO) == 0) { + return toValue(res[0]); + } + return toValue(new BigDecimal(bip1).divide(new BigDecimal(bip2))); + case BIG_DEC: + return toValue(param1.getAsBigDecimal().divide(param2.getAsBigDecimal())); + default: + throw new RuntimeException("Unexpected type " + mathType); + } } + @SuppressWarnings("fallthrough") public static Value intDivide(Value param1, Value param2) { - // FIXME - return null; + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + Value.Type mathType = getGeneralMathTypePrecedence(param1, param2); + + boolean wasDouble = false; + switch(mathType) { + case BOOLEAN: + return toValue(getAsNumericBoolean(param1) / getAsNumericBoolean(param2)); + // case STRING: break; unsupported + // case DATE: break; promoted to double + // case TIME: break; promoted to double + // case DATE_TIME: break; promoted to double + case LONG: + return toValue(param1.getAsLong() / param2.getAsLong()); + case DOUBLE: + wasDouble = true; + // fallthrough + case BIG_INT: + case BIG_DEC: + BigInteger result = param1.getAsBigInteger().divide( + param2.getAsBigInteger()); + return (wasDouble ? toValue(result.longValue()) : toValue(result)); + default: + throw new RuntimeException("Unexpected type " + mathType); + } } public static Value exp(Value param1, Value param2) { - // FIXME - return null; + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + Value.Type mathType = getGeneralMathTypePrecedence(param1, param2); + + // jdk only supports general pow() as doubles, let's go with that + double result = Math.pow(param1.getAsDouble(), param2.getAsDouble()); + + // attempt to convert integral types back to integrals if possible + switch(mathType) { + case BOOLEAN: + case LONG: + if(isIntegral(result)) { + return toValue((long)result); + } + break; + case BIG_INT: + if(isIntegral(result)) { + return toValue(BigDecimal.valueOf(result).toBigInteger()); + } + break; + } + + return toValue(result); + } + + @SuppressWarnings("fallthrough") + public static Value mod(Value param1, Value param2) { + if(anyParamIsNull(param1, param2)) { + // null propagation + return NULL_VAL; + } + + Value.Type mathType = getGeneralMathTypePrecedence(param1, param2); + + boolean wasDouble = false; + switch(mathType) { + case BOOLEAN: + return toValue(getAsNumericBoolean(param1) % getAsNumericBoolean(param2)); + // case STRING: break; unsupported + // case DATE: break; promoted to double + // case TIME: break; promoted to double + // case DATE_TIME: break; promoted to double + case LONG: + return toValue(param1.getAsDouble() % param2.getAsDouble()); + case DOUBLE: + wasDouble = true; + // fallthrough + case BIG_INT: + case BIG_DEC: + BigInteger result = param1.getAsBigInteger().mod(param2.getAsBigInteger()); + return (wasDouble ? toValue(result.longValue()) : toValue(result)); + default: + throw new RuntimeException("Unexpected type " + mathType); + } } public static Value concat(Value param1, Value param2) { + // note, this op converts null to empty string - + if(paramIsNull(param1)) { + param1 = EMPTY_STR_VAL; + } - // FIXME - return null; + if(paramIsNull(param2)) { + param2 = EMPTY_STR_VAL; + } + + return nonNullConcat(param1, param2); } - public static Value mod(Value param1, Value param2) { - // FIXME - return null; + private static Value nonNullConcat(Value param1, Value param2) { + return toValue(param1.getAsString().concat(param2.getAsString())); } public static Value not(Value param1) { @@ -132,7 +338,7 @@ public class BuiltinOperators return NULL_VAL; } - return toValue(!nonNullValueToBoolean(param1)); + return toValue(!param1.getAsBoolean()); } public static Value lessThan(Value param1, Value param2) { @@ -197,7 +403,7 @@ public class BuiltinOperators return NULL_VAL; } - boolean b1 = nonNullValueToBoolean(param1); + boolean b1 = param1.getAsBoolean(); if(!b1) { return FALSE_VAL; } @@ -206,7 +412,7 @@ public class BuiltinOperators return NULL_VAL; } - return toValue(nonNullValueToBoolean(param2)); + return toValue(param2.getAsBoolean()); } public static Value or(Value param1, Value param2) { @@ -217,7 +423,7 @@ public class BuiltinOperators return NULL_VAL; } - boolean b1 = nonNullValueToBoolean(param1); + boolean b1 = param1.getAsBoolean(); if(b1) { return TRUE_VAL; } @@ -226,7 +432,7 @@ public class BuiltinOperators return NULL_VAL; } - return toValue(nonNullValueToBoolean(param2)); + return toValue(param2.getAsBoolean()); } public static Value eqv(Value param1, Value param2) { @@ -235,8 +441,8 @@ public class BuiltinOperators return NULL_VAL; } - boolean b1 = nonNullValueToBoolean(param1); - boolean b2 = nonNullValueToBoolean(param2); + boolean b1 = param1.getAsBoolean(); + boolean b2 = param2.getAsBoolean(); return toValue(b1 == b2); } @@ -247,8 +453,8 @@ public class BuiltinOperators return NULL_VAL; } - boolean b1 = nonNullValueToBoolean(param1); - boolean b2 = nonNullValueToBoolean(param2); + boolean b1 = param1.getAsBoolean(); + boolean b2 = param2.getAsBoolean(); return toValue(b1 ^ b2); } @@ -258,7 +464,7 @@ public class BuiltinOperators // "imp" uses short-circuit logic if(paramIsNull(param1)) { - if(paramIsNull(param2) || !nonNullValueToBoolean(param2)) { + if(paramIsNull(param2) || !param2.getAsBoolean()) { // null propagation return NULL_VAL; } @@ -266,7 +472,7 @@ public class BuiltinOperators return TRUE_VAL; } - boolean b1 = nonNullValueToBoolean(param1); + boolean b1 = param1.getAsBoolean(); if(!b1) { return TRUE_VAL; } @@ -276,7 +482,7 @@ public class BuiltinOperators return NULL_VAL; } - return toValue(nonNullValueToBoolean(param2)); + return toValue(param2.getAsBoolean()); } public static Value isNull(Value param1) { @@ -288,20 +494,23 @@ public class BuiltinOperators } public static Value like(Value param1, Pattern pattern) { - // FIXME - return null; + if(paramIsNull(param1)) { + // null propagation + return NULL_VAL; + } + + return toValue(pattern.matcher(param1.getAsString()).matches()); } public static Value between(Value param1, Value param2, Value param3) { - - // null propagate any field left to right. uses short circuit eval + // FIXME, use delay for and() or check here? + // null propagate any param. uses short circuit eval of params if(anyParamIsNull(param1, param2, param3)) { // null propagation return NULL_VAL; } - // FIXME - return null; + return and(greaterThanEq(param1, param2), lessThanEq(param1, param3)); } public static Value notBetween(Value param1, Value param2, Value param3) { @@ -346,95 +555,163 @@ public class BuiltinOperators return (param1.getType() == Value.Type.NULL); } - protected static CharSequence paramToString(Object param) { - try { - return ColumnImpl.toCharSequence(param); - } catch(IOException e) { - throw new RuntimeIOException(e); - } - } - - protected static boolean paramToBoolean(Object param) { - // FIXME, null is false...? - return ColumnImpl.toBooleanValue(param); - } - - protected static Number paramToNumber(Object param) { - // FIXME - return null; - } + protected static int nonNullCompareTo( + Value param1, Value param2) + { + Value.Type compareType = getGeneralMathTypePrecedence(param1, param2); - protected static boolean nonNullValueToBoolean(Value val) { - switch(val.getType()) { + switch(compareType) { case BOOLEAN: - return (Boolean)val.get(); + return compare(getAsNumericBoolean(param1), getAsNumericBoolean(param2)); case STRING: - case DATE: - case TIME: - case DATE_TIME: - // strings and dates are always true - return true; + // string comparison is only valid if _both_ params are strings + if(param1.getType() != param2.getType()) { + throw new RuntimeException("Unexpected type " + compareType); + } + return param1.getAsString().compareToIgnoreCase(param2.getAsString()); + // case DATE: break; promoted to double + // case TIME: break; promoted to double + // case DATE_TIME: break; promoted to double case LONG: - return (((Number)val.get()).longValue() != 0L); + return param1.getAsLong().compareTo(param2.getAsLong()); case DOUBLE: - return (((Number)val.get()).doubleValue() != 0.0d); + return param1.getAsDouble().compareTo(param2.getAsDouble()); case BIG_INT: - return (((BigInteger)val.get()).compareTo(BigInteger.ZERO) != 0L); + return param1.getAsBigInteger().compareTo(param2.getAsBigInteger()); case BIG_DEC: - return (((BigDecimal)val.get()).compareTo(BigDecimal.ZERO) != 0L); + return param1.getAsBigDecimal().compareTo(param2.getAsBigDecimal()); default: - throw new RuntimeException("Unexpected type " + val.getType()); + throw new RuntimeException("Unexpected type " + compareType); } } - protected static int nonNullCompareTo( - Value param1, Value param2) - { - // FIXME - return 0; + private static int compare(long l1, long l2) { + return ((l1 < l2) ? -1 : ((l1 > l2) ? 1 : 0)); + } + + private static long getAsNumericBoolean(Value v) { + return BooleanValue.numericBoolean(v.getAsBoolean()); } public static Value toValue(boolean b) { return (b ? TRUE_VAL : FALSE_VAL); } - public static Value toValue(Object obj) { - if(obj == null) { - return NULL_VAL; + public static Value toValue(String s) { + return new StringValue(s); + } + + public static Value toValue(Long s) { + return new LongValue(s); + } + + public static Value toValue(Double s) { + return new DoubleValue(s); + } + + public static Value toValue(BigInteger s) { + return new BigIntegerValue(s); + } + + public static Value toValue(BigDecimal s) { + return new BigDecimalValue(s); + } + + private static Value toDateValue(Value.Type type, double v, + Value param1, Value param2) + { + // FIXME find format from first matching param + DateFormat fmt = null; + // if(param1.getType() == type) { + // fmt = (DateFormat)param1.getFormat(); + // } else if(param2 != null) { + // fmt = (DateFormat)param2.getFormat(); + // } + + Date d = new Date(ColumnImpl.fromDateDouble(v, fmt.getCalendar())); + + switch(type) { + case DATE: + return new DateValue(d, fmt); + case TIME: + return new TimeValue(d, fmt); + case DATE_TIME: + return new DateTimeValue(d, fmt); + default: + throw new RuntimeException("Unexpected type " + type); + } + } + + 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(obj instanceof Value) { - return (Value)obj; + if((t1 == Value.Type.STRING) || (t2 == Value.Type.STRING)) { + // string always wins + return Value.Type.STRING; } - if(obj instanceof Boolean) { - return (((Boolean)obj) ? TRUE_VAL : FALSE_VAL); + // 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(obj instanceof Date) { - // any way to figure out whether it's a date/time/dateTime? - return new SimpleValue(Value.Type.DATE_TIME, obj); + // if both types are integral, choose "largest" + if(t1.isIntegral() && t2.isIntegral()) { + return max(t1, t2); } - if(obj instanceof Number) { - if((obj instanceof Double) || (obj instanceof Float)) { - return new SimpleValue(Value.Type.DOUBLE, obj); - } - if(obj instanceof BigDecimal) { - return new SimpleValue(Value.Type.BIG_DEC, obj); - } - if(obj instanceof BigInteger) { - return new SimpleValue(Value.Type.BIG_INT, obj); + // choose largest relevant floating-point type + return max(t1.getPreferredFPType(), t2.getPreferredFPType()); + } + + private static Value.Type getGeneralMathTypePrecedence( + Value param1, Value param2) + { + Value.Type t1 = param1.getType(); + Value.Type t2 = param2.getType(); + + // note: for general math, date/time become double + + if(t1 == t2) { + + if(t1.isTemporal()) { + return Value.Type.DOUBLE; } - return new SimpleValue(Value.Type.LONG, obj); + + return t1; } - try { - return new SimpleValue(Value.Type.STRING, - ColumnImpl.toCharSequence(obj).toString()); - } catch(IOException e) { - throw new RuntimeIOException(e); + if((t1 == Value.Type.STRING) || (t2 == Value.Type.STRING)) { + // string always wins + return Value.Type.STRING; } + + // 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 max(Value.Type t1, Value.Type t2) { + return ((t1.compareTo(t2) > 0) ? t1 : t2); + } + + private static boolean isIntegral(double d) { + return (d == Math.rint(d)); } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java new file mode 100644 index 0000000..abc047f --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java @@ -0,0 +1,37 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl.expr; + +import java.text.DateFormat; +import java.util.Date; + +/** + * + * @author James Ahlborn + */ +public class DateTimeValue extends BaseDateValue +{ + + public DateTimeValue(Date val, DateFormat fmt) + { + super(val, fmt); + } + + public Type getType() { + return Type.DATE_TIME; + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateValue.java new file mode 100644 index 0000000..558e3ab --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateValue.java @@ -0,0 +1,36 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl.expr; + +import java.text.DateFormat; +import java.util.Date; + +/** + * + * @author James Ahlborn + */ +public class DateValue extends BaseDateValue +{ + public DateValue(Date val, DateFormat fmt) + { + super(val, fmt); + } + + public Type getType() { + return Type.DATE; + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java index 7031f0e..70ff3cc 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java @@ -20,10 +20,8 @@ import java.util.HashMap; import java.util.Map; -import com.healthmarketscience.jackcess.expr.Expression; import com.healthmarketscience.jackcess.expr.Value; import com.healthmarketscience.jackcess.expr.Function; -import com.healthmarketscience.jackcess.expr.RowContext; /** * @@ -68,21 +66,6 @@ public class DefaultFunctions } } - protected static CharSequence paramToString(Object param) - { - return BuiltinOperators.paramToString(param); - } - - protected static boolean paramToBoolean(Object param) - { - return BuiltinOperators.paramToBoolean(param); - } - - protected static Number paramToNumber(Object param) - { - return BuiltinOperators.paramToNumber(param); - } - @Override public String toString() { return getName() + "()"; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java new file mode 100644 index 0000000..4a77fb9 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java @@ -0,0 +1,67 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl.expr; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * + * @author James Ahlborn + */ +public class DoubleValue extends BaseNumericValue +{ + private final Double _val; + + public DoubleValue(Double val) + { + _val = val; + } + + public Type getType() { + return Type.DOUBLE; + } + + public Object get() { + return _val; + } + + @Override + protected Number getNumber() { + return _val; + } + + @Override + public Boolean getAsBoolean() { + return (_val.doubleValue() != 0.0d); + } + + @Override + public Double getAsDouble() { + return _val; + } + + @Override + public BigInteger getAsBigInteger() { + return getAsBigDecimal().toBigInteger(); + } + + @Override + public BigDecimal getAsBigDecimal() { + return BigDecimal.valueOf(_val); + } +} 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 40418f4..570823a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java @@ -17,15 +17,20 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; +import java.text.DateFormat; +import java.text.FieldPosition; import java.text.ParseException; -import java.text.SimpleDateFormat; +import java.text.ParsePosition; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TimeZone; import static com.healthmarketscience.jackcess.impl.expr.Expressionator.*; import com.healthmarketscience.jackcess.expr.Value; @@ -44,9 +49,16 @@ class ExpressionTokenizer private static final char DATE_LIT_QUOTE_CHAR = '#'; private static final char EQUALS_CHAR = '='; - private static final String DATE_FORMAT = "M/d/yyyy"; - private static final String TIME_FORMAT = "HH:mm:ss"; - private static final String DATE_TIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT; + static final String DATE_FORMAT = "M/d/yyyy"; + static final String TIME_FORMAT_24 = "HH:mm:ss"; + static final String TIME_FORMAT_12 = "hh:mm:ss a"; + static final String DATE_TIME_FORMAT_24 = DATE_FORMAT + " " + TIME_FORMAT_24; + static final String DATE_TIME_FORMAT_12 = DATE_FORMAT + " " + TIME_FORMAT_12; + private static final int AMPM_SUFFIX_LEN = 3; + private static final String AM_SUFFIX = " am"; + private static final String PM_SUFFIX = " pm"; + // access times are based on this date (not the UTC base) + private static final String BASE_DATE = "12/30/1899 "; private static final byte IS_OP_FLAG = 0x01; private static final byte IS_COMP_FLAG = 0x02; @@ -87,7 +99,7 @@ class ExpressionTokenizer List tokens = new ArrayList(); - ExprBuf buf = new ExprBuf(exprStr); + ExprBuf buf = new ExprBuf(exprStr, context); while(buf.hasNext()) { char c = buf.next(); @@ -156,7 +168,7 @@ class ExpressionTokenizer Value.Type.STRING)); break; case DATE_LIT_QUOTE_CHAR: - tokens.add(parseDateLiteralString(buf, context)); + tokens.add(parseDateLiteralString(buf)); break; case OBJ_NAME_START_CHAR: tokens.add(new Token(TokenType.OBJ_NAME, parseObjNameString(buf))); @@ -307,24 +319,33 @@ class ExpressionTokenizer return sb.toString(); } - private static Token parseDateLiteralString( - ExprBuf buf, ParseContext context) + private static Token parseDateLiteralString(ExprBuf buf) { String dateStr = parseStringUntil(buf, DATE_LIT_QUOTE_CHAR, null); boolean hasDate = (dateStr.indexOf('/') >= 0); boolean hasTime = (dateStr.indexOf(':') >= 0); + boolean hasAmPm = false; + + if(hasTime) { + int strLen = dateStr.length(); + hasTime = ((strLen >= AMPM_SUFFIX_LEN) && + (dateStr.regionMatches(true, strLen - AMPM_SUFFIX_LEN, + AM_SUFFIX, 0, AMPM_SUFFIX_LEN) || + dateStr.regionMatches(true, strLen - AMPM_SUFFIX_LEN, + PM_SUFFIX, 0, AMPM_SUFFIX_LEN))); + } - SimpleDateFormat sdf = null; + DateFormat sdf = null; Value.Type valType = null; if(hasDate && hasTime) { - sdf = buf.getDateTimeFormat(context); + sdf = (hasAmPm ? buf.getDateTimeFormat12() : buf.getDateTimeFormat24()); valType = Value.Type.DATE_TIME; } else if(hasDate) { - sdf = buf.getDateFormat(context); + sdf = buf.getDateFormat(); valType = Value.Type.DATE; } else if(hasTime) { - sdf = buf.getTimeFormat(context); + sdf = (hasAmPm ? buf.getTimeFormat12() : buf.getTimeFormat24()); valType = Value.Type.TIME; } else { throw new IllegalArgumentException("Invalid date time literal " + dateStr + @@ -409,14 +430,18 @@ class ExpressionTokenizer private static final class ExprBuf { private final String _str; + private final ParseContext _ctx; private int _pos; - private SimpleDateFormat _dateFmt; - private SimpleDateFormat _timeFmt; - private SimpleDateFormat _dateTimeFmt; + private DateFormat _dateFmt; + private DateFormat _timeFmt12; + private DateFormat _dateTimeFmt12; + private DateFormat _timeFmt24; + private DateFormat _dateTimeFmt24; private final StringBuilder _scratch = new StringBuilder(); - private ExprBuf(String str) { + private ExprBuf(String str, ParseContext ctx) { _str = str; + _ctx = ctx; } private int len() { @@ -459,25 +484,41 @@ class ExpressionTokenizer return _scratch; } - public SimpleDateFormat getDateFormat(ParseContext context) { + public DateFormat getDateFormat() { if(_dateFmt == null) { - _dateFmt = context.createDateFormat(DATE_FORMAT); + _dateFmt = _ctx.createDateFormat(DATE_FORMAT); } return _dateFmt; } - public SimpleDateFormat getTimeFormat(ParseContext context) { - if(_timeFmt == null) { - _timeFmt = context.createDateFormat(TIME_FORMAT); + public DateFormat getTimeFormat12() { + if(_timeFmt12 == null) { + _timeFmt12 = new TimeFormat( + getDateTimeFormat12(), _ctx.createDateFormat(TIME_FORMAT_12)); + } + return _timeFmt12; + } + + public DateFormat getDateTimeFormat12() { + if(_dateTimeFmt12 == null) { + _dateTimeFmt12 = _ctx.createDateFormat(DATE_TIME_FORMAT_12); + } + return _dateTimeFmt12; + } + + public DateFormat getTimeFormat24() { + if(_timeFmt24 == null) { + _timeFmt24 = new TimeFormat( + getDateTimeFormat24(), _ctx.createDateFormat(TIME_FORMAT_24)); } - return _timeFmt; + return _timeFmt24; } - public SimpleDateFormat getDateTimeFormat(ParseContext context) { - if(_dateTimeFmt == null) { - _dateTimeFmt = context.createDateFormat(DATE_TIME_FORMAT); + public DateFormat getDateTimeFormat24() { + if(_dateTimeFmt24 == null) { + _dateTimeFmt24 = _ctx.createDateFormat(DATE_TIME_FORMAT_24); } - return _dateTimeFmt; + return _dateTimeFmt24; } @Override @@ -493,20 +534,27 @@ class ExpressionTokenizer private final Object _val; private final String _valStr; private final Value.Type _valType; + private final DateFormat _sdf; private Token(TokenType type, String val) { this(type, val, val); } private Token(TokenType type, Object val, String valStr) { - this(type, val, valStr, null); + this(type, val, valStr, null, null); } private Token(TokenType type, Object val, String valStr, Value.Type valType) { + this(type, val, valStr, valType, null); + } + + private Token(TokenType type, Object val, String valStr, Value.Type valType, + DateFormat sdf) { _type = type; _val = ((val != null) ? val : valStr); _valStr = valStr; _valType = valType; + _sdf = sdf; } public TokenType getType() { @@ -525,6 +573,10 @@ class ExpressionTokenizer return _valType; } + public DateFormat getDateFormat() { + return _sdf; + } + @Override public String toString() { if(_type == TokenType.SPACE) { @@ -536,6 +588,41 @@ class ExpressionTokenizer } return str; } - } + } + + private static final class TimeFormat extends DateFormat + { + private static final long serialVersionUID = 0L; + private final DateFormat _parseDelegate; + private final DateFormat _fmtDelegate; + + private TimeFormat(DateFormat parseDelegate, DateFormat fmtDelegate) + { + _parseDelegate = parseDelegate; + _fmtDelegate = fmtDelegate; + } + + @Override + public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { + return _fmtDelegate.format(date, toAppendTo, fieldPosition); + } + + @Override + public Date parse(String source, ParsePosition pos) { + // we parse as a full date/time in order to get the correct "base date" + // used by access + return _parseDelegate.parse(BASE_DATE + source, pos); + } + + @Override + public Calendar getCalendar() { + return _fmtDelegate.getCalendar(); + } + + @Override + public TimeZone getTimeZone() { + return _fmtDelegate.getTimeZone(); + } + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java index 625358e..e616393 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java @@ -16,10 +16,13 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; +import java.math.BigDecimal; +import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; @@ -32,11 +35,12 @@ import java.util.regex.Pattern; import com.healthmarketscience.jackcess.DatabaseBuilder; import com.healthmarketscience.jackcess.expr.Expression; -import com.healthmarketscience.jackcess.expr.Value; import com.healthmarketscience.jackcess.expr.Function; import com.healthmarketscience.jackcess.expr.RowContext; -import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.TokenType; +import com.healthmarketscience.jackcess.expr.Value; import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.Token; +import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.TokenType; + /** * @@ -452,7 +456,8 @@ public class Expressionator case LITERAL: - buf.setPendingExpr(new ELiteralValue(t.getValueType(), t.getValue())); + buf.setPendingExpr(new ELiteralValue(t.getValueType(), t.getValue(), + t.getDateFormat())); break; case OP: @@ -1230,6 +1235,24 @@ public class Expressionator Pattern.UNICODE_CASE); } + private static Value toLiteralValue(Value.Type valType, Object value, + DateFormat sdf) + { + switch(valType) { + case STRING: + return new StringValue((String)value); + case DATE: + return new DateValue((Date)value, sdf); + case TIME: + return new TimeValue((Date)value, sdf); + case DATE_TIME: + return new DateTimeValue((Date)value, sdf); + case BIG_DEC: + return new BigDecimalValue((BigDecimal)value); + default: + throw new RuntimeException("unexpected literal type " + valType); + } + } private interface LeftAssocExpr { public OpType getOp(); @@ -1243,9 +1266,8 @@ public class Expressionator public void setRight(Expr right); } - private static final class DelayedValue implements Value + private static final class DelayedValue extends BaseDelayedValue { - private Value _val; private final Expr _expr; private final RowContext _ctx; @@ -1254,19 +1276,9 @@ public class Expressionator _ctx = ctx; } - private Value getDelegate() { - if(_val == null) { - _val = _expr.eval(_ctx); - } - return _val; - } - - public Value.Type getType() { - return getDelegate().getType(); - } - - public Object get() { - return getDelegate().get(); + @Override + protected Value eval() { + return _expr.eval(_ctx); } } @@ -1411,8 +1423,9 @@ public class Expressionator { private final Value _val; - private ELiteralValue(Value.Type valType, Object value) { - _val = new BuiltinOperators.SimpleValue(valType, value); + private ELiteralValue(Value.Type valType, Object value, + DateFormat sdf) { + _val = toLiteralValue(valType, value, sdf); } @Override @@ -1425,9 +1438,7 @@ public class Expressionator if(_val.getType() == Value.Type.STRING) { literalStrToString((String)_val.get(), sb); } else if(_val.getType().isTemporal()) { - // // FIXME Date,Time,DateTime formatting? - // sb.append("#").append(_value).append("#"); - throw new UnsupportedOperationException(); + sb.append("#").append(_val.getAsString()).append("#"); } else { sb.append(_val.get()); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java new file mode 100644 index 0000000..a85b7ff --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/LongValue.java @@ -0,0 +1,67 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl.expr; + +import java.math.BigDecimal; +import java.math.BigInteger; + +/** + * + * @author James Ahlborn + */ +public class LongValue extends BaseNumericValue +{ + private final Long _val; + + public LongValue(Long val) + { + _val = val; + } + + public Type getType() { + return Type.LONG; + } + + public Object get() { + return _val; + } + + @Override + protected Number getNumber() { + return _val; + } + + @Override + public Boolean getAsBoolean() { + return (_val.longValue() != 0L); + } + + @Override + public Long getAsLong() { + return _val; + } + + @Override + public BigInteger getAsBigInteger() { + return BigInteger.valueOf(_val); + } + + @Override + public BigDecimal getAsBigDecimal() { + return BigDecimal.valueOf(_val); + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java new file mode 100644 index 0000000..5bbaa9d --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java @@ -0,0 +1,50 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl.expr; + +/** + * + * @author James Ahlborn + */ +public class StringValue extends BaseValue +{ + private final String _val; + + public StringValue(String val) + { + _val = val; + } + + public Type getType() { + return Type.STRING; + } + + public Object get() { + return _val; + } + + @Override + public Boolean getAsBoolean() { + // ms access seems to treat strings as "true" + return Boolean.TRUE; + } + + @Override + public String getAsString() { + return _val; + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/TimeValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/TimeValue.java new file mode 100644 index 0000000..cedb461 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/TimeValue.java @@ -0,0 +1,37 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl.expr; + +import java.text.DateFormat; +import java.util.Date; + +/** + * + * @author James Ahlborn + */ +public class TimeValue extends BaseDateValue +{ + + public TimeValue(Date val, DateFormat fmt) + { + super(val, fmt); + } + + public Type getType() { + return Type.TIME; + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java index a2bf31a..a564834 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java @@ -23,6 +23,7 @@ import java.util.Arrays; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.Table; import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; import org.apache.commons.lang.ObjectUtils; /** @@ -54,8 +55,9 @@ public class SimpleColumnMatcher implements ColumnMatcher { // values and try again DataType dataType = table.getColumn(columnName).getType(); try { - Object internalV1 = ColumnImpl.toInternalValue(dataType, value1); - Object internalV2 = ColumnImpl.toInternalValue(dataType, value2); + DatabaseImpl db = (DatabaseImpl)table.getDatabase(); + Object internalV1 = ColumnImpl.toInternalValue(dataType, value1, db); + Object internalV2 = ColumnImpl.toInternalValue(dataType, value2, db); return equals(internalV1, internalV2); } catch(IOException e) {