From 985b8674968ca118a4845280174aa69924242145 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Tue, 27 Nov 2018 02:35:52 +0000 Subject: update parent pom; fix test failure git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1229 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/test') diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java index 132b788..b50f2de 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java @@ -589,7 +589,7 @@ public class DefaultFunctionsTest extends TestCase assertEval("409.090909090909", "=CStr(SYD(30000,7500,10,10))"); assertEval("-1.63048347266756E-02", "=CStr(Rate(3,200,-610,0,-20,0.1))"); - assertEval("7.70147248820175E-03", "=CStr(Rate(4*12,-200,8000))"); + assertEval("7.70147248820165E-03", "=CStr(Rate(4*12,-200,8000))"); assertEval("-1.09802980531205", "=CStr(Rate(60,93.22,5000,0.1))"); } -- cgit v1.2.3 From f6315def9c252d7b3866eb580d4c0c5de003a86e Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Tue, 27 Nov 2018 02:59:03 +0000 Subject: switch to commons lang3 git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1230 f203690c-595d-4dc9-a70b-905162fa7fd2 --- pom.xml | 10 ++-- .../jackcess/expr/Identifier.java | 9 ++-- .../jackcess/impl/ColumnImpl.java | 2 +- .../jackcess/impl/CompoundOleUtil.java | 2 +- .../jackcess/impl/CustomToStringStyle.java | 20 ++++---- .../jackcess/impl/DatabaseImpl.java | 4 +- .../jackcess/impl/IndexData.java | 2 +- .../jackcess/impl/IndexImpl.java | 2 +- .../jackcess/impl/IndexPageCache.java | 2 +- .../healthmarketscience/jackcess/impl/OleUtil.java | 2 +- .../jackcess/impl/RowIdImpl.java | 2 +- .../jackcess/impl/TableImpl.java | 2 +- .../jackcess/impl/expr/DefaultTextFunctions.java | 12 ++--- .../jackcess/impl/expr/Expressionator.java | 2 +- .../jackcess/impl/expr/StringValue.java | 2 +- .../jackcess/impl/query/QueryFormat.java | 15 +++--- .../jackcess/impl/query/QueryImpl.java | 2 +- .../jackcess/util/RowFilter.java | 32 ++++++------ .../jackcess/util/SimpleColumnMatcher.java | 8 +-- .../jackcess/query/QueryTest.java | 57 +++++++++++----------- .../jackcess/util/ExportTest.java | 11 ++--- 21 files changed, 98 insertions(+), 102 deletions(-) (limited to 'src/test') diff --git a/pom.xml b/pom.xml index 41af67c..a876000 100644 --- a/pom.xml +++ b/pom.xml @@ -194,9 +194,9 @@ - commons-lang - commons-lang - 2.6 + org.apache.commons + commons-lang3 + 3.8.1 commons-logging @@ -254,10 +254,10 @@ 128m 512 - http://docs.oracle.com/javase/1.5.0/docs/api/ + https://docs.oracle.com/javase/8/docs/api/ http://docs.oracle.com/javaee/5/api/ - 1.5 + 1.8 public ${basedir}/src/site/javadoc/stylesheet.css diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java b/src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java index 45db1ad..709a7cd 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/Identifier.java @@ -16,7 +16,8 @@ limitations under the License. package com.healthmarketscience.jackcess.expr; -import org.apache.commons.lang.ObjectUtils; +import java.util.Objects; + /** * identifies a database entity (e.g. the name of a database field). An @@ -71,9 +72,9 @@ public class Identifier Identifier oi = (Identifier)o; - return (ObjectUtils.equals(_objectName, oi._objectName) && - ObjectUtils.equals(_collectionName, oi._collectionName) && - ObjectUtils.equals(_propertyName, oi._propertyName)); + return (Objects.equals(_objectName, oi._objectName) && + Objects.equals(_collectionName, oi._collectionName) && + Objects.equals(_propertyName, oi._propertyName)); } @Override diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index be2bf1c..8fa1906 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -54,7 +54,7 @@ import com.healthmarketscience.jackcess.expr.Identifier; import com.healthmarketscience.jackcess.impl.complex.ComplexValueForeignKeyImpl; import com.healthmarketscience.jackcess.util.ColumnValidator; import com.healthmarketscience.jackcess.util.SimpleColumnValidator; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/CompoundOleUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/CompoundOleUtil.java index 8440f81..18811c3 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/CompoundOleUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/CompoundOleUtil.java @@ -32,7 +32,7 @@ import com.healthmarketscience.jackcess.RuntimeIOException; import static com.healthmarketscience.jackcess.impl.OleUtil.*; import com.healthmarketscience.jackcess.util.MemFileChannel; import static com.healthmarketscience.jackcess.util.OleBlob.*; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.poi.poifs.filesystem.DirectoryEntry; import org.apache.poi.poifs.filesystem.DocumentEntry; import org.apache.poi.poifs.filesystem.DocumentInputStream; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java b/src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java index 707e163..89ef061 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/CustomToStringStyle.java @@ -21,9 +21,8 @@ import java.util.Collection; import java.util.Iterator; import java.util.Map; -import org.apache.commons.lang.SystemUtils; -import org.apache.commons.lang.builder.StandardToStringStyle; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.StandardToStringStyle; +import org.apache.commons.lang3.builder.ToStringBuilder; /** * Custom ToStringStyle for use with ToStringBuilder. @@ -34,7 +33,7 @@ public class CustomToStringStyle extends StandardToStringStyle { private static final long serialVersionUID = 0L; - private static final String ML_FIELD_SEP = SystemUtils.LINE_SEPARATOR + " "; + private static final String ML_FIELD_SEP = System.lineSeparator() + " "; private static final String IMPL_SUFFIX = "Impl"; private static final int MAX_BYTE_DETAIL_LEN = 20; private static final Object IGNORE_ME = new Object(); @@ -47,7 +46,7 @@ public class CustomToStringStyle extends StandardToStringStyle setFieldSeparatorAtStart(true); setFieldNameValueSeparator(": "); setArraySeparator("," + ML_FIELD_SEP); - setContentEnd(SystemUtils.LINE_SEPARATOR + "]"); + setContentEnd(System.lineSeparator() + "]"); setUseShortClassName(true); } }; @@ -91,7 +90,7 @@ public class CustomToStringStyle extends StandardToStringStyle } @Override - protected String getShortClassName(Class clss) { + protected String getShortClassName(Class clss) { String shortName = super.getShortClassName(clss); if(shortName.endsWith(IMPL_SUFFIX)) { shortName = shortName.substring(0, @@ -116,7 +115,7 @@ public class CustomToStringStyle extends StandardToStringStyle @Override protected void appendDetail(StringBuffer buffer, String fieldName, - Collection value) { + Collection value) { buffer.append("["); // gather contents of list in a new StringBuffer @@ -145,13 +144,12 @@ public class CustomToStringStyle extends StandardToStringStyle @Override protected void appendDetail(StringBuffer buffer, String fieldName, - Map value) { + Map value) { buffer.append("{"); // gather contents of map in a new StringBuffer StringBuffer sb = new StringBuffer(); - @SuppressWarnings("unchecked") - Iterator> iter = value.entrySet().iterator(); + Iterator> iter = value.entrySet().iterator(); if(iter.hasNext()) { if(isFieldSeparatorAtStart()) { appendFieldSeparator(sb); @@ -203,7 +201,7 @@ public class CustomToStringStyle extends StandardToStringStyle private static String indent(Object obj) { return ((obj != null) ? obj.toString().replaceAll( - SystemUtils.LINE_SEPARATOR, ML_FIELD_SEP) : null); + System.lineSeparator(), ML_FIELD_SEP) : null); } public static Object ignoreNull(Object obj) { diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index 6831e99..220d5b0 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -73,8 +73,8 @@ import com.healthmarketscience.jackcess.util.LinkResolver; import com.healthmarketscience.jackcess.util.ReadOnlyFileChannel; import com.healthmarketscience.jackcess.util.SimpleColumnValidatorFactory; import com.healthmarketscience.jackcess.util.TableIterableBuilder; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java index e2b2dd5..341859d 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java @@ -33,7 +33,7 @@ import com.healthmarketscience.jackcess.IndexBuilder; import com.healthmarketscience.jackcess.RuntimeIOException; import static com.healthmarketscience.jackcess.impl.ByteUtil.ByteStream; import static com.healthmarketscience.jackcess.impl.IndexCodes.*; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java index 0fbd231..5332432 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java @@ -25,7 +25,7 @@ import java.util.Map; import com.healthmarketscience.jackcess.CursorBuilder; import com.healthmarketscience.jackcess.Index; import com.healthmarketscience.jackcess.IndexBuilder; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java index 4ee1882..d594c1c 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java @@ -29,7 +29,7 @@ import java.util.Map; import java.util.RandomAccess; import static com.healthmarketscience.jackcess.impl.IndexData.*; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; /** * Manager of the index pages for a IndexData. diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/OleUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/OleUtil.java index d8a2336..466b6a4 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/OleUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/OleUtil.java @@ -35,7 +35,7 @@ import java.util.regex.Pattern; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.util.OleBlob; import static com.healthmarketscience.jackcess.util.OleBlob.*; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; /** * Utility code for working with OLE data. diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java index 7bfab4a..5ce8214 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/RowIdImpl.java @@ -19,7 +19,7 @@ package com.healthmarketscience.jackcess.impl; import java.io.Serializable; import com.healthmarketscience.jackcess.RowId; -import org.apache.commons.lang.builder.CompareToBuilder; +import org.apache.commons.lang3.builder.CompareToBuilder; /** diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java index 15a0c8c..e065f05 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java @@ -52,7 +52,7 @@ import com.healthmarketscience.jackcess.Table; import com.healthmarketscience.jackcess.expr.Identifier; import com.healthmarketscience.jackcess.util.ErrorHandler; import com.healthmarketscience.jackcess.util.ExportUtil; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java index 5643766..fcc9cb5 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultTextFunctions.java @@ -13,7 +13,6 @@ 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; @@ -23,7 +22,6 @@ import com.healthmarketscience.jackcess.expr.EvalException; import com.healthmarketscience.jackcess.expr.Function; import com.healthmarketscience.jackcess.expr.LocaleContext; import com.healthmarketscience.jackcess.expr.Value; -import org.apache.commons.lang.WordUtils; import static com.healthmarketscience.jackcess.impl.expr.DefaultFunctions.*; import static com.healthmarketscience.jackcess.impl.expr.FunctionSupport.*; @@ -360,6 +358,7 @@ public class DefaultTextFunctions } }); + @SuppressWarnings("deprecation") public static final Function STRCONV = registerStringFunc(new FuncVar("StrConv", 2, 3) { @Override protected Value evalVar(EvalContext ctx, Value[] params) { @@ -387,7 +386,8 @@ public class DefaultTextFunctions break; case 3: // vbProperCase - str = WordUtils.capitalize(str.toLowerCase()); + str = org.apache.commons.lang3.text.WordUtils.capitalize( + str.toLowerCase()); break; default: // do nothing @@ -441,11 +441,11 @@ public class DefaultTextFunctions String fmtStr = params[1].getAsString(ctx); int firstDay = DefaultDateFunctions.getFirstDayParam(ctx, params, 2); int firstWeekType = DefaultDateFunctions.getFirstWeekTypeParam(ctx, params, 3); - + return FormatUtil.format(ctx, expr, fmtStr, firstDay, firstWeekType); } }); - + private static String nchars(int num, char c) { StringBuilder sb = new StringBuilder(num); nchars(sb, num, c); @@ -457,7 +457,7 @@ public class DefaultTextFunctions sb.append(c); } } - + private static String trim(String str, boolean doLeft, boolean doRight) { int start = 0; int end = str.length(); 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 d2bb847..bee27ca 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java @@ -47,7 +47,7 @@ import com.healthmarketscience.jackcess.expr.TemporalConfig; import com.healthmarketscience.jackcess.expr.Value; import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.Token; import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.TokenType; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; /** diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java index 19b22eb..197d8b5 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java @@ -23,7 +23,7 @@ import java.text.DecimalFormatSymbols; import com.healthmarketscience.jackcess.expr.EvalException; import com.healthmarketscience.jackcess.expr.LocaleContext; import com.healthmarketscience.jackcess.expr.Value; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; /** * diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryFormat.java b/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryFormat.java index f9513d4..f0272f3 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryFormat.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryFormat.java @@ -22,14 +22,13 @@ import java.util.regex.Pattern; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.query.Query; -import org.apache.commons.lang.SystemUtils; /** * Constants used by the query data parsing. - * + * * @author James Ahlborn */ -public class QueryFormat +public class QueryFormat { private QueryFormat() {} @@ -90,7 +89,7 @@ public class QueryFormat public static final short APPEND_VALUE_FLAG = (short)0x8000; public static final short CROSSTAB_PIVOT_FLAG = 0x01; - public static final short CROSSTAB_NORMAL_FLAG = 0x02; + public static final short CROSSTAB_NORMAL_FLAG = 0x02; public static final String UNION_PART1 = "X7YZ_____1"; public static final String UNION_PART2 = "X7YZ_____2"; @@ -102,10 +101,10 @@ public class QueryFormat public static final Pattern IDENTIFIER_SEP_PAT = Pattern.compile("\\."); public static final char IDENTIFIER_SEP_CHAR = '.'; - public static final String NEWLINE = SystemUtils.LINE_SEPARATOR; + public static final String NEWLINE = System.lineSeparator(); - public static final Map PARAM_TYPE_MAP = + public static final Map PARAM_TYPE_MAP = new HashMap(); static { PARAM_TYPE_MAP.put((short)0, "Value"); @@ -123,7 +122,7 @@ public class QueryFormat PARAM_TYPE_MAP.put((short)DataType.GUID.getValue(), "Guid"); } - public static final Map JOIN_TYPE_MAP = + public static final Map JOIN_TYPE_MAP = new HashMap(); static { JOIN_TYPE_MAP.put((short)1, " INNER JOIN "); @@ -131,7 +130,7 @@ public class QueryFormat JOIN_TYPE_MAP.put((short)3, " RIGHT JOIN "); } - public static final Map TYPE_MAP = + public static final Map TYPE_MAP = new HashMap(); static { for(Query.Type type : Query.Type.values()) { diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java index 6b51236..389ca90 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/query/QueryImpl.java @@ -27,7 +27,7 @@ import com.healthmarketscience.jackcess.impl.RowIdImpl; import com.healthmarketscience.jackcess.impl.RowImpl; import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*; import com.healthmarketscience.jackcess.query.Query; -import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; diff --git a/src/main/java/com/healthmarketscience/jackcess/util/RowFilter.java b/src/main/java/com/healthmarketscience/jackcess/util/RowFilter.java index 440d9dd..607bf9a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/RowFilter.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/RowFilter.java @@ -19,17 +19,17 @@ package com.healthmarketscience.jackcess.util; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; import com.healthmarketscience.jackcess.Column; import com.healthmarketscience.jackcess.Row; -import org.apache.commons.lang.ObjectUtils; /** * The RowFilter class encapsulates a filter test for a table row. This can * be used by the {@link #apply(Iterable)} method to create an Iterable over a * table which returns only rows matching some criteria. - * + * * @author Patricia Donaldson, Xerox Corporation * @usage _general_class_ */ @@ -59,22 +59,22 @@ public abstract class RowFilter /** * Creates a filter based on a row pattern. - * + * * @param rowPattern Map from column names to the values to be matched. * A table row will match the target if - * {@code ObjectUtils.equals(rowPattern.get(s), row.get(s))} + * {@code Objects.equals(rowPattern.get(s), row.get(s))} * for all column names in the pattern map. * @return a filter which matches table rows which match the values in the * row pattern */ - public static RowFilter matchPattern(final Map rowPattern) + public static RowFilter matchPattern(final Map rowPattern) { return new RowFilter() { @Override - public boolean matches(Row row) + public boolean matches(Row row) { for(Map.Entry e : rowPattern.entrySet()) { - if(!ObjectUtils.equals(e.getValue(), row.get(e.getKey()))) { + if(!Objects.equals(e.getValue(), row.get(e.getKey()))) { return false; } } @@ -89,18 +89,18 @@ public abstract class RowFilter * @param columnPattern column to be matched * @param valuePattern value to be matched. * A table row will match the target if - * {@code ObjectUtils.equals(valuePattern, row.get(columnPattern.getName()))}. + * {@code Objects.equals(valuePattern, row.get(columnPattern.getName()))}. * @return a filter which matches table rows which match the value in the * row pattern */ - public static RowFilter matchPattern(final Column columnPattern, - final Object valuePattern) + public static RowFilter matchPattern(final Column columnPattern, + final Object valuePattern) { return new RowFilter() { @Override - public boolean matches(Row row) + public boolean matches(Row row) { - return ObjectUtils.equals(valuePattern, columnPattern.getRowValue(row)); + return Objects.equals(valuePattern, columnPattern.getRowValue(row)); } }; } @@ -118,7 +118,7 @@ public abstract class RowFilter { return new RowFilter() { @Override - public boolean matches(Row row) + public boolean matches(Row row) { return !filter.matches(row); } @@ -140,7 +140,7 @@ public abstract class RowFilter public static Iterable apply(RowFilter rowFilter, Iterable iterable) { - return((rowFilter != null) ? rowFilter.apply(iterable) : + return((rowFilter != null) ? rowFilter.apply(iterable) : (Iterable)iterable); } @@ -152,7 +152,7 @@ public abstract class RowFilter { private final Iterable _iterable; - private FilterIterable(Iterable iterable) + private FilterIterable(Iterable iterable) { _iterable = iterable; } @@ -163,7 +163,7 @@ public abstract class RowFilter * iterable, returning only rows for which the {@link RowFilter#matches} * method returns {@code true} */ - public Iterator iterator() + public Iterator iterator() { return new Iterator() { private final Iterator _iter = _iterable.iterator(); diff --git a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java index a564834..b814e93 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/SimpleColumnMatcher.java @@ -19,12 +19,12 @@ package com.healthmarketscience.jackcess.util; import java.io.IOException; import java.util.Arrays; +import java.util.Objects; 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; /** * Simple concrete implementation of ColumnMatcher which tests for equality. @@ -48,7 +48,7 @@ public class SimpleColumnMatcher implements ColumnMatcher { return true; } - if((value1 != null) && (value2 != null) && + if((value1 != null) && (value2 != null) && (value1.getClass() != value2.getClass())) { // the values aren't the same type, try coercing them to "internal" @@ -58,7 +58,7 @@ public class SimpleColumnMatcher implements ColumnMatcher { 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) { // ignored, just go with the original result @@ -73,7 +73,7 @@ public class SimpleColumnMatcher implements ColumnMatcher { */ private static boolean equals(Object o1, Object o2) { - return (ObjectUtils.equals(o1, o2) || + return (Objects.equals(o1, o2) || ((o1 instanceof byte[]) && (o2 instanceof byte[]) && Arrays.equals((byte[])o1, (byte[])o2))); } diff --git a/src/test/java/com/healthmarketscience/jackcess/query/QueryTest.java b/src/test/java/com/healthmarketscience/jackcess/query/QueryTest.java index f7e48f4..f2fe675 100644 --- a/src/test/java/com/healthmarketscience/jackcess/query/QueryTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/query/QueryTest.java @@ -29,9 +29,8 @@ import com.healthmarketscience.jackcess.TestUtil; import com.healthmarketscience.jackcess.impl.query.QueryImpl; import com.healthmarketscience.jackcess.impl.query.QueryImpl.Row; import junit.framework.TestCase; -import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.StringUtils; -import static org.apache.commons.lang.SystemUtils.LINE_SEPARATOR; import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*; import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; @@ -53,22 +52,22 @@ public class QueryTest extends TestCase String expr2 = "Select * from Table2"; UnionQuery query = (UnionQuery)newQuery( - Query.Type.UNION, + Query.Type.UNION, newRow(TABLE_ATTRIBUTE, expr1, null, UNION_PART1), newRow(TABLE_ATTRIBUTE, expr2, null, UNION_PART2)); setFlag(query, 3); assertEquals(multiline("Select * from Table1", - "UNION Select * from Table2;"), + "UNION Select * from Table2;"), query.toSQLString()); setFlag(query, 1); assertEquals(multiline("Select * from Table1", - "UNION ALL Select * from Table2;"), + "UNION ALL Select * from Table2;"), query.toSQLString()); - addRows(query, newRow(ORDERBY_ATTRIBUTE, "Table1.id", + addRows(query, newRow(ORDERBY_ATTRIBUTE, "Table1.id", null, null)); assertEquals(multiline("Select * from Table1", @@ -112,7 +111,7 @@ public class QueryTest extends TestCase public void testUpdateQuery() throws Exception { UpdateQuery query = (UpdateQuery)newQuery( - Query.Type.UPDATE, + Query.Type.UPDATE, newRow(TABLE_ATTRIBUTE, null, "Table1", null), newRow(COLUMN_ATTRIBUTE, "\"some string\"", null, "Table1.id"), newRow(COLUMN_ATTRIBUTE, "42", null, "Table1.col1")); @@ -122,7 +121,7 @@ public class QueryTest extends TestCase "SET Table1.id = \"some string\", Table1.col1 = 42;"), query.toSQLString()); - addRows(query, newRow(WHERE_ATTRIBUTE, "(Table1.col2 < 13)", + addRows(query, newRow(WHERE_ATTRIBUTE, "(Table1.col2 < 13)", null, null)); assertEquals( @@ -135,14 +134,14 @@ public class QueryTest extends TestCase public void testSelectQuery() throws Exception { SelectQuery query = (SelectQuery)newQuery( - Query.Type.SELECT, + Query.Type.SELECT, newRow(TABLE_ATTRIBUTE, null, "Table1", null)); setFlag(query, 1); assertEquals(multiline("SELECT *", "FROM Table1;"), query.toSQLString()); - + doTestColumns(query); doTestSelectFlags(query); doTestParameters(query); @@ -168,7 +167,7 @@ public class QueryTest extends TestCase } addRows(query, newRow(TYPE_ATTRIBUTE, null, -1, null, null)); - + try { query.getTypeRow(); fail("IllegalStateException should have been thrown"); @@ -265,7 +264,7 @@ public class QueryTest extends TestCase newRow(COLUMN_ATTRIBUTE, "54", APPEND_VALUE_FLAG, null, null), newRow(COLUMN_ATTRIBUTE, "'hello'", APPEND_VALUE_FLAG, null, null)); - assertEquals(multiline("INSERT INTO Table2", + assertEquals(multiline("INSERT INTO Table2", "VALUES (54, 'hello');"), query.toSQLString()); query = (AppendQuery)newQuery( @@ -274,7 +273,7 @@ public class QueryTest extends TestCase newRow(COLUMN_ATTRIBUTE, "54", APPEND_VALUE_FLAG, null, "ID"), newRow(COLUMN_ATTRIBUTE, "'hello'", APPEND_VALUE_FLAG, null, "Field 3")); - assertEquals(multiline("INSERT INTO Table2 (ID, [Field 3])", + assertEquals(multiline("INSERT INTO Table2 (ID, [Field 3])", "VALUES (54, 'hello');"), query.toSQLString()); } @@ -291,27 +290,27 @@ public class QueryTest extends TestCase private void doTestSelectFlags(SelectQuery query) throws Exception { setFlag(query, 3); - + assertEquals(multiline("SELECT DISTINCT Table1.id, Table1.col AS [Some.Alias], *", "FROM Table1;"), query.toSQLString()); setFlag(query, 9); - + assertEquals(multiline("SELECT DISTINCTROW Table1.id, Table1.col AS [Some.Alias], *", "FROM Table1;"), query.toSQLString()); setFlag(query, 7); - + assertEquals(multiline("SELECT DISTINCT Table1.id, Table1.col AS [Some.Alias], *", "FROM Table1", "WITH OWNERACCESS OPTION;"), query.toSQLString()); - replaceRows(query, + replaceRows(query, newRow(FLAG_ATTRIBUTE, null, 49, null, "5", null)); - + assertEquals(multiline("SELECT TOP 5 PERCENT Table1.id, Table1.col AS [Some.Alias], *", "FROM Table1;"), query.toSQLString()); @@ -354,7 +353,7 @@ public class QueryTest extends TestCase assertEquals(multiline("SELECT Table1.id, Table1.col AS [Some.Alias]", "FROM Table1, Table2 AS [Another Table], [Select val from Table3].val AS Table3Val;"), - query.toSQLString()); + query.toSQLString()); } private void doTestRemoteDb(SelectQuery query) throws Exception @@ -387,7 +386,7 @@ public class QueryTest extends TestCase assertEquals(multiline("SELECT Table1.id, Table1.col AS [Some.Alias]", "FROM [Select val from Table3].val AS Table3Val, Table1 INNER JOIN Table2 AS [Another Table] ON (Table1.id = [Another Table].id);"), query.toSQLString()); - + addRows(query, newRow(JOIN_ATTRIBUTE, "(Table1.id = Table3Val.id)", 2, "Table1", "Table3Val")); assertEquals(multiline("SELECT Table1.id, Table1.col AS [Some.Alias]", @@ -417,13 +416,13 @@ public class QueryTest extends TestCase private void doTestWhereExpression(SelectQuery query) throws Exception { - addRows(query, newRow(WHERE_ATTRIBUTE, "(Table1.col2 < 13)", + addRows(query, newRow(WHERE_ATTRIBUTE, "(Table1.col2 < 13)", null, null)); assertEquals(multiline("SELECT Table1.id, Table1.col AS [Some.Alias]", "FROM Table1, Table2 AS [Another Table], [Select val from Table3].val AS Table3Val", "WHERE (Table1.col2 < 13);"), - query.toSQLString()); + query.toSQLString()); } private void doTestGroupings(SelectQuery query) throws Exception @@ -435,7 +434,7 @@ public class QueryTest extends TestCase "FROM Table1, Table2 AS [Another Table], [Select val from Table3].val AS Table3Val", "WHERE (Table1.col2 < 13)", "GROUP BY Table1.id, SUM(Table1.val);"), - query.toSQLString()); + query.toSQLString()); } private void doTestHavingExpression(SelectQuery query) throws Exception @@ -447,7 +446,7 @@ public class QueryTest extends TestCase "WHERE (Table1.col2 < 13)", "GROUP BY Table1.id, SUM(Table1.val)", "HAVING (SUM(Table1.val) = 500);"), - query.toSQLString()); + query.toSQLString()); } private void doTestOrderings(SelectQuery query) throws Exception @@ -461,7 +460,7 @@ public class QueryTest extends TestCase "GROUP BY Table1.id, SUM(Table1.val)", "HAVING (SUM(Table1.val) = 500)", "ORDER BY Table1.id, Table2.val DESC;"), - query.toSQLString()); + query.toSQLString()); } public void testComplexJoins() throws Exception @@ -484,7 +483,7 @@ public class QueryTest extends TestCase addJoinRows(query, 1, 2, 1, 2, 1, 1); - + assertEquals(multiline("SELECT *", "FROM Table3, Table4, Table5, Table6, Table7, Table8, Table9, Table10, Table1 INNER JOIN Table2 ON (Table2.f3 = Table1.f3) AND (Table1.f0 = Table2.f0);"), query.toSQLString()); @@ -498,7 +497,7 @@ public class QueryTest extends TestCase } catch(IllegalStateException e) { // success } - + addJoinRows(query, 1, 2, 1, 3, 4, 1, 5, 6, 1, @@ -613,7 +612,7 @@ public class QueryTest extends TestCase private static void setFlag(Query query, Number newFlagNum) { - replaceRows(query, + replaceRows(query, newRow(FLAG_ATTRIBUTE, null, newFlagNum, null, null, null)); } @@ -646,7 +645,7 @@ public class QueryTest extends TestCase private static String multiline(String... strs) { - return StringUtils.join(strs, LINE_SEPARATOR); + return StringUtils.join(strs, System.lineSeparator()); } } diff --git a/src/test/java/com/healthmarketscience/jackcess/util/ExportTest.java b/src/test/java/com/healthmarketscience/jackcess/util/ExportTest.java index 754be06..5ffdbd9 100644 --- a/src/test/java/com/healthmarketscience/jackcess/util/ExportTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/util/ExportTest.java @@ -30,7 +30,6 @@ import com.healthmarketscience.jackcess.Table; import com.healthmarketscience.jackcess.TableBuilder; import com.healthmarketscience.jackcess.impl.JetFormatTest; import junit.framework.TestCase; -import org.apache.commons.lang.SystemUtils; import static com.healthmarketscience.jackcess.TestUtil.*; /** @@ -39,7 +38,7 @@ import static com.healthmarketscience.jackcess.TestUtil.*; */ public class ExportTest extends TestCase { - private static final String NL = SystemUtils.LINE_SEPARATOR; + private static final String NL = System.lineSeparator(); public ExportTest(String name) { @@ -78,7 +77,7 @@ public class ExportTest extends TestCase new ExportUtil.Builder(db, "test") .exportWriter(new BufferedWriter(out)); - String expected = + String expected = "some text||some more,13,13.25,\"61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78\n79 7A 61 62 63 64\",true," + testDate + NL + "\"crazy'data\"\"here\",-345,-3.45E-4,61 62 63 64 65 66 67,true," + NL + "C:\\temp\\some_file.txt,25,0.0,,false," + NL; @@ -86,14 +85,14 @@ public class ExportTest extends TestCase assertEquals(expected, out.toString()); out = new StringWriter(); - + new ExportUtil.Builder(db, "test") .setHeader(true) .setDelimiter("||") .setQuote('\'') .exportWriter(new BufferedWriter(out)); - expected = + expected = "col1||col2||col3||col4||col5||col6" + NL + "'some text||some more'||13||13.25||'61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78\n79 7A 61 62 63 64'||true||" + testDate + NL + "'crazy''data\"here'||-345||-3.45E-4||61 62 63 64 65 66 67||true||" + NL + @@ -117,7 +116,7 @@ public class ExportTest extends TestCase .setFilter(oddFilter) .exportWriter(new BufferedWriter(out)); - expected = + expected = "some text||some more,13,13.25,\"61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78\n79 7A 61 62 63 64\",true," + testDate + NL + "C:\\temp\\some_file.txt,25,0.0,,false," + NL; -- cgit v1.2.3 From ecccae0407e34e62f4afa9c17b7ce186e487327f Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Tue, 27 Nov 2018 21:24:19 +0000 Subject: upgrade poi; upgrade various plugins git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1231 f203690c-595d-4dc9-a70b-905162fa7fd2 --- pom.xml | 59 ++-- .../com/healthmarketscience/jackcess/Cursor.java | 18 +- .../jackcess/CursorBuilder.java | 4 +- .../com/healthmarketscience/jackcess/Database.java | 8 +- .../jackcess/DatabaseBuilder.java | 4 +- .../healthmarketscience/jackcess/IndexCursor.java | 16 +- .../healthmarketscience/jackcess/PropertyMap.java | 8 +- .../jackcess/RelationshipBuilder.java | 2 +- .../java/com/healthmarketscience/jackcess/Row.java | 6 +- .../com/healthmarketscience/jackcess/Table.java | 14 +- .../healthmarketscience/jackcess/TableBuilder.java | 2 +- .../jackcess/expr/TemporalConfig.java | 4 +- .../jackcess/expr/package-info.java | 20 +- .../jackcess/impl/CalculatedColumnUtil.java | 2 +- .../jackcess/impl/CompoundOleUtil.java | 12 +- .../jackcess/impl/CursorImpl.java | 2 +- .../jackcess/impl/IndexData.java | 346 ++++++++++----------- .../healthmarketscience/jackcess/impl/RowImpl.java | 6 +- .../jackcess/impl/TableImpl.java | 4 +- .../jackcess/util/MemFileChannel.java | 4 +- .../healthmarketscience/jackcess/util/OleBlob.java | 12 +- .../jackcess/util/ReadOnlyFileChannel.java | 2 +- src/site/javadoc/taglets.properties | 41 --- .../jackcess/util/OleBlobTest.java | 42 +-- 24 files changed, 301 insertions(+), 337 deletions(-) delete mode 100644 src/site/javadoc/taglets.properties (limited to 'src/test') diff --git a/pom.xml b/pom.xml index a876000..d3d3fcf 100644 --- a/pom.xml +++ b/pom.xml @@ -189,6 +189,35 @@ com.google.code.maven-replacer-plugin replacer + + + with-regex2 + site + + replace + + + + ${project.build.directory}/site/apidocs/**/*.html + + true + + + _general_(method|class|field)_ + <span class="UsageGeneral"><span class="UsageGeneralHeader">General</span>: This $1 is general use.</span> + + + _intermediate_(method|class|field)_ + <span class="UsageIntermediate"><span class="UsageIntermediateHeader">Intermediate</span>: This $1 requires moderate API knowledge.</span> + + + _advanced_(method|class|field)_ + <span class="UsageAdvanced"><span class="UsageAdvancedHeader">Advanced</span>: This $1 is for advanced/internal use.</span> + + + + + @@ -201,7 +230,7 @@ commons-logging commons-logging - 1.1.3 + 1.2 log4j @@ -221,7 +250,7 @@ org.apache.poi poi - 3.9 + 4.0.0 true @@ -250,9 +279,9 @@ maven-javadoc-plugin - -J-DTaglets.ConfigurationFile=${basedir}/src/site/javadoc/taglets.properties -J-DTaglets.NoDefaultConfiguration=true 128m 512 + -accessibility,-missing https://docs.oracle.com/javase/8/docs/api/ http://docs.oracle.com/javaee/5/api/ @@ -267,16 +296,6 @@ Usage: - - - net.sourceforge.taglets.Taglets - - net.sourceforge.taglets - taglets - 2.0.3 - - - true @@ -289,20 +308,6 @@ - - - - true - - - false - - taglets - Taglets - http://maven.geotoolkit.org/ - default - - jackcess-build-site diff --git a/src/main/java/com/healthmarketscience/jackcess/Cursor.java b/src/main/java/com/healthmarketscience/jackcess/Cursor.java index 7260b79..8503623 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Cursor.java +++ b/src/main/java/com/healthmarketscience/jackcess/Cursor.java @@ -31,7 +31,7 @@ import com.healthmarketscience.jackcess.util.IterableBuilder; * of table modification during traversal (although depending on how the table * is traversed, row updates may or may not be seen). Multiple cursors may * traverse the same table simultaneously. - *

+ *
* Basic cursors will generally iterate table data in the order it appears in * the database and searches will require scanning the entire table. * Additional features are available when utilizing an {@link Index} backed @@ -135,7 +135,7 @@ public interface Cursor extends Iterable * Iterator which will iterate through all the rows of this table. Use of * the Iterator follows the same restrictions as a call to * {@link #getNextRow}. - *

+ *
* For more flexible iteration see {@link #newIterable}. * @throws RuntimeIOException if an IOException is thrown by one of the * operations, the actual exception will be contained within @@ -151,7 +151,7 @@ public interface Cursor extends Iterable /** * Delete the current row. - *

+ *
* Note, re-deleting an already deleted row is allowed (it does nothing). * @throws IllegalStateException if the current row is not valid (at * beginning or end of table) @@ -178,7 +178,7 @@ public interface Cursor extends Iterable /** * Moves to the next row in the table and returns it. - * @return The next row in this table (Column name -> Column value), or + * @return The next row in this table (Column name -> Column value), or * {@code null} if no next row is found */ public Row getNextRow() throws IOException; @@ -186,7 +186,7 @@ public interface Cursor extends Iterable /** * Moves to the next row in the table and returns it. * @param columnNames Only column names in this collection will be returned - * @return The next row in this table (Column name -> Column value), or + * @return The next row in this table (Column name -> Column value), or * {@code null} if no next row is found */ public Row getNextRow(Collection columnNames) @@ -194,7 +194,7 @@ public interface Cursor extends Iterable /** * Moves to the previous row in the table and returns it. - * @return The previous row in this table (Column name -> Column value), or + * @return The previous row in this table (Column name -> Column value), or * {@code null} if no previous row is found */ public Row getPreviousRow() throws IOException; @@ -202,7 +202,7 @@ public interface Cursor extends Iterable /** * Moves to the previous row in the table and returns it. * @param columnNames Only column names in this collection will be returned - * @return The previous row in this table (Column name -> Column value), or + * @return The previous row in this table (Column name -> Column value), or * {@code null} if no previous row is found */ public Row getPreviousRow(Collection columnNames) @@ -325,12 +325,12 @@ public interface Cursor extends Iterable public int movePreviousRows(int numRows) throws IOException; /** - * Returns the current row in this cursor (Column name -> Column value). + * Returns the current row in this cursor (Column name -> Column value). */ public Row getCurrentRow() throws IOException; /** - * Returns the current row in this cursor (Column name -> Column value). + * Returns the current row in this cursor (Column name -> Column value). * @param columnNames Only column names in this collection will be returned */ public Row getCurrentRow(Collection columnNames) diff --git a/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java b/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java index 831c78f..3ae058b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java @@ -34,14 +34,14 @@ import com.healthmarketscience.jackcess.util.ColumnMatcher; * Builder style class for constructing a {@link Cursor}. By default, a * cursor is created at the beginning of the table, and any start/end rows are * inclusive. - *

+ *
* Simple example traversal: *

  *   for(Row row : table.newCursor().toCursor()) {
  *     // ... process each row ...
  *   }
  * 
- *

+ *
* Simple example search: *

  *   Row row = CursorBuilder.findRow(table, Collections.singletonMap(col, "foo"));
diff --git a/src/main/java/com/healthmarketscience/jackcess/Database.java b/src/main/java/com/healthmarketscience/jackcess/Database.java
index d853fe8..2006dcf 100644
--- a/src/main/java/com/healthmarketscience/jackcess/Database.java
+++ b/src/main/java/com/healthmarketscience/jackcess/Database.java
@@ -44,11 +44,11 @@ import com.healthmarketscience.jackcess.util.TableIterableBuilder;
  * Database has been opened, you can interact with the data via the relevant
  * {@link Table}.  When a Database instance is no longer useful, it should
  * always be closed ({@link #close}) to avoid corruption.
- * 

+ *
* Database instances (and all the related objects) are not * thread-safe. However, separate Database instances (and their respective * objects) can be used by separate threads without a problem. - *

+ *
* Database instances do not implement any "transactional" support, and * therefore concurrent editing of the same database file by multiple Database * instances (or with outside programs such as MS Access) will generally @@ -240,7 +240,7 @@ public interface Database extends Iterable, Closeable, Flushable /** * Finds all the relationships in the database in non-system tables. - *

+ *
* Warning, this may load all the Tables (metadata, not data) in the * database which could cause memory issues. * @usage _intermediate_method_ @@ -250,7 +250,7 @@ public interface Database extends Iterable
, Closeable, Flushable /** * Finds all the relationships in the database, including system * tables. - *

+ *
* Warning, this may load all the Tables (metadata, not data) in the * database which could cause memory issues. * @usage _intermediate_method_ diff --git a/src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java b/src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java index c5e0252..5aebdd4 100644 --- a/src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java @@ -35,12 +35,12 @@ import com.healthmarketscience.jackcess.util.MemFileChannel; /** * Builder style class for opening/creating a {@link Database}. - *

+ *
* Simple example usage: *

  *   Database db = DatabaseBuilder.open(new File("test.mdb"));
  * 
- *

+ *
* Advanced example usage: *

  *   Database db = new DatabaseBuilder(new File("test.mdb"))
diff --git a/src/main/java/com/healthmarketscience/jackcess/IndexCursor.java b/src/main/java/com/healthmarketscience/jackcess/IndexCursor.java
index 1e4aa34..a48a2ce 100644
--- a/src/main/java/com/healthmarketscience/jackcess/IndexCursor.java
+++ b/src/main/java/com/healthmarketscience/jackcess/IndexCursor.java
@@ -41,9 +41,9 @@ public interface IndexCursor extends Cursor
    * @param entryValues the column values for the index's columns.
    * @return the matching row or {@code null} if a match could not be found.
    */
-  public Row findRowByEntry(Object... entryValues) 
+  public Row findRowByEntry(Object... entryValues)
     throws IOException;
-  
+
   /**
    * Moves to the first row (as defined by the cursor) where the index entries
    * match the given values.  If a match is not found (or an exception is
@@ -56,32 +56,32 @@ public interface IndexCursor extends Cursor
    * @return {@code true} if a valid row was found with the given values,
    *         {@code false} if no row was found
    */
-  public boolean findFirstRowByEntry(Object... entryValues) 
+  public boolean findFirstRowByEntry(Object... entryValues)
     throws IOException;
 
   /**
    * Moves to the first row (as defined by the cursor) where the index entries
-   * are >= the given values.  If a an exception is thrown, the cursor is
+   * are >= the given values.  If a an exception is thrown, the cursor is
    * restored to its previous state.
    *
    * @param entryValues the column values for the index's columns.
    */
-  public void findClosestRowByEntry(Object... entryValues) 
+  public void findClosestRowByEntry(Object... entryValues)
     throws IOException;
 
   /**
    * Returns {@code true} if the current row matches the given index entries.
-   * 
+   *
    * @param entryValues the column values for the index's columns.
    */
-  public boolean currentRowMatchesEntry(Object... entryValues) 
+  public boolean currentRowMatchesEntry(Object... entryValues)
     throws IOException;
 
   /**
    * Convenience method for constructing a new EntryIterableBuilder for this
    * cursor.  An EntryIterableBuilder provides a variety of options for more
    * flexible iteration based on a specific index entry.
-   * 
+   *
    * @param entryValues the column values for the index's columns.
    */
   public EntryIterableBuilder newEntryIterable(Object... entryValues);
diff --git a/src/main/java/com/healthmarketscience/jackcess/PropertyMap.java b/src/main/java/com/healthmarketscience/jackcess/PropertyMap.java
index 516a098..c8c8703 100644
--- a/src/main/java/com/healthmarketscience/jackcess/PropertyMap.java
+++ b/src/main/java/com/healthmarketscience/jackcess/PropertyMap.java
@@ -80,7 +80,7 @@ public interface PropertyMap extends Iterable
    * determine the type of the property based on the name and value (the
    * property names listed above have their types builtin, otherwise the type
    * of the value is used).
-   * 

+ *
* Note, this change will not be persisted until the {@link #save} method * has been called. * @@ -92,7 +92,7 @@ public interface PropertyMap extends Iterable /** * Creates a new (or updates an existing) property in the map. - *

+ *
* Note, this change will not be persisted until the {@link #save} method * has been called. * @@ -102,7 +102,7 @@ public interface PropertyMap extends Iterable /** * Creates a new (or updates an existing) property in the map. - *

+ *
* Note, this change will not be persisted until the {@link #save} method * has been called. * @@ -151,7 +151,7 @@ public interface PropertyMap extends Iterable /** * Sets the new value for this property. - *

+ *
* Note, this change will not be persisted until the {@link * PropertyMap#save} method has been called. */ diff --git a/src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java b/src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java index 3ca3e85..d183de0 100644 --- a/src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java @@ -32,7 +32,7 @@ import com.healthmarketscience.jackcess.impl.RelationshipImpl; * if integrity enforcement is enabled, there must already be a unique index * on the "from" Table for the relevant columns (same requirement as MS * Access). - *

+ *
* Example: *

  *   Relationship rel = new RelationshipBuilder("FromTable", "ToTable")
diff --git a/src/main/java/com/healthmarketscience/jackcess/Row.java b/src/main/java/com/healthmarketscience/jackcess/Row.java
index a599c20..4e43541 100644
--- a/src/main/java/com/healthmarketscience/jackcess/Row.java
+++ b/src/main/java/com/healthmarketscience/jackcess/Row.java
@@ -26,7 +26,7 @@ import com.healthmarketscience.jackcess.util.OleBlob;
 
 
 /**
- * A row of data as column name->value pairs.  Values are strongly typed, and
+ * A row of data as column name->value pairs.  Values are strongly typed, and
  * column names are case sensitive.
  *
  * @author James Ahlborn
@@ -35,7 +35,7 @@ import com.healthmarketscience.jackcess.util.OleBlob;
 public interface Row extends Map
 {
   /**
-   * @return the id of this row 
+   * @return the id of this row
    */
   public RowId getId();
 
@@ -108,7 +108,7 @@ public interface Row extends Map
   /**
    * Convenience method which gets the value for the row with the given name,
    * converting it to an {@link OleBlob} (DataTypes OLE).
-   * 

+ *
* Note, the OleBlob should be closed after use. */ public OleBlob getBlob(String name) throws IOException; diff --git a/src/main/java/com/healthmarketscience/jackcess/Table.java b/src/main/java/com/healthmarketscience/jackcess/Table.java index bbb6885..496f2ea 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Table.java +++ b/src/main/java/com/healthmarketscience/jackcess/Table.java @@ -33,7 +33,7 @@ import com.healthmarketscience.jackcess.util.ErrorHandler; * {@link TableBuilder}. The {@link com.healthmarketscience.jackcess.util.Joiner} utility can be used to traverse * table relationships (e.g. find rows in another table based on a foreign-key * relationship). - *

+ *
* A Table instance is not thread-safe (see {@link Database} for more * thread-safety details). * @@ -160,14 +160,14 @@ public interface Table extends Iterable public Index getForeignKeyIndex(Table otherTable); /** - * Converts a map of columnName -> columnValue to an array of row values + * Converts a map of columnName -> columnValue to an array of row values * appropriate for a call to {@link #addRow(Object...)}. * @usage _general_method_ */ public Object[] asRow(Map rowMap); /** - * Converts a map of columnName -> columnValue to an array of row values + * Converts a map of columnName -> columnValue to an array of row values * appropriate for a call to {@link Cursor#updateCurrentRow(Object...)}. * @usage _general_method_ */ @@ -203,7 +203,7 @@ public interface Table extends Iterable /** * Calls {@link #asRow} on the given row map and passes the result to {@link * #addRow}. - *

+ *
* Note, if this table has an auto-number column, the value generated will be * put back into the given row map. * @return the given row map, which will contain any autonumbers generated @@ -242,7 +242,7 @@ public interface Table extends Iterable /** * Calls {@link #asRow} on the given row maps and passes the results to * {@link #addRows}. - *

+ *
* Note, if this table has an auto-number column, the values generated will * be put back into the appropriate row maps. *

@@ -278,7 +278,7 @@ public interface Table extends Iterable * Iterator which will iterate through all the rows of this table. Use of * the Iterator follows the same restrictions as a call to * {@link #getNextRow}. - *

+ *
* For more advanced iteration, use the {@link #getDefaultCursor default * cursor} directly. * @throws RuntimeIOException if an IOException is thrown by one of the @@ -296,7 +296,7 @@ public interface Table extends Iterable public void reset(); /** - * @return The next row in this table (Column name -> Column value) (uses + * @return The next row in this table (Column name -> Column value) (uses * the {@link #getDefaultCursor default cursor}) * @usage _general_method_ */ diff --git a/src/main/java/com/healthmarketscience/jackcess/TableBuilder.java b/src/main/java/com/healthmarketscience/jackcess/TableBuilder.java index 31aa3a0..eaf6b1a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/TableBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/TableBuilder.java @@ -32,7 +32,7 @@ import com.healthmarketscience.jackcess.impl.TableCreator; /** * Builder style class for constructing a {@link Table}. - *

+ *
* Example: *

  *   Table table = new TableBuilder("Test")
diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java b/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java
index db7806f..919d682 100644
--- a/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java
+++ b/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java
@@ -147,8 +147,8 @@ public class TemporalConfig
   /**
    * Instantiates a new TemporalConfig with the given configuration.  Note
    * that the date/time format variants will be created by concatenating the
-   * relevant date and time formats, separated by a single space, e.g. "
-   * 
, Closeable, Flushable */ public File getFile(); + /** + * Returns the File underlying this Database + */ + public Path getPath(); + /** * @return The names of all of the user tables * @usage _general_method_ diff --git a/src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java b/src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java index 5aebdd4..dce1901 100644 --- a/src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java @@ -20,6 +20,7 @@ import java.io.File; import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.charset.Charset; +import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; @@ -51,10 +52,10 @@ import com.healthmarketscience.jackcess.util.MemFileChannel; * @author James Ahlborn * @usage _general_class_ */ -public class DatabaseBuilder +public class DatabaseBuilder { /** the file name of the mdb to open/create */ - private File _mdbFile; + private Path _mdbFile; /** whether or not to open existing mdb read-only */ private boolean _readOnly; /** whether or not to auto-sync writes to the filesystem */ @@ -77,12 +78,16 @@ public class DatabaseBuilder /** database user-defined (if any) */ private Map _userProps; - + public DatabaseBuilder() { - this(null); + this((Path)null); } public DatabaseBuilder(File mdbFile) { + this(toPath(mdbFile)); + } + + public DatabaseBuilder(Path mdbFile) { _mdbFile = mdbFile; } @@ -93,6 +98,16 @@ public class DatabaseBuilder * @usage _general_method_ */ public DatabaseBuilder setFile(File mdbFile) { + return setPath(toPath(mdbFile)); + } + + /** + * File containing an existing database for {@link #open} or target file for + * new database for {@link #create} (in which case, tf this file already + * exists, it will be overwritten.) + * @usage _general_method_ + */ + public DatabaseBuilder setPath(Path mdbFile) { _mdbFile = mdbFile; return this; } @@ -183,7 +198,7 @@ public class DatabaseBuilder public DatabaseBuilder putDatabaseProperty(String name, Object value) { return putDatabaseProperty(name, null, value); } - + /** * Sets the database property with the given name and type to the given * value. @@ -193,7 +208,7 @@ public class DatabaseBuilder _dbProps = putProperty(_dbProps, name, type, value); return this; } - + /** * Sets the summary database property with the given name to the given * value. Attempts to determine the type of the property (see @@ -203,7 +218,7 @@ public class DatabaseBuilder public DatabaseBuilder putSummaryProperty(String name, Object value) { return putSummaryProperty(name, null, value); } - + /** * Sets the summary database property with the given name and type to * the given value. @@ -223,7 +238,7 @@ public class DatabaseBuilder public DatabaseBuilder putUserDefinedProperty(String name, Object value) { return putUserDefinedProperty(name, null, value); } - + /** * Sets the user-defined database property with the given name and type to * the given value. @@ -257,7 +272,7 @@ public class DatabaseBuilder * Creates a new Database using the configured information. */ public Database create() throws IOException { - Database db = DatabaseImpl.create(_fileFormat, _mdbFile, _channel, _autoSync, + Database db = DatabaseImpl.create(_fileFormat, _mdbFile, _channel, _autoSync, _charset, _timeZone); if(_dbProps != null) { PropertyMap props = db.getDatabaseProperties(); @@ -281,19 +296,19 @@ public class DatabaseBuilder * Open an existing Database. If the existing file is not writeable, the * file will be opened read-only. Auto-syncing is enabled for the returned * Database. - * + * * @param mdbFile File containing the database - * + * * @see DatabaseBuilder for more flexible Database opening * @usage _general_method_ */ public static Database open(File mdbFile) throws IOException { return new DatabaseBuilder(mdbFile).open(); } - + /** * Create a new Database for the given fileFormat - * + * * @param fileFormat version of new database. * @param mdbFile Location to write the new database to. If this file * already exists, it will be overwritten. @@ -301,8 +316,8 @@ public class DatabaseBuilder * @see DatabaseBuilder for more flexible Database creation * @usage _general_method_ */ - public static Database create(Database.FileFormat fileFormat, File mdbFile) - throws IOException + public static Database create(Database.FileFormat fileFormat, File mdbFile) + throws IOException { return new DatabaseBuilder(mdbFile).setFileFormat(fileFormat).create(); } @@ -330,4 +345,8 @@ public class DatabaseBuilder } return cal; } + + private static Path toPath(File file) { + return ((file != null) ? file.toPath() : null); + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index 220d5b0..03e32c8 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -20,7 +20,6 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.io.RandomAccessFile; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; @@ -28,6 +27,10 @@ import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -198,9 +201,11 @@ public class DatabaseImpl implements Database SYSTEM_OBJECT_FLAG | ALT_SYSTEM_OBJECT_FLAG; /** read-only channel access mode */ - public static final String RO_CHANNEL_MODE = "r"; + public static final OpenOption[] RO_CHANNEL_OPTS = + {StandardOpenOption.READ}; /** read/write channel access mode */ - public static final String RW_CHANNEL_MODE = "rw"; + public static final OpenOption[] RW_CHANNEL_OPTS = + {StandardOpenOption.READ, StandardOpenOption.WRITE}; /** Name of the system object that is the parent of all tables */ private static final String SYSTEM_OBJECT_NAME_TABLES = "Tables"; @@ -252,7 +257,7 @@ public class DatabaseImpl implements Database Pattern.compile("[\\p{Cntrl}.!`\\]\\[]"); /** the File of the database */ - private final File _file; + private final Path _file; /** the simple name of the database */ private final String _name; /** Buffer to hold database pages */ @@ -362,20 +367,20 @@ public class DatabaseImpl implements Database * @usage _advanced_method_ */ public static DatabaseImpl open( - File mdbFile, boolean readOnly, FileChannel channel, + Path mdbFile, boolean readOnly, FileChannel channel, boolean autoSync, Charset charset, TimeZone timeZone, CodecProvider provider) throws IOException { boolean closeChannel = false; if(channel == null) { - if(!mdbFile.exists() || !mdbFile.canRead()) { + if(!Files.isReadable(mdbFile)) { throw new FileNotFoundException("given file does not exist: " + mdbFile); } // force read-only for non-writable files - readOnly |= !mdbFile.canWrite(); + readOnly |= !Files.isWritable(mdbFile); // open file channel channel = openChannel(mdbFile, readOnly); @@ -431,7 +436,7 @@ public class DatabaseImpl implements Database * @param timeZone TimeZone to use, if {@code null}, uses default * @usage _advanced_method_ */ - public static DatabaseImpl create(FileFormat fileFormat, File mdbFile, + public static DatabaseImpl create(FileFormat fileFormat, Path mdbFile, FileChannel channel, boolean autoSync, Charset charset, TimeZone timeZone) throws IOException @@ -482,11 +487,11 @@ public class DatabaseImpl implements Database * that name cannot be created, or if some other error occurs * while opening or creating the file */ - static FileChannel openChannel(final File mdbFile, final boolean readOnly) - throws FileNotFoundException + static FileChannel openChannel(Path mdbFile, boolean readOnly) + throws IOException { - final String mode = (readOnly ? RO_CHANNEL_MODE : RW_CHANNEL_MODE); - return new RandomAccessFile(mdbFile, mode).getChannel(); + OpenOption[] opts = (readOnly ? RO_CHANNEL_OPTS : RW_CHANNEL_OPTS); + return FileChannel.open(mdbFile, opts); } /** @@ -508,7 +513,7 @@ public class DatabaseImpl implements Database * @param charset Charset to use, if {@code null}, uses default * @param timeZone TimeZone to use, if {@code null}, uses default */ - protected DatabaseImpl(File file, FileChannel channel, boolean closeChannel, + protected DatabaseImpl(Path file, FileChannel channel, boolean closeChannel, boolean autoSync, FileFormat fileFormat, Charset charset, TimeZone timeZone, CodecProvider provider) throws IOException @@ -536,6 +541,10 @@ public class DatabaseImpl implements Database } public File getFile() { + return ((_file != null) ? _file.toFile() : null); + } + + public Path getPath() { return _file; } @@ -2096,11 +2105,11 @@ public class DatabaseImpl implements Database FILE_FORMAT_DETAILS.put(fileFormat, new FileFormatDetails(emptyFile, format)); } - private static String getName(File file) { + private static String getName(Path file) { if(file == null) { return ""; } - return file.getName(); + return file.getFileName().toString(); } private String withErrorContext(String msg) { diff --git a/src/main/java/com/healthmarketscience/jackcess/util/CustomLinkResolver.java b/src/main/java/com/healthmarketscience/jackcess/util/CustomLinkResolver.java index 7992d71..0463b2d 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/CustomLinkResolver.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/CustomLinkResolver.java @@ -17,10 +17,11 @@ limitations under the License. package com.healthmarketscience.jackcess.util; import java.io.Closeable; -import java.io.File; import java.io.IOException; -import java.io.RandomAccessFile; import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Random; import com.healthmarketscience.jackcess.Database; @@ -64,11 +65,11 @@ public abstract class CustomLinkResolver implements LinkResolver /** temp dbs default to the filesystem, not in memory */ public static final boolean DEFAULT_IN_MEMORY = false; /** temp dbs end up in the system temp dir by default */ - public static final File DEFAULT_TEMP_DIR = null; + public static final Path DEFAULT_TEMP_DIR = null; private final FileFormat _defaultFormat; private final boolean _defaultInMemory; - private final File _defaultTempDir; + private final Path _defaultTempDir; /** * Creates a CustomLinkResolver using the default behavior for creating temp @@ -93,7 +94,7 @@ public abstract class CustomLinkResolver implements LinkResolver * directory) */ protected CustomLinkResolver(FileFormat defaultFormat, boolean defaultInMemory, - File defaultTempDir) + Path defaultTempDir) { _defaultFormat = defaultFormat; _defaultInMemory = defaultInMemory; @@ -108,7 +109,7 @@ public abstract class CustomLinkResolver implements LinkResolver return _defaultInMemory; } - protected File getDefaultTempDirectory() { + protected Path getDefaultTempDirectory() { return _defaultTempDir; } @@ -117,27 +118,27 @@ public abstract class CustomLinkResolver implements LinkResolver *
    *   // attempt to load the linkeeFileName as a custom file
    *   Object customFile = loadCustomFile(linkerDb, linkeeFileName);
-   *   
+   *
    *   if(customFile != null) {
    *     // this is a custom file, create and return relevant temp db
    *     return createTempDb(customFile, getDefaultFormat(), isDefaultInMemory(),
    *                         getDefaultTempDirectory());
    *   }
-   *   
+   *
    *   // not a custmom file, load using the default behavior
    *   return LinkResolver.DEFAULT.resolveLinkedDatabase(linkerDb, linkeeFileName);
    * 
- * + * * @see #loadCustomFile * @see #createTempDb * @see LinkResolver#DEFAULT */ public Database resolveLinkedDatabase(Database linkerDb, String linkeeFileName) - throws IOException + throws IOException { Object customFile = loadCustomFile(linkerDb, linkeeFileName); if(customFile != null) { - return createTempDb(customFile, getDefaultFormat(), isDefaultInMemory(), + return createTempDb(customFile, getDefaultFormat(), isDefaultInMemory(), getDefaultTempDirectory()); } return LinkResolver.DEFAULT.resolveLinkedDatabase(linkerDb, linkeeFileName); @@ -157,28 +158,30 @@ public abstract class CustomLinkResolver implements LinkResolver * * @return the temp db for holding the linked table info */ - protected Database createTempDb(Object customFile, FileFormat format, - boolean inMemory, File tempDir) + protected Database createTempDb(Object customFile, FileFormat format, + boolean inMemory, Path tempDir) throws IOException { - File dbFile = null; + Path dbFile = null; FileChannel channel = null; boolean success = false; try { if(inMemory) { - dbFile = new File(MEM_DB_PREFIX + DB_ID.nextLong() + - format.getFileExtension()); + dbFile = Paths.get(MEM_DB_PREFIX + DB_ID.nextLong() + + format.getFileExtension()); channel = MemFileChannel.newChannel(); } else { - dbFile = File.createTempFile(FILE_DB_PREFIX, format.getFileExtension(), - tempDir); - channel = new RandomAccessFile(dbFile, DatabaseImpl.RW_CHANNEL_MODE) - .getChannel(); + dbFile = ((tempDir != null) ? + Files.createTempFile(tempDir, FILE_DB_PREFIX, + format.getFileExtension()) : + Files.createTempFile(FILE_DB_PREFIX, + format.getFileExtension())); + channel = FileChannel.open(dbFile, DatabaseImpl.RW_CHANNEL_OPTS); } TempDatabaseImpl.initDbChannel(channel, format); - TempDatabaseImpl db = new TempDatabaseImpl(this, customFile, dbFile, + TempDatabaseImpl db = new TempDatabaseImpl(this, customFile, dbFile, channel, format); success = true; return db; @@ -192,9 +195,12 @@ public abstract class CustomLinkResolver implements LinkResolver } } - private static void deleteDbFile(File dbFile) { - if((dbFile != null) && (dbFile.getName().startsWith(FILE_DB_PREFIX))) { - dbFile.delete(); + private static void deleteDbFile(Path dbFile) { + if((dbFile != null) && + dbFile.getFileName().toString().startsWith(FILE_DB_PREFIX)) { + try { + Files.deleteIfExists(dbFile); + } catch(IOException ignores) {} } } @@ -203,7 +209,7 @@ public abstract class CustomLinkResolver implements LinkResolver ByteUtil.closeQuietly((Closeable)customFile); } } - + /** * Called by {@link #resolveLinkedDatabase} to determine whether the * linkeeFileName should be treated as a custom file (thus utiliziing a temp @@ -252,7 +258,7 @@ public abstract class CustomLinkResolver implements LinkResolver private final Object _customFile; protected TempDatabaseImpl(CustomLinkResolver resolver, Object customFile, - File file, FileChannel channel, + Path file, FileChannel channel, FileFormat fileFormat) throws IOException { @@ -262,11 +268,11 @@ public abstract class CustomLinkResolver implements LinkResolver } @Override - protected TableImpl getTable(String name, boolean includeSystemTables) - throws IOException + protected TableImpl getTable(String name, boolean includeSystemTables) + throws IOException { TableImpl table = super.getTable(name, includeSystemTables); - if((table == null) && + if((table == null) && _resolver.loadCustomTable(this, _customFile, name)) { table = super.getTable(name, includeSystemTables); } @@ -278,7 +284,7 @@ public abstract class CustomLinkResolver implements LinkResolver try { super.close(); } finally { - deleteDbFile(getFile()); + deleteDbFile(getPath()); closeCustomFile(_customFile); } } diff --git a/src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java b/src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java index 9ff542f..4420938 100644 --- a/src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java +++ b/src/main/java/com/healthmarketscience/jackcess/util/MemFileChannel.java @@ -29,6 +29,9 @@ import java.nio.channels.FileLock; import java.nio.channels.NonWritableChannelException; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import com.healthmarketscience.jackcess.Database; import com.healthmarketscience.jackcess.DatabaseBuilder; @@ -53,8 +56,13 @@ import com.healthmarketscience.jackcess.impl.DatabaseImpl; * @author James Ahlborn * @usage _advanced_class_ */ -public class MemFileChannel extends FileChannel +public class MemFileChannel extends FileChannel { + /** read-only channel access mode */ + public static final String RO_CHANNEL_MODE = "r"; + /** read/write channel access mode */ + public static final String RW_CHANNEL_MODE = "rw"; + private static final byte[][] EMPTY_DATA = new byte[0][]; // use largest possible Jet "page size" to ensure that reads/writes will @@ -68,10 +76,10 @@ public class MemFileChannel extends FileChannel /** current amount of actual data in the file */ private long _size; /** chunks containing the file data. the length of the chunk array is - always a power of 2 and the chunks are always CHUNK_SIZE. */ + always a power of 2 and the chunks are always CHUNK_SIZE. */ private byte[][] _data; - private MemFileChannel() + private MemFileChannel() { this(0L, 0L, EMPTY_DATA); } @@ -95,7 +103,7 @@ public class MemFileChannel extends FileChannel * affect the original File source. */ public static MemFileChannel newChannel(File file) throws IOException { - return newChannel(file, DatabaseImpl.RW_CHANNEL_MODE); + return newChannel(file, RW_CHANNEL_MODE); } /** @@ -105,25 +113,62 @@ public class MemFileChannel extends FileChannel * modifications to the returned channel will not affect the original * File source. */ - public static MemFileChannel newChannel(File file, String mode) - throws IOException + public static MemFileChannel newChannel(File file, String mode) + throws IOException { FileChannel in = null; try { return newChannel(in = new RandomAccessFile( - file, DatabaseImpl.RO_CHANNEL_MODE).getChannel(), + file, RO_CHANNEL_MODE).getChannel(), + mode); + } finally { + ByteUtil.closeQuietly(in); + } + } + + /** + * Creates a new MemFileChannel containing the contents of the + * given Path with the given mode (for mode details see + * {@link RandomAccessFile#RandomAccessFile(File,String)}). Note, + * modifications to the returned channel will not affect the original + * File source. + */ + public static MemFileChannel newChannel(Path file, OpenOption... opts) + throws IOException + { + FileChannel in = null; + try { + String mode = RO_CHANNEL_MODE; + if(opts != null) { + for(OpenOption opt : opts) { + if(opt == StandardOpenOption.WRITE) { + mode = RW_CHANNEL_MODE; + break; + } + } + } + return newChannel(in = FileChannel.open(file, StandardOpenOption.READ), mode); } finally { ByteUtil.closeQuietly(in); } } + /** + * Creates a new read/write MemFileChannel containing the contents of the + * given Path. Note, modifications to the returned channel will not + * affect the original File source. + */ + public static MemFileChannel newChannel(Path file) throws IOException { + return newChannel(file, DatabaseImpl.RW_CHANNEL_OPTS); + } + /** * Creates a new read/write MemFileChannel containing the contents of the * given InputStream. */ public static MemFileChannel newChannel(InputStream in) throws IOException { - return newChannel(in, DatabaseImpl.RW_CHANNEL_MODE); + return newChannel(in, RW_CHANNEL_MODE); } /** @@ -131,8 +176,8 @@ public class MemFileChannel extends FileChannel * given InputStream with the given mode (for mode details see * {@link RandomAccessFile#RandomAccessFile(File,String)}). */ - public static MemFileChannel newChannel(InputStream in, String mode) - throws IOException + public static MemFileChannel newChannel(InputStream in, String mode) + throws IOException { return newChannel(Channels.newChannel(in), mode); } @@ -141,10 +186,10 @@ public class MemFileChannel extends FileChannel * Creates a new read/write MemFileChannel containing the contents of the * given ReadableByteChannel. */ - public static MemFileChannel newChannel(ReadableByteChannel in) + public static MemFileChannel newChannel(ReadableByteChannel in) throws IOException { - return newChannel(in, DatabaseImpl.RW_CHANNEL_MODE); + return newChannel(in, RW_CHANNEL_MODE); } /** @@ -152,7 +197,7 @@ public class MemFileChannel extends FileChannel * given ReadableByteChannel with the given mode (for mode details see * {@link RandomAccessFile#RandomAccessFile(File,String)}). */ - public static MemFileChannel newChannel(ReadableByteChannel in, String mode) + public static MemFileChannel newChannel(ReadableByteChannel in, String mode) throws IOException { MemFileChannel channel = new MemFileChannel(); @@ -282,7 +327,7 @@ public class MemFileChannel extends FileChannel if(position >= _size) { return 0L; } - + count = Math.min(count, _size - position); int chunkIndex = getChunkIndex(position); @@ -304,7 +349,7 @@ public class MemFileChannel extends FileChannel numBytes += bytesWritten; count -= bytesWritten; } while(src.hasRemaining()); - + ++chunkIndex; chunkOffset = 0; } @@ -360,11 +405,11 @@ public class MemFileChannel extends FileChannel count -= bytesRead; _size = Math.max(_size, position + numBytes); } while(dst.hasRemaining()); - + ++chunkIndex; - chunkOffset = 0; + chunkOffset = 0; } - + return numBytes; } @@ -410,7 +455,7 @@ public class MemFileChannel extends FileChannel private static int getChunkIndex(long pos) { return (int)(pos / CHUNK_SIZE); } - + private static int getChunkOffset(long pos) { return (int)(pos % CHUNK_SIZE); } @@ -418,7 +463,7 @@ public class MemFileChannel extends FileChannel private static int getNumChunks(long size) { return getChunkIndex(size + CHUNK_SIZE - 1); } - + @Override public long write(ByteBuffer[] srcs, int offset, int length) throws IOException @@ -433,7 +478,7 @@ public class MemFileChannel extends FileChannel @Override public long read(ByteBuffer[] dsts, int offset, int length) throws IOException - { + { long numBytes = 0L; for(int i = offset; i < offset + length; ++i) { if(_position >= _size) { @@ -474,7 +519,7 @@ public class MemFileChannel extends FileChannel { super(channel._position, channel._size, channel._data); } - + @Override public int write(ByteBuffer src, long position) throws IOException { throw new NonWritableChannelException(); @@ -491,6 +536,6 @@ public class MemFileChannel extends FileChannel throws IOException { throw new NonWritableChannelException(); - } + } } } diff --git a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java index 025e180..2ce343b 100644 --- a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -51,7 +51,7 @@ import static com.healthmarketscience.jackcess.TestUtil.*; /** * @author Tim McCune */ -public class DatabaseTest extends TestCase +public class DatabaseTest extends TestCase { public DatabaseTest(String name) throws Exception { super(name); @@ -114,7 +114,7 @@ public class DatabaseTest extends TestCase db.close(); } } - + public void testReadDeletedRows() throws Exception { for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.DEL, true)) { Table table = open(testDB).getTable("Table"); @@ -122,11 +122,11 @@ public class DatabaseTest extends TestCase while (table.getNextRow() != null) { rows++; } - assertEquals(2, rows); + assertEquals(2, rows); table.getDatabase().close(); } } - + public void testGetColumns() throws Exception { for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) { @@ -143,9 +143,9 @@ public class DatabaseTest extends TestCase checkColumn(columns, 8, "I", DataType.BOOLEAN); } } - + private static void checkColumn( - List columns, int columnNumber, String name, + List columns, int columnNumber, String name, DataType dataType) throws Exception { @@ -153,7 +153,7 @@ public class DatabaseTest extends TestCase assertEquals(name, column.getName()); assertEquals(dataType, column.getType()); } - + public void testGetNextRow() throws Exception { for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) { final Database db = open(testDB); @@ -183,7 +183,7 @@ public class DatabaseTest extends TestCase db.close(); } } - + public void testDeleteCurrentRow() throws Exception { // make sure correct row is deleted @@ -269,7 +269,7 @@ public class DatabaseTest extends TestCase table.reset(); List rows = RowFilterTest.toList(table); - + Row r1 = rows.remove(7); Row r2 = rows.remove(3); assertEquals(8, rows.size()); @@ -282,10 +282,10 @@ public class DatabaseTest extends TestCase table.deleteRow(r2); table.deleteRow(r1); - assertTable(rows, table); + assertTable(rows, table); } } - + public void testMissingFile() throws Exception { File bogusFile = new File("fooby-dooby.mdb"); assertTrue(!bogusFile.exists()); @@ -326,7 +326,7 @@ public class DatabaseTest extends TestCase } rowNum++; } - + table.getDatabase().close(); } } @@ -508,7 +508,7 @@ public class DatabaseTest extends TestCase db.close(); } - } + } public void testMultiPageTableDef() throws Exception { @@ -580,7 +580,7 @@ public class DatabaseTest extends TestCase db.close(); } - } + } public void testLargeTableDef() throws Exception { @@ -685,8 +685,8 @@ public class DatabaseTest extends TestCase TimeZone tz = TimeZone.getTimeZone("America/New_York"); SimpleDateFormat sdf = DatabaseBuilder.createDateFormat("yyyy-MM-dd"); sdf.getCalendar().setTimeZone(tz); - - List dates = Arrays.asList("1582-10-15", "1582-10-14", + + List dates = Arrays.asList("1582-10-15", "1582-10-14", "1492-01-10", "1392-01-10"); @@ -703,7 +703,7 @@ public class DatabaseTest extends TestCase Date d = sdf.parse(dateStr); table.addRow("row " + dateStr, d); } - + List foundDates = new ArrayList(); for(Row row : table) { foundDates.add(sdf.format(row.getDate("date"))); @@ -741,7 +741,7 @@ public class DatabaseTest extends TestCase sysTables.addAll( Arrays.asList("MSysObjects", "MSysQueries", "MSysACES", "MSysRelationships")); - + if (fileFormat == FileFormat.GENERIC_JET4) { assertNull("file format: " + fileFormat, db.getSystemTable("MSysAccessObjects")); } else if (fileFormat.ordinal() < FileFormat.V2003.ordinal()) { @@ -766,11 +766,11 @@ public class DatabaseTest extends TestCase if(fileFormat.ordinal() >= FileFormat.V2010.ordinal()) { sysTables.add("f_12D7448B56564D8AAE333BCC9B3718E5_Data"); sysTables.add("MSysResources"); - } + } } assertEquals(sysTables, db.getSystemTableNames()); - + assertNotNull(db.getSystemTable("MSysObjects")); assertNotNull(db.getSystemTable("MSysQueries")); assertNotNull(db.getSystemTable("MSysACES")); @@ -782,7 +782,7 @@ public class DatabaseTest extends TestCase assertEquals("MSysObjects", tmd.getName()); assertFalse(tmd.isLinked()); assertTrue(tmd.isSystem()); - + db.close(); } } @@ -839,7 +839,7 @@ public class DatabaseTest extends TestCase "RawData[(12) FF FE 6F 74 68 65 72 20 64 61 74 61]", null); List fixVals = Arrays.asList("RawData[(4) 37 00 00 00]", - "RawData[(4) F3 FF FF FF]", + "RawData[(4) F3 FF FF FF]", "RawData[(4) 02 00 00 00]"); int idx = 0; @@ -891,7 +891,8 @@ public class DatabaseTest extends TestCase Database linkeeDb = db.getLinkedDatabases().get(linkeeDbName); assertNotNull(linkeeDb); assertEquals(linkeeFile, linkeeDb.getFile()); - + assertEquals("linkeeTest.accdb", ((DatabaseImpl)linkeeDb).getName()); + List> expectedRows = createExpectedTable( createExpectedRow( @@ -900,7 +901,7 @@ public class DatabaseTest extends TestCase assertTable(expectedRows, t2); - db.createLinkedTable("FooTable", linkeeDbName, "Table2"); + db.createLinkedTable("FooTable", linkeeDbName, "Table2"); tmd = db.getTableMetaData("FooTable"); assertEquals("FooTable", tmd.getName()); @@ -929,7 +930,7 @@ public class DatabaseTest extends TestCase assertNull(tmd.getLinkedDbName()); Table t1 = tmd.open(db); - + assertFalse(db.isLinkedTable(null)); assertTrue(db.isLinkedTable(t2)); assertTrue(db.isLinkedTable(t3)); @@ -941,21 +942,21 @@ public class DatabaseTest extends TestCase assertTrue(tables.contains(t2)); assertTrue(tables.contains(t3)); assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog())); - + tables = getTables(db.newIterable().setIncludeNormalTables(false)); assertEquals(2, tables.size()); assertFalse(tables.contains(t1)); assertTrue(tables.contains(t2)); assertTrue(tables.contains(t3)); assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog())); - + tables = getTables(db.newIterable().withLocalUserTablesOnly()); assertEquals(1, tables.size()); assertTrue(tables.contains(t1)); assertFalse(tables.contains(t2)); assertFalse(tables.contains(t3)); assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog())); - + tables = getTables(db.newIterable().withSystemTablesOnly()); assertTrue(tables.size() > 5); assertFalse(tables.contains(t1)); @@ -975,7 +976,7 @@ public class DatabaseTest extends TestCase } return tableList; } - + public void testTimeZone() throws Exception { TimeZone tz = TimeZone.getTimeZone("America/New_York"); diff --git a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java index 7680fb3..0bef2e0 100644 --- a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java +++ b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java @@ -86,7 +86,7 @@ public class TestUtil throws Exception { FileChannel channel = (inMem ? MemFileChannel.newChannel( - file, DatabaseImpl.RW_CHANNEL_MODE) + file, MemFileChannel.RW_CHANNEL_MODE) : null); final Database db = new DatabaseBuilder(file).setReadOnly(true) .setAutoSync(getTestAutoSync()).setChannel(channel).open(); diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java index 1ed17b7..36ab9bd 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java @@ -98,7 +98,7 @@ public class JetFormatTest extends TestCase { } SUPPORTED_FILEFORMATS = supported.toArray(new FileFormat[0]); - SUPPORTED_FILEFORMATS_FOR_READ = + SUPPORTED_FILEFORMATS_FOR_READ = supportedForRead.toArray(new FileFormat[0]); } @@ -110,7 +110,7 @@ public class JetFormatTest extends TestCase { private final File dbFile; private final FileFormat expectedFileFormat; - private TestDB(File databaseFile, + private TestDB(File databaseFile, FileFormat expectedDBFileFormat) { dbFile = databaseFile; @@ -119,12 +119,12 @@ public class JetFormatTest extends TestCase { public final File getFile() { return dbFile; } - public final FileFormat getExpectedFileFormat() { - return expectedFileFormat; + public final FileFormat getExpectedFileFormat() { + return expectedFileFormat; } - public final JetFormat getExpectedFormat() { - return DatabaseImpl.getFileFormatDetails(expectedFileFormat).getFormat(); + public final JetFormat getExpectedFormat() { + return DatabaseImpl.getFileFormatDetails(expectedFileFormat).getFormat(); } @Override @@ -141,14 +141,14 @@ public class JetFormatTest extends TestCase { boolean readOnly) { List supportedTestDBs = new ArrayList(); - for (FileFormat fileFormat : + for (FileFormat fileFormat : (readOnly ? SUPPORTED_FILEFORMATS_FOR_READ : SUPPORTED_FILEFORMATS)) { File testFile = getFileForBasename(basename, fileFormat); if(!testFile.exists()) { continue; } - + // verify that the db is the file format expected try { Database db = new DatabaseBuilder(testFile).setReadOnly(true).open(); @@ -170,16 +170,16 @@ public class JetFormatTest extends TestCase { private static File getFileForBasename( Basename basename, FileFormat fileFormat) { - return new File(DIR_TEST_DATA, + return new File(DIR_TEST_DATA, fileFormat.name() + File.separator + - basename + fileFormat.name() + + basename + fileFormat.name() + fileFormat.getFileExtension()); } } - public static final List SUPPORTED_DBS_TEST = + public static final List SUPPORTED_DBS_TEST = TestDB.getSupportedForBasename(Basename.TEST); - public static final List SUPPORTED_DBS_TEST_FOR_READ = + public static final List SUPPORTED_DBS_TEST_FOR_READ = TestDB.getSupportedForBasename(Basename.TEST, true); @@ -193,11 +193,12 @@ public class JetFormatTest extends TestCase { for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) { - final FileChannel channel = DatabaseImpl.openChannel(testDB.dbFile, false); + final FileChannel channel = DatabaseImpl.openChannel( + testDB.dbFile.toPath(), false); try { JetFormat fmtActual = JetFormat.getFormat(channel); - assertEquals("Unexpected JetFormat for dbFile: " + + assertEquals("Unexpected JetFormat for dbFile: " + testDB.dbFile.getAbsolutePath(), testDB.getExpectedFormat(), fmtActual); @@ -221,7 +222,7 @@ public class JetFormatTest extends TestCase { PropertyMap props = db.getUserDefinedProperties(); props.put("foo", "bar"); props.save(); - } + } } catch(Exception e) { failure = e; @@ -268,7 +269,7 @@ public class JetFormatTest extends TestCase { } public void testSqlTypes() throws Exception { - + JetFormat v2000 = JetFormat.VERSION_4; for(DataType dt : DataType.values()) { if(v2000.isSupportedDataType(dt)) { diff --git a/src/test/java/com/healthmarketscience/jackcess/util/CustomLinkResolverTest.java b/src/test/java/com/healthmarketscience/jackcess/util/CustomLinkResolverTest.java index 31a8853..b2dfc06 100644 --- a/src/test/java/com/healthmarketscience/jackcess/util/CustomLinkResolverTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/util/CustomLinkResolverTest.java @@ -16,9 +16,9 @@ limitations under the License. package com.healthmarketscience.jackcess.util; -import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.file.Path; import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.DataType; @@ -55,7 +55,7 @@ public class CustomLinkResolverTest extends TestCase Table t1 = db.getTable("Table1"); assertNotNull(t1); assertNotSame(db, t1.getDatabase()); - + assertTable(createExpectedTable(createExpectedRow("id", 0, "data1", "row0"), createExpectedRow("id", 1, @@ -101,7 +101,7 @@ public class CustomLinkResolverTest extends TestCase Database linkerDb, String linkeeFileName) throws IOException { return (("testFile1.txt".equals(linkeeFileName) || - "testFile2.txt".equals(linkeeFileName)) ? + "testFile2.txt".equals(linkeeFileName)) ? linkeeFileName : null); } @@ -121,7 +121,7 @@ public class CustomLinkResolverTest extends TestCase for(int i = 0; i < 3; ++i) { t.addRow(i, "row" + i); } - + return true; } else if("OtherTable2".equals(tableName)) { @@ -135,7 +135,7 @@ public class CustomLinkResolverTest extends TestCase for(int i = 3; i < 6; ++i) { t.addRow(i, "row" + i); } - + return true; } else if("Table4".equals(tableName)) { @@ -149,7 +149,7 @@ public class CustomLinkResolverTest extends TestCase @Override protected Database createTempDb(Object customFile, FileFormat format, - boolean inMemory, File tempDir) + boolean inMemory, Path tempDir) throws IOException { inMemory = "testFile1.txt".equals(customFile); -- cgit v1.2.3 From abf32c90b101a0c1f76d89e8cdf80cd24e72f6c8 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Tue, 11 Dec 2018 02:07:56 +0000 Subject: initial support for LocalDateTime and Temporal types git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1235 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../com/healthmarketscience/jackcess/DataType.java | 56 ++-- .../com/healthmarketscience/jackcess/Database.java | 36 ++- .../healthmarketscience/jackcess/DateTimeType.java | 31 ++ .../jackcess/InvalidValueException.java | 4 + .../java/com/healthmarketscience/jackcess/Row.java | 7 + .../jackcess/impl/ColumnImpl.java | 356 ++++++++++++++++++--- .../jackcess/impl/DatabaseImpl.java | 46 ++- .../healthmarketscience/jackcess/impl/RowImpl.java | 5 + .../healthmarketscience/jackcess/DatabaseTest.java | 5 +- .../healthmarketscience/jackcess/TableTest.java | 22 +- 10 files changed, 474 insertions(+), 94 deletions(-) create mode 100644 src/main/java/com/healthmarketscience/jackcess/DateTimeType.java (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/DataType.java b/src/main/java/com/healthmarketscience/jackcess/DataType.java index 6850ab6..11483b1 100644 --- a/src/main/java/com/healthmarketscience/jackcess/DataType.java +++ b/src/main/java/com/healthmarketscience/jackcess/DataType.java @@ -24,13 +24,14 @@ import java.sql.Types; import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.time.LocalDateTime; import com.healthmarketscience.jackcess.impl.DatabaseImpl; import com.healthmarketscience.jackcess.impl.JetFormat; /** * Supported access data types. - * + * * @author Tim McCune * @usage _general_class_ */ @@ -87,9 +88,10 @@ public enum DataType { */ DOUBLE((byte) 0x07, Types.DOUBLE, 8), /** - * Corresponds to a java {@link Date}. Accepts a Date, any {@link Number} - * (using {@link Number#longValue}), or {@code null}. Equivalent to SQL - * {@link Types#TIMESTAMP}, {@link Types#DATE}, {@link Types#TIME}. + * Corresponds to a java {@link Date} or {@link LocalDateTime}. Accepts a + * Date, LocalDateTime (or related types), any {@link Number} (using {@link + * Number#longValue}), or {@code null}. Equivalent to SQL {@link + * Types#TIMESTAMP}, {@link Types#DATE}, {@link Types#TIME}. */ SHORT_DATE_TIME((byte) 0x08, Types.TIMESTAMP, 8), /** @@ -104,7 +106,7 @@ public enum DataType { * null}. Equivalent to SQL {@link Types#VARCHAR}, {@link Types#CHAR}. */ TEXT((byte) 0x0A, Types.VARCHAR, null, true, false, 0, - JetFormat.TEXT_FIELD_MAX_LENGTH, JetFormat.TEXT_FIELD_MAX_LENGTH, + JetFormat.TEXT_FIELD_MAX_LENGTH, JetFormat.TEXT_FIELD_MAX_LENGTH, JetFormat.TEXT_FIELD_UNIT_SIZE), /** * Corresponds to a java {@code byte[]} of max length 16777215 bytes. @@ -151,7 +153,7 @@ public enum DataType { * Complex type corresponds to a special {@link #LONG} autonumber field * which is the key for a secondary table which holds the "real" data. */ - COMPLEX_TYPE((byte) 0x12, null, 4), + COMPLEX_TYPE((byte) 0x12, null, 4), /** * Corresponds to a java {@link Long}. Accepts any {@link Number} (using * {@link Number#longValue}), Boolean as 1 or 0, any Object converted to a @@ -206,7 +208,7 @@ public enum DataType { addNewSqlType("TIME_WITH_TIMEZONE", SHORT_DATE_TIME, null); addNewSqlType("TIMESTAMP_WITH_TIMEZONE", SHORT_DATE_TIME, null); } - + private static Map DATA_TYPES = new HashMap(); static { for (DataType type : DataType.values()) { @@ -249,11 +251,11 @@ public enum DataType { private final int _maxPrecision; /** the number of bytes per "unit" for this data type */ private final int _unitSize; - + private DataType(byte value) { this(value, null, null); } - + private DataType(byte value, Integer sqlType, Integer fixedSize) { this(value, sqlType, fixedSize, false, false, 0, 0, 0, 1); } @@ -269,7 +271,7 @@ public enum DataType { minSize, defaultSize, maxSize, false, 0, 0, 0, 0, 0, 0, unitSize); } - + private DataType(byte value, Integer sqlType, Integer fixedSize, boolean variableLength, boolean longValue, @@ -301,11 +303,11 @@ public enum DataType { _maxPrecision = maxPrecision; _unitSize = unitSize; } - + public byte getValue() { return _value; } - + public boolean isVariableLength() { return _variableLength; } @@ -315,7 +317,7 @@ public enum DataType { // e.g. NUMERIC return (isVariableLength() && (getMinSize() != getMaxSize())); } - + public boolean isLongValue() { return _longValue; } @@ -327,7 +329,7 @@ public enum DataType { public int getFixedSize() { return getFixedSize(null); } - + public int getFixedSize(Short colLength) { if(_fixedSize != null) { if(colLength != null) { @@ -338,7 +340,7 @@ public enum DataType { if(colLength != null) { return colLength; } - throw new IllegalArgumentException("Unexpected fixed length column " + + throw new IllegalArgumentException("Unexpected fixed length column " + this); } @@ -353,7 +355,7 @@ public enum DataType { public int getMaxSize() { return _maxSize; } - + public int getSQLType() throws SQLException { if (_sqlType != null) { return _sqlType; @@ -368,19 +370,19 @@ public enum DataType { public int getDefaultScale() { return _defaultScale; } - + public int getMaxScale() { return _maxScale; } - + public int getMinPrecision() { return _minPrecision; } - + public int getDefaultPrecision() { return _defaultPrecision; } - + public int getMaxPrecision() { return _maxPrecision; } @@ -414,7 +416,7 @@ public enum DataType { private static boolean isWithinRange(int value, int minValue, int maxValue) { return((value >= minValue) && (value <= maxValue)); } - + public int toValidSize(int size) { return toValidRange(size, getMinSize(), getMaxSize()); } @@ -442,12 +444,12 @@ public enum DataType { public boolean isUnsupported() { return((this == UNSUPPORTED_FIXEDLEN) || (this == UNSUPPORTED_VARLEN)); } - + private static int toValidRange(int value, int minValue, int maxValue) { return((value > maxValue) ? maxValue : ((value < minValue) ? minValue : value)); } - + public static DataType fromByte(byte b) throws IOException { DataType rtn = DATA_TYPES.get(b); if (rtn != null) { @@ -455,13 +457,13 @@ public enum DataType { } throw new IOException("Unrecognized data type: " + b); } - + public static DataType fromSQLType(int sqlType) throws SQLException { return fromSQLType(sqlType, 0, null); } - + public static DataType fromSQLType(int sqlType, int lengthInUnits) throws SQLException { @@ -504,7 +506,7 @@ public enum DataType { rtn = altRtn; } } - + return rtn; } @@ -512,7 +514,7 @@ public enum DataType { * Adds mappings for a sql type which was added after jdk 1.5 (using * reflection). */ - private static void addNewSqlType(String typeName, DataType type, + private static void addNewSqlType(String typeName, DataType type, DataType altType) { try { diff --git a/src/main/java/com/healthmarketscience/jackcess/Database.java b/src/main/java/com/healthmarketscience/jackcess/Database.java index 6a7bcc4..3a7b65a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Database.java +++ b/src/main/java/com/healthmarketscience/jackcess/Database.java @@ -22,6 +22,7 @@ import java.io.Flushable; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Path; +import java.time.ZoneId; import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.List; @@ -207,6 +208,7 @@ public interface Database extends Iterable
, Closeable, Flushable * database while an Iterator is in use. * @usage _general_method_ */ + @Override public Iterator
iterator(); /** @@ -325,6 +327,7 @@ public interface Database extends Iterable
, Closeable, Flushable * databases) to disk. * @usage _general_method_ */ + @Override public void flush() throws IOException; /** @@ -335,6 +338,7 @@ public interface Database extends Iterable
, Closeable, Flushable * OutputStream or jdbc Connection). * @usage _general_method_ */ + @Override public void close() throws IOException; /** @@ -383,17 +387,33 @@ public interface Database extends Iterable
, Closeable, Flushable public boolean isLinkedTable(Table table) throws IOException; /** - * Gets currently configured TimeZone (always non-{@code null}). + * Gets currently configured TimeZone (always non-{@code null} and aligned + * with the ZoneId). * @usage _intermediate_method_ */ public TimeZone getTimeZone(); /** - * Sets a new TimeZone. If {@code null}, resets to the default value. + * Sets a new TimeZone. If {@code null}, resets to the default value. Note + * that setting the TimeZone will alter the ZoneId as well. * @usage _intermediate_method_ */ public void setTimeZone(TimeZone newTimeZone); + /** + * Gets currently configured ZoneId (always non-{@code null} and aligned + * with the TimeZone). + * @usage _intermediate_method_ + */ + public ZoneId getZoneId(); + + /** + * Sets a new ZoneId. If {@code null}, resets to the default value. Note + * that setting the ZoneId will alter the TimeZone as well. + * @usage _intermediate_method_ + */ + public void setZoneId(ZoneId newZoneId); + /** * Gets currently configured Charset (always non-{@code null}). * @usage _intermediate_method_ @@ -498,4 +518,16 @@ public interface Database extends Iterable
, Closeable, Flushable * Returns the EvalConfig for configuring expression evaluation. */ public EvalConfig getEvalConfig(); + + /** + * Gets the currently configured DateTimeType. + * @usage _general_method_ + */ + public DateTimeType getDateTimeType(); + + /** + * Sets the DateTimeType. If {@code null}, resets to the default value. + * @usage _general_method_ + */ + public void setDateTimeType(DateTimeType dateTimeType); } diff --git a/src/main/java/com/healthmarketscience/jackcess/DateTimeType.java b/src/main/java/com/healthmarketscience/jackcess/DateTimeType.java new file mode 100644 index 0000000..7f5cdb1 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/DateTimeType.java @@ -0,0 +1,31 @@ +/* +Copyright (c) 2018 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; + +/** + * Enum for selecting how a Database returns date/time types. + * + * @author James Ahlborn + */ +public enum DateTimeType +{ + /** use legacy {@link java.util.Date} objects. To maintain backwards + compatibility, this is the default type. */ + DATE, + /** use jdk8+ {@link java.time.LocalDateTime} objects */ + LOCAL_DATE_TIME; +} diff --git a/src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java b/src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java index adffc0f..2e161d2 100644 --- a/src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java +++ b/src/main/java/com/healthmarketscience/jackcess/InvalidValueException.java @@ -29,4 +29,8 @@ public class InvalidValueException extends JackcessException public InvalidValueException(String msg) { super(msg); } + + public InvalidValueException(String msg, Throwable cause) { + super(msg, cause); + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/Row.java b/src/main/java/com/healthmarketscience/jackcess/Row.java index 4e43541..6917628 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Row.java +++ b/src/main/java/com/healthmarketscience/jackcess/Row.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.Date; import java.util.Map; import java.math.BigDecimal; +import java.time.LocalDateTime; import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; import com.healthmarketscience.jackcess.util.OleBlob; @@ -93,6 +94,12 @@ public interface Row extends Map */ public Date getDate(String name); + /** + * Convenience method which gets the value for the row with the given name, + * casting it to a LocalDateTime (DataType SHORT_DATE_TIME). + */ + public LocalDateTime getLocalDateTime(String name); + /** * Convenience method which gets the value for the row with the given name, * casting it to a byte[] (DataTypes BINARY, OLE). diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index 8fa1906..f5b7d5b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -32,11 +32,23 @@ import java.nio.charset.Charset; import java.sql.Blob; import java.sql.Clob; import java.sql.SQLException; +import java.time.DateTimeException; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalQueries; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; +import java.util.TimeZone; import java.util.UUID; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -44,6 +56,7 @@ import java.util.regex.Pattern; import com.healthmarketscience.jackcess.Column; import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.DateTimeType; import com.healthmarketscience.jackcess.InvalidValueException; import com.healthmarketscience.jackcess.PropertyMap; import com.healthmarketscience.jackcess.Table; @@ -79,8 +92,9 @@ public class ColumnImpl implements Column, Comparable { /** * Access stores numeric dates in days. Java stores them in milliseconds. */ - private static final long MILLISECONDS_PER_DAY = - (24L * 60L * 60L * 1000L); + private static final long MILLISECONDS_PER_DAY = (24L * 60L * 60L * 1000L); + private static final long SECONDS_PER_DAY = (24L * 60L * 60L); + private static final long NANOS_PER_SECOND = 1_000_000_000L; /** * Access starts counting dates at Dec 30, 1899 (note, this strange date @@ -91,6 +105,16 @@ public class ColumnImpl implements Column, Comparable { static final long MILLIS_BETWEEN_EPOCH_AND_1900 = 25569L * MILLISECONDS_PER_DAY; + static final LocalDate BASE_LD = LocalDate.of(1899, 12, 30); + static final LocalTime BASE_LT = LocalTime.of(0, 0); + static final LocalDateTime BASE_LDT = LocalDateTime.of(BASE_LD, BASE_LT); + + private static final DateTimeFactory DEF_DATE_TIME_FACTORY = + new DefaultDateTimeFactory(); + + private static final DateTimeFactory LDT_DATE_TIME_FACTORY = + new LDTDateTimeFactory(); + /** * mask for the fixed len bit * @usage _advanced_field_ @@ -455,8 +479,16 @@ public class ColumnImpl implements Column, Comparable { return getDatabase().getCharset(); } - protected Calendar getCalendar() { - return getDatabase().getCalendar(); + protected TimeZone getTimeZone() { + return getDatabase().getTimeZone(); + } + + protected ZoneId getZoneId() { + return getDatabase().getZoneId(); + } + + protected DateTimeFactory getDateTimeFactory() { + return getDatabase().getDateTimeFactory(); } public boolean isAppendOnly() { @@ -881,45 +913,43 @@ public class ColumnImpl implements Column, Comparable { /** * Decodes a date value. */ - private Date readDateValue(ByteBuffer buffer) - { - // seems access stores dates in the local timezone. guess you just hope - // you read it in the same timezone in which it was written! + private Object readDateValue(ByteBuffer buffer) { long dateBits = buffer.getLong(); - long time = fromDateDouble(Double.longBitsToDouble(dateBits)); - return new DateExt(time, dateBits); + return getDateTimeFactory().fromDateBits(this, dateBits); } /** * Returns a java long time value converted from an access date double. * @usage _advanced_method_ */ - public long fromDateDouble(double value) - { - return fromDateDouble(value, getCalendar()); + public long fromDateDouble(double value) { + return fromDateDouble(value, getTimeZone()); } /** * 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()); + public static long fromDateDouble(double value, DatabaseImpl db) { + return fromDateDouble(value, db.getTimeZone()); } /** * Returns a java long time value converted from an access date double. * @usage _advanced_method_ */ - public static long fromDateDouble(double value, Calendar c) - { + @Deprecated + public static long fromDateDouble(double value, Calendar c) { + // FIXME, remove me + return fromDateDouble(value, c.getTimeZone()); + } + + public static long fromDateDouble(double value, TimeZone tz) { long localTime = fromLocalDateDouble(value); - return localTime - getFromLocalTimeZoneOffset(localTime, c); + return localTime - getFromLocalTimeZoneOffset(localTime, tz); } - static long fromLocalDateDouble(double value) - { + public static long fromLocalDateDouble(double value) { long datePart = ((long)value) * MILLISECONDS_PER_DAY; // the fractional part of the double represents the time. it is always @@ -927,29 +957,49 @@ public class ColumnImpl implements Column, Comparable { // _not_ the time distance from zero (as one would expect with "normal" // numbers). therefore, we need to do a little number logic to convert // the absolute time fraction into a normal distance from zero number. - long timePart = Math.round((Math.abs(value) % 1.0) * - (double)MILLISECONDS_PER_DAY); + long timePart = Math.round((Math.abs(value) % 1.0d) * + MILLISECONDS_PER_DAY); long time = datePart + timePart; - time -= MILLIS_BETWEEN_EPOCH_AND_1900; - return time; + return time - MILLIS_BETWEEN_EPOCH_AND_1900; } + public static LocalDateTime ldtFromLocalDateDouble(double value) { + Duration dateTimeOffset = durationFromLocalDateDouble1900(value); + return BASE_LDT.plus(dateTimeOffset); + } + + private static Duration durationFromLocalDateDouble1900(double value) { + long dateSeconds = ((long)value) * SECONDS_PER_DAY; + + // the fractional part of the double represents the time. it is always + // a positive fraction of the day (even if the double is negative), + // _not_ the time distance from zero (as one would expect with "normal" + // numbers). therefore, we need to do a little number logic to convert + // the absolute time fraction into a normal distance from zero number. + + double secondsDouble = (Math.abs(value) % 1.0d) * SECONDS_PER_DAY; + long timeSeconds = (long)secondsDouble; + long timeNanos = Math.round((secondsDouble % 1.0d) * NANOS_PER_SECOND); + + return Duration.ofSeconds(dateSeconds + timeSeconds, timeNanos); + } + + + /** * Writes a date value. */ private void writeDateValue(ByteBuffer buffer, Object value) + throws InvalidValueException { if(value == null) { buffer.putDouble(0d); } else if(value instanceof DateExt) { - // this is a Date value previously read from readDateValue(). use the // original bits to store the value so we don't lose any precision buffer.putLong(((DateExt)value).getDateBits()); - } else { - buffer.putDouble(toDateDouble(value)); } } @@ -960,8 +1010,13 @@ public class ColumnImpl implements Column, Comparable { * @usage _advanced_method_ */ public double toDateDouble(Object value) + throws InvalidValueException { - return toDateDouble(value, getCalendar()); + try { + return toDateDouble(value, getTimeZone(), getZoneId()); + } catch(IllegalArgumentException iae) { + throw new InvalidValueException(withErrorContext(iae.getMessage()), iae); + } } /** @@ -971,25 +1026,100 @@ public class ColumnImpl implements Column, Comparable { */ public static double toDateDouble(Object value, DatabaseImpl db) { - return toDateDouble(value, db.getCalendar()); + return toDateDouble(value, db.getTimeZone(), db.getZoneId()); } /** - * Returns an access date double converted from a java Date/Calendar/Number - * time value. + * Returns an access date double converted from a java + * Date/Calendar/Number/Temporal time value. * @usage _advanced_method_ */ - public static double toDateDouble(Object value, Calendar c) + @Deprecated + public static double toDateDouble(Object value, Calendar c) { + // FIXME remove me + return toDateDouble(value, c.getTimeZone()); + } + + public static double toDateDouble(Object value, TimeZone tz) { + return toDateDouble(value, tz, null); + } + + /** + * Returns an access date double converted from a java + * Date/Calendar/Number/Temporal time value. + * @usage _advanced_method_ + */ + public static double toDateDouble(Object value, TimeZone tz, ZoneId zoneId) + { + if(value instanceof TemporalAccessor) { + return toDateDouble(toLocalDateTime((Temporal)value, tz, zoneId)); + } + // 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, c); + time += getToLocalTimeZoneOffset(time, tz); return toLocalDateDouble(time); } - static double toLocalDateDouble(long time) - { + private static LocalDateTime toLocalDateTime( + TemporalAccessor value, TimeZone tz, ZoneId zoneId) { + + // handle some common Temporal types + if(value instanceof LocalDateTime) { + return (LocalDateTime)value; + } + if(value instanceof ZonedDateTime) { + // if the temporal value has a timezone, convert it to this db's timezone + return ((ZonedDateTime)value).withZoneSameInstant( + getZoneId(tz, zoneId)).toLocalDateTime(); + } + if(value instanceof Instant) { + return LocalDateTime.ofInstant((Instant)value, getZoneId(tz, zoneId)); + } + if(value instanceof LocalDate) { + return ((LocalDate)value).atTime(BASE_LT); + } + if(value instanceof LocalTime) { + return ((LocalTime)value).atDate(BASE_LD); + } + + // generic handling for many other Temporal types + try { + + LocalDate ld = value.query(TemporalQueries.localDate()); + if(ld == null) { + ld = BASE_LD; + } + LocalTime lt = value.query(TemporalQueries.localTime()); + if(lt == null) { + lt = BASE_LT; + } + ZoneId zone = value.query(TemporalQueries.zone()); + if(zone != null) { + // the Temporal has a zone, see if it is the right zone. if not, + // adjust it + zoneId = getZoneId(tz, zoneId); + if(!zoneId.equals(zone)) { + return ZonedDateTime.of(ld, lt, zone).withZoneSameInstant(zoneId) + .toLocalDateTime(); + } + } + + return LocalDateTime.of(ld, lt); + + } catch(DateTimeException | ArithmeticException e) { + throw new IllegalArgumentException( + "Unsupported temporal type " + value.getClass(), e); + } + } + + private static ZoneId getZoneId(TimeZone tz, ZoneId zoneId) { + return ((zoneId != null) ? zoneId : tz.toZoneId()); + } + + static double toLocalDateDouble(long time) { time += MILLIS_BETWEEN_EPOCH_AND_1900; if(time < 0L) { @@ -1003,11 +1133,36 @@ public class ColumnImpl implements Column, Comparable { return time / (double)MILLISECONDS_PER_DAY; } + public static double toDateDouble(LocalDateTime ldt) { + Duration dateTimeOffset = Duration.between(BASE_LDT, ldt); + return toLocalDateDouble1900(dateTimeOffset); + } + + private static double toLocalDateDouble1900(Duration time) { + long dateTimeSeconds = time.getSeconds(); + long timeSeconds = dateTimeSeconds % SECONDS_PER_DAY; + if(timeSeconds < 0) { + timeSeconds += SECONDS_PER_DAY; + } + long dateSeconds = dateTimeSeconds - timeSeconds; + long timeNanos = time.getNano(); + + double timeDouble = ((((double)timeNanos / NANOS_PER_SECOND) + timeSeconds) + / SECONDS_PER_DAY); + + double dateDouble = ((double)dateSeconds / SECONDS_PER_DAY); + + if(dateSeconds < 0) { + timeDouble = -timeDouble; + } + + return dateDouble + timeDouble; + } + /** * @return an appropriate Date long value for the given object */ - private static long toDateLong(Object value) - { + private static long toDateLong(Object value) { return ((value instanceof Date) ? ((Date)value).getTime() : ((value instanceof Calendar) ? @@ -1019,24 +1174,19 @@ public class ColumnImpl implements Column, Comparable { * Gets the timezone offset from UTC to local time for the given time * (including DST). */ - private static long getToLocalTimeZoneOffset(long time, Calendar c) - { - c.setTimeInMillis(time); - return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); + private static long getToLocalTimeZoneOffset(long time, TimeZone tz) { + return tz.getOffset(time); } /** * Gets the timezone offset from local time to UTC for the given time * (including DST). */ - private static long getFromLocalTimeZoneOffset(long time, Calendar c) - { + private static long getFromLocalTimeZoneOffset(long time, TimeZone tz) { // getting from local time back to UTC is a little wonky (and not - // guaranteed to get you back to where you started) - c.setTimeInMillis(time); - // apply the zone offset first to get us closer to the original time - c.setTimeInMillis(time - c.get(Calendar.ZONE_OFFSET)); - return ((long)c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET)); + // guaranteed to get you back to where you started). apply the zone + // offset first to get us closer to the original time + return tz.getOffset(time - tz.getRawOffset()); } /** @@ -1990,8 +2140,7 @@ public class ColumnImpl implements Column, Comparable { return ((value instanceof Double) ? value : toNumber(value, db).doubleValue()); case SHORT_DATE_TIME: - return ((value instanceof DateExt) ? value : - new Date(toDateLong(value))); + return db.getDateTimeFactory().toInternalValue(db, value); case TEXT: case MEMO: case GUID: @@ -2011,6 +2160,11 @@ public class ColumnImpl implements Column, Comparable { } } + static DateTimeFactory getDateTimeFactory(DateTimeType type) { + return ((type == DateTimeType.LOCAL_DATE_TIME) ? + LDT_DATE_TIME_FACTORY : DEF_DATE_TIME_FACTORY); + } + String withErrorContext(String msg) { return withErrorContext(msg, getDatabase(), getTable().getName(), getName()); } @@ -2028,8 +2182,10 @@ public class ColumnImpl implements Column, Comparable { /** * Date subclass which stashes the original date bits, in case we attempt to - * re-write the value (will not lose precision). + * re-write the value (will not lose precision). Also, this implementation + * is immutable. */ + @SuppressWarnings("deprecation") private static final class DateExt extends Date { private static final long serialVersionUID = 0L; @@ -2046,6 +2202,41 @@ public class ColumnImpl implements Column, Comparable { return _dateBits; } + @Override + public void setDate(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public void setHours(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMinutes(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public void setMonth(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public void setSeconds(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public void setYear(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public void setTime(long time) { + throw new UnsupportedOperationException(); + } + private Object writeReplace() throws ObjectStreamException { // if we are going to serialize this Date, convert it back to a normal // Date (in case it is restored outside of the context of jackcess) @@ -2450,4 +2641,65 @@ public class ColumnImpl implements Column, Comparable { sb.append("allowZeroLength=false"); } } + + /** + * Factory which handles date/time values appropriately for a DateTimeType. + */ + static abstract class DateTimeFactory + { + public abstract DateTimeType getType(); + + public abstract Object fromDateBits(ColumnImpl col, long dateBits); + + public abstract Object toInternalValue(DatabaseImpl db, Object value); + } + + /** + * Factory impl for legacy Date handling. + */ + static final class DefaultDateTimeFactory extends DateTimeFactory + { + @Override + public DateTimeType getType() { + return DateTimeType.DATE; + } + + @Override + public Object fromDateBits(ColumnImpl col, long dateBits) { + long time = col.fromDateDouble( + Double.longBitsToDouble(dateBits)); + return new DateExt(time, dateBits); + } + + @Override + public Object toInternalValue(DatabaseImpl db, Object value) { + return ((value instanceof Date) ? value : + new Date(toDateLong(value))); + } + } + + /** + * Factory impl for LocalDateTime handling. + */ + static final class LDTDateTimeFactory extends DateTimeFactory + { + @Override + public DateTimeType getType() { + return DateTimeType.LOCAL_DATE_TIME; + } + + @Override + public Object fromDateBits(ColumnImpl col, long dateBits) { + return ldtFromLocalDateDouble(Double.longBitsToDouble(dateBits)); + } + + @Override + public Object toInternalValue(DatabaseImpl db, Object value) { + if(value instanceof TemporalAccessor) { + return toLocalDateTime((TemporalAccessor)value, null, db.getZoneId()); + } + Instant inst = Instant.ofEpochMilli(toDateLong(value)); + return LocalDateTime.ofInstant(inst, db.getZoneId()); + } + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index 03e32c8..f132a99 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -32,6 +32,7 @@ import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.text.SimpleDateFormat; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -56,6 +57,7 @@ import com.healthmarketscience.jackcess.CursorBuilder; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.Database; import com.healthmarketscience.jackcess.DatabaseBuilder; +import com.healthmarketscience.jackcess.DateTimeType; import com.healthmarketscience.jackcess.Index; import com.healthmarketscience.jackcess.IndexBuilder; import com.healthmarketscience.jackcess.IndexCursor; @@ -303,6 +305,8 @@ public class DatabaseImpl implements Database private Charset _charset; /** timezone to use when handling dates */ private TimeZone _timeZone; + /** zoneId to use when handling dates */ + private ZoneId _zoneId; /** language sort order to be used for textual columns */ private ColumnImpl.SortOrder _defaultSortOrder; /** default code page to be used for textual columns (in some dbs) */ @@ -342,6 +346,9 @@ public class DatabaseImpl implements Database private Calendar _calendar; /** shared context for evaluating expressions */ private DBEvalContext _evalCtx; + /** factory for the appropriate date/time type */ + private ColumnImpl.DateTimeFactory _dtf = + ColumnImpl.getDateTimeFactory(DateTimeType.DATE); /** * Open an existing Database. If the existing file is not writeable or the @@ -644,15 +651,38 @@ public class DatabaseImpl implements Database (_linkedDbs.get(linkedDbName) == table.getDatabase())); } + @Override public TimeZone getTimeZone() { return _timeZone; } + @Override public void setTimeZone(TimeZone newTimeZone) { - if(newTimeZone == null) { + setZoneInfo(newTimeZone, null); + } + + @Override + public ZoneId getZoneId() { + return _zoneId; + } + + public void setZoneId(ZoneId newZoneId) { + setZoneInfo(null, newZoneId); + } + + private void setZoneInfo(TimeZone newTimeZone, ZoneId newZoneId) { + if(newTimeZone != null) { + newZoneId = newTimeZone.toZoneId(); + } else if(newZoneId != null) { + newTimeZone = TimeZone.getTimeZone(newZoneId); + } else { newTimeZone = getDefaultTimeZone(); + newZoneId = newTimeZone.toZoneId(); } + _timeZone = newTimeZone; + _zoneId = newZoneId; + // clear cached calendar(s) when timezone is changed _calendar = null; if(_evalCtx != null) { @@ -660,6 +690,20 @@ public class DatabaseImpl implements Database } } + @Override + public DateTimeType getDateTimeType() { + return _dtf.getType(); + } + + @Override + public void setDateTimeType(DateTimeType dateTimeType) { + _dtf = ColumnImpl.getDateTimeFactory(dateTimeType); + } + + protected ColumnImpl.DateTimeFactory getDateTimeFactory() { + return _dtf; + } + public Charset getCharset() { return _charset; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java index 66d9c80..0e6fe6e 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.util.LinkedHashMap; import java.util.Date; import java.math.BigDecimal; +import java.time.LocalDateTime; import com.healthmarketscience.jackcess.Row; import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; @@ -94,6 +95,10 @@ public class RowImpl extends LinkedHashMap implements Row return (Date)get(name); } + public LocalDateTime getLocalDateTime(String name) { + return (LocalDateTime)get(name); + } + public byte[] getBytes(String name) { return (byte[])get(name); } diff --git a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java index 2ce343b..de6bd94 100644 --- a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -990,7 +991,9 @@ public class DatabaseTest extends TestCase { ColumnImpl col = new ColumnImpl(null, null, DataType.SHORT_DATE_TIME, 0, 0, 0) { @Override - protected Calendar getCalendar() { return Calendar.getInstance(tz); } + protected TimeZone getTimeZone() { return tz; } + @Override + protected ZoneId getZoneId() { return null; } }; SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd"); diff --git a/src/test/java/com/healthmarketscience/jackcess/TableTest.java b/src/test/java/com/healthmarketscience/jackcess/TableTest.java index 339ba39..3bc2dbd 100644 --- a/src/test/java/com/healthmarketscience/jackcess/TableTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/TableTest.java @@ -21,8 +21,8 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; -import java.util.Calendar; import java.util.List; +import java.util.TimeZone; import com.healthmarketscience.jackcess.impl.ColumnImpl; import com.healthmarketscience.jackcess.impl.JetFormat; @@ -40,8 +40,8 @@ public class TableTest extends TestCase { private TestTable _testTable; private int _varLenIdx; private int _fixedOffset; - - + + public TableTest(String name) { super(name); } @@ -52,14 +52,14 @@ public class TableTest extends TestCase { _varLenIdx = 0; _fixedOffset = 0; } - + public void testCreateRow() throws Exception { reset(); newTestColumn(DataType.INT, false); newTestColumn(DataType.TEXT, false); newTestColumn(DataType.TEXT, false); newTestTable(); - + int colCount = _columns.size(); ByteBuffer buffer = createRow(9, "Tim", "McCune"); @@ -91,10 +91,10 @@ public class TableTest extends TestCase { newTestColumn(DataType.TEXT, true); newTestColumn(DataType.TEXT, true); newTestTable(); - + ByteBuffer[] bufCmp1 = encodeColumns(small, large); ByteBuffer[] bufCmp2 = encodeColumns(smallNotAscii, largeNotAscii); - + assertEquals(buf1[0].remaining(), (bufCmp1[0].remaining() + small.length() - 2)); assertEquals(buf1[1].remaining(), @@ -111,7 +111,7 @@ public class TableTest extends TestCase { } - private ByteBuffer createRow(Object... row) + private ByteBuffer createRow(Object... row) throws IOException { return _testTable.createRow(row); @@ -146,7 +146,7 @@ public class TableTest extends TestCase { return b; } - private TableImpl newTestTable() + private TableImpl newTestTable() throws Exception { _testTable = new TestTable(); @@ -185,8 +185,8 @@ public class TableTest extends TestCase { return getFormat().CHARSET; } @Override - protected Calendar getCalendar() { - return Calendar.getInstance(); + protected TimeZone getTimeZone() { + return TimeZone.getDefault(); } @Override public boolean isCompressedUnicode() { -- cgit v1.2.3 From 99af2bc3a62fe0e710bb386365770439bc2984c3 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Thu, 13 Dec 2018 01:15:30 +0000 Subject: round LDT date/times to millis; add some initial tests for LDT times git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1237 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/impl/ColumnImpl.java | 45 +++++- .../jackcess/LocalDateTimeTest.java | 160 +++++++++++++++++++++ 2 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index f5b7d5b..423b19e 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -65,6 +65,7 @@ import com.healthmarketscience.jackcess.complex.ComplexValue; import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; import com.healthmarketscience.jackcess.expr.Identifier; import com.healthmarketscience.jackcess.impl.complex.ComplexValueForeignKeyImpl; +import com.healthmarketscience.jackcess.impl.expr.NumberFormatter; import com.healthmarketscience.jackcess.util.ColumnValidator; import com.healthmarketscience.jackcess.util.SimpleColumnValidator; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -95,6 +96,8 @@ public class ColumnImpl implements Column, Comparable { private static final long MILLISECONDS_PER_DAY = (24L * 60L * 60L * 1000L); private static final long SECONDS_PER_DAY = (24L * 60L * 60L); private static final long NANOS_PER_SECOND = 1_000_000_000L; + private static final long NANOS_PER_MILLI = 1_000_000L; + private static final long MILLIS_PER_SECOND = 1000L; /** * Access starts counting dates at Dec 30, 1899 (note, this strange date @@ -980,13 +983,13 @@ public class ColumnImpl implements Column, Comparable { double secondsDouble = (Math.abs(value) % 1.0d) * SECONDS_PER_DAY; long timeSeconds = (long)secondsDouble; - long timeNanos = Math.round((secondsDouble % 1.0d) * NANOS_PER_SECOND); + long timeMillis = (long)(roundToMillis(secondsDouble % 1.0d) * + MILLIS_PER_SECOND); - return Duration.ofSeconds(dateSeconds + timeSeconds, timeNanos); + return Duration.ofSeconds(dateSeconds + timeSeconds, + timeMillis * NANOS_PER_MILLI); } - - /** * Writes a date value. */ @@ -1053,7 +1056,8 @@ public class ColumnImpl implements Column, Comparable { public static double toDateDouble(Object value, TimeZone tz, ZoneId zoneId) { if(value instanceof TemporalAccessor) { - return toDateDouble(toLocalDateTime((Temporal)value, tz, zoneId)); + return toDateDouble( + toLocalDateTime((TemporalAccessor)value, tz, zoneId)); } // seems access stores dates in the local timezone. guess you just @@ -1147,8 +1151,25 @@ public class ColumnImpl implements Column, Comparable { long dateSeconds = dateTimeSeconds - timeSeconds; long timeNanos = time.getNano(); - double timeDouble = ((((double)timeNanos / NANOS_PER_SECOND) + timeSeconds) - / SECONDS_PER_DAY); + // we have a difficult choice to make here between keeping a value which + // most accurately represents the bits saved and rounding to a value that + // would match what the user would expect too see. since we do a double + // to long conversion, we end up in a situation where the value might be + // 19.9999 seconds. access will display this as 20 seconds (access seems + // to only record times to second precision). if we return 19.9999, then + // when the value is written back out it will be exactly the same double + // (good), but will display as 19 seconds (bad because it looks wrong to + // the user). on the flip side, if we round, the value will display + // "correctly" to the user, but if the value is written back out, it will + // be a slightly different double value. this may not be a problem for + // most situations, but may result in incorrect index based lookups. in + // the old date time handling we use DateExt to store the original bits. + // in jdk8, we cannot extend LocalDateTime. for now, we will try + // returning the value rounded to milliseconds (technically still more + // precision than access uses but more likely to round trip to the same + // value). + double timeDouble = ((roundToMillis((double)timeNanos / NANOS_PER_SECOND) + + timeSeconds) / SECONDS_PER_DAY); double dateDouble = ((double)dateSeconds / SECONDS_PER_DAY); @@ -1159,6 +1180,16 @@ public class ColumnImpl implements Column, Comparable { return dateDouble + timeDouble; } + /** + * Rounds the given decimal to milliseconds (3 decimal places) using the + * standard access rounding mode. + */ + private static double roundToMillis(double dbl) { + return ((dbl == 0d) ? dbl : + new BigDecimal(dbl).setScale(3, NumberFormatter.ROUND_MODE) + .doubleValue()); + } + /** * @return an appropriate Date long value for the given object */ diff --git a/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java new file mode 100644 index 0000000..5dde831 --- /dev/null +++ b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java @@ -0,0 +1,160 @@ +/* +Copyright (c) 2018 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; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.math.BigDecimal; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import java.util.TreeSet; +import java.util.UUID; + +import com.healthmarketscience.jackcess.impl.ColumnImpl; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; +import com.healthmarketscience.jackcess.impl.RowIdImpl; +import com.healthmarketscience.jackcess.impl.RowImpl; +import com.healthmarketscience.jackcess.impl.TableImpl; +import com.healthmarketscience.jackcess.util.LinkResolver; +import junit.framework.TestCase; +import static com.healthmarketscience.jackcess.TestUtil.*; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import static com.healthmarketscience.jackcess.Database.*; + +/** + * + * @author James Ahlborn + */ +public class LocalDateTimeTest extends TestCase +{ + public LocalDateTimeTest(String name) throws Exception { + super(name); + } + + public void testAncientDates() throws Exception + { + ZoneId zoneId = ZoneId.of("America/New_York"); + DateTimeFormatter sdf = DateTimeFormatter.ofPattern("uuuu-MM-dd"); + + List dates = Arrays.asList("1582-10-15", "1582-10-14", + "1492-01-10", "1392-01-10"); + + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = createMem(fileFormat); + db.setZoneId(zoneId); + db.setDateTimeType(DateTimeType.LOCAL_DATE_TIME); + + Table table = new TableBuilder("test") + .addColumn(new ColumnBuilder("name", DataType.TEXT)) + .addColumn(new ColumnBuilder("date", DataType.SHORT_DATE_TIME)) + .toTable(db); + + for(String dateStr : dates) { + LocalDate ld = LocalDate.parse(dateStr, sdf); + table.addRow("row " + dateStr, ld); + } + + List foundDates = new ArrayList(); + for(Row row : table) { + foundDates.add(sdf.format(row.getLocalDateTime("date"))); + } + + assertEquals(dates, foundDates); + + db.close(); + } + + for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.OLD_DATES)) { + Database db = openCopy(testDB); + db.setDateTimeType(DateTimeType.LOCAL_DATE_TIME); + + Table t = db.getTable("Table1"); + + List foundDates = new ArrayList(); + for(Row row : t) { + foundDates.add(sdf.format(row.getLocalDateTime("DateField"))); + } + + assertEquals(dates, foundDates); + + db.close(); + } + + } + + public void testZoneId() throws Exception + { + ZoneId zoneId = ZoneId.of("America/New_York"); + doTestZoneId(zoneId); + + zoneId = ZoneId.of("Australia/Sydney"); + doTestZoneId(zoneId); + } + + private static void doTestZoneId(final ZoneId zoneId) throws Exception + { + final TimeZone tz = TimeZone.getTimeZone(zoneId); + ColumnImpl col = new ColumnImpl(null, null, DataType.SHORT_DATE_TIME, 0, 0, 0) { + @Override + protected TimeZone getTimeZone() { return tz; } + @Override + protected ZoneId getZoneId() { return zoneId; } + }; + + SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd"); + df.setTimeZone(tz); + + long startDate = df.parse("2012.01.01").getTime(); + long endDate = df.parse("2013.01.01").getTime(); + + Calendar curCal = Calendar.getInstance(tz); + curCal.setTimeInMillis(startDate); + + DateTimeFormatter sdf = DateTimeFormatter.ofPattern("uuuu.MM.dd HH:mm:ss"); + + while(curCal.getTimeInMillis() < endDate) { + Date curDate = curCal.getTime(); + LocalDateTime curLdt = LocalDateTime.ofInstant( + Instant.ofEpochMilli(curDate.getTime()), zoneId); + LocalDateTime newLdt = ColumnImpl.ldtFromLocalDateDouble( + col.toDateDouble(curDate)); + if(!curLdt.equals(newLdt)) { + System.out.println("FOO " + curLdt + " " + newLdt); + assertEquals(sdf.format(curLdt), sdf.format(newLdt)); + } + curCal.add(Calendar.MINUTE, 30); + } + } + +} -- cgit v1.2.3 From 843a44dfe074a2b587ff4aacbd4089ab22e6f087 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Sat, 15 Dec 2018 06:13:06 +0000 Subject: switch expression engine to LocalDateTime git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1238 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/expr/LocaleContext.java | 15 +- .../jackcess/expr/TemporalConfig.java | 51 +-- .../healthmarketscience/jackcess/expr/Value.java | 6 +- .../jackcess/impl/BaseEvalContext.java | 21 +- .../jackcess/impl/ColumnImpl.java | 76 ++-- .../jackcess/impl/DBEvalContext.java | 23 +- .../jackcess/impl/DatabaseImpl.java | 21 +- .../jackcess/impl/PropertyMapImpl.java | 3 +- .../jackcess/impl/expr/BaseDelayedValue.java | 6 +- .../jackcess/impl/expr/BaseNumericValue.java | 2 +- .../jackcess/impl/expr/BaseValue.java | 8 +- .../jackcess/impl/expr/BuiltinOperators.java | 6 +- .../jackcess/impl/expr/DateTimeValue.java | 12 +- .../jackcess/impl/expr/DefaultDateFunctions.java | 449 +++++++++------------ .../jackcess/impl/expr/DefaultFunctions.java | 15 +- .../jackcess/impl/expr/DefaultNumberFunctions.java | 2 +- .../jackcess/impl/expr/ExpressionTokenizer.java | 183 ++------- .../jackcess/impl/expr/Expressionator.java | 11 +- .../jackcess/impl/expr/FormatUtil.java | 8 +- .../jackcess/impl/expr/StringValue.java | 3 +- .../jackcess/impl/expr/ValueSupport.java | 49 ++- .../jackcess/PropertyExpressionTest.java | 4 +- .../jackcess/impl/DatabaseReadWriteTest.java | 18 +- .../jackcess/impl/expr/DefaultFunctionsTest.java | 7 +- .../jackcess/impl/expr/ExpressionatorTest.java | 33 +- 25 files changed, 395 insertions(+), 637 deletions(-) (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/LocaleContext.java b/src/main/java/com/healthmarketscience/jackcess/expr/LocaleContext.java index a90a80b..7b7a306 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/LocaleContext.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/LocaleContext.java @@ -17,8 +17,8 @@ limitations under the License. package com.healthmarketscience.jackcess.expr; import java.text.DecimalFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; /** * LocaleContext encapsulates all shared localization state for expression @@ -35,16 +35,15 @@ public interface LocaleContext public TemporalConfig getTemporalConfig(); /** - * @return an appropriately configured (i.e. TimeZone and other date/time - * flags) SimpleDateFormat for the given format. + * @return an appropriately configured (i.e. locale) DateTimeFormatter for + * the given format. */ - public SimpleDateFormat createDateFormat(String formatStr); + public DateTimeFormatter createDateFormatter(String formatStr); /** - * @return an appropriately configured (i.e. TimeZone and other date/time - * flags) Calendar. + * @return the currently configured ZoneId */ - public Calendar getCalendar(); + public ZoneId getZoneId(); /** * @return the currently configured NumericConfig (from the diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java b/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java index 919d682..b441c88 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java @@ -30,19 +30,18 @@ import java.util.Locale; */ public class TemporalConfig { - public static final String US_DATE_FORMAT = "M/d/yyyy"; - public static final String US_DATE_IMPLICIT_YEAR_FORMAT = "M/d"; + public static final String US_DATE_FORMAT = "M/d[/uuuu]"; public static final String US_TIME_FORMAT_12_FORMAT = "h:mm:ss a"; public static final String US_TIME_FORMAT_24_FORMAT = "H:mm:ss"; - public static final String US_LONG_DATE_FORMAT = "EEEE, MMMM dd, yyyy"; + public static final String US_LONG_DATE_FORMAT = "EEEE, MMMM dd, uuuu"; - public static final String MEDIUM_DATE_FORMAT = "dd-MMM-yy"; + public static final String MEDIUM_DATE_FORMAT = "dd-MMM-uu"; public static final String MEDIUM_TIME_FORMAT = "hh:mm a"; public static final String SHORT_TIME_FORMAT = "HH:mm"; /** default implementation which is configured for the US locale */ public static final TemporalConfig US_TEMPORAL_CONFIG = new TemporalConfig( - US_DATE_FORMAT, US_DATE_IMPLICIT_YEAR_FORMAT, US_LONG_DATE_FORMAT, + US_DATE_FORMAT, US_LONG_DATE_FORMAT, US_TIME_FORMAT_12_FORMAT, US_TIME_FORMAT_24_FORMAT, '/', ':', Locale.US); public enum Type { @@ -133,8 +132,8 @@ public class TemporalConfig } } + private final Locale _locale; private final String _dateFormat; - private final String _dateImplicitYearFormat; private final String _longDateFormat; private final String _timeFormat12; private final String _timeFormat24; @@ -142,7 +141,7 @@ public class TemporalConfig private final char _timeSeparator; private final String _dateTimeFormat12; private final String _dateTimeFormat24; - private final DateFormatSymbols _symbols; + private final String[] _amPmStrings; /** * Instantiates a new TemporalConfig with the given configuration. Note @@ -151,7 +150,6 @@ public class TemporalConfig * <time>". * * @param dateFormat the date (no time) format - * @param dateImplicitYearFormat the date (no time) with no year format * @param timeFormat12 the 12 hour time format * @param timeFormat24 the 24 hour time format * @param dateSeparator the primary separator used to separate elements in @@ -163,21 +161,26 @@ public class TemporalConfig * string. This value should differ from the * dateSeparator. */ - public TemporalConfig(String dateFormat, String dateImplicitYearFormat, - String longDateFormat, + public TemporalConfig(String dateFormat, String longDateFormat, String timeFormat12, String timeFormat24, char dateSeparator, char timeSeparator, Locale locale) { + _locale = locale; _dateFormat = dateFormat; - _dateImplicitYearFormat = dateImplicitYearFormat; _longDateFormat = longDateFormat; _timeFormat12 = timeFormat12; _timeFormat24 = timeFormat24; _dateSeparator = dateSeparator; _timeSeparator = timeSeparator; - _dateTimeFormat12 = _dateFormat + " " + _timeFormat12; - _dateTimeFormat24 = _dateFormat + " " + _timeFormat24; - _symbols = DateFormatSymbols.getInstance(locale); + _dateTimeFormat12 = toDateTimeFormat(_dateFormat, _timeFormat12); + _dateTimeFormat24 = toDateTimeFormat(_dateFormat, _timeFormat24); + // there doesn't seem to be a good/easy way to get this in new jave.time + // api, so just use old api + _amPmStrings = DateFormatSymbols.getInstance(locale).getAmPmStrings(); + } + + public Locale getLocale() { + return _locale; } public String getDateFormat() { @@ -252,24 +255,8 @@ public class TemporalConfig } } - public String getImplicitYearDateTimeFormat(Type type) { - switch(type) { - case DATE: - return _dateImplicitYearFormat; - case DATE_TIME: - return toDateTimeFormat(_dateImplicitYearFormat, getDefaultTimeFormat()); - case DATE_TIME_12: - return toDateTimeFormat(_dateImplicitYearFormat, getTimeFormat12()); - case DATE_TIME_24: - return toDateTimeFormat(_dateImplicitYearFormat, getTimeFormat24()); - default: - throw new IllegalArgumentException( - "the given format does not include a date " + type); - } - } - - public DateFormatSymbols getDateFormatSymbols() { - return _symbols; + public String[] getAmPmStrings() { + return _amPmStrings; } private static String toDateTimeFormat(String dateFormat, String timeFormat) { diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/Value.java b/src/main/java/com/healthmarketscience/jackcess/expr/Value.java index 118215e..ded758b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/Value.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/Value.java @@ -17,7 +17,7 @@ limitations under the License. package com.healthmarketscience.jackcess.expr; import java.math.BigDecimal; -import java.util.Date; +import java.time.LocalDateTime; /** * Wrapper for a typed primitive value used within the expression evaluation @@ -97,9 +97,9 @@ public interface Value public String getAsString(LocaleContext ctx); /** - * @return this primitive value converted to a Date + * @return this primitive value converted to a LocalDateTime */ - public Date getAsDateTime(LocaleContext ctx); + public LocalDateTime getAsLocalDateTime(LocaleContext ctx); /** * Since date/time values have different types, it may be more convenient to diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java index 0e52fa4..28785d1 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/BaseEvalContext.java @@ -19,8 +19,9 @@ package com.healthmarketscience.jackcess.impl; import java.io.IOException; import java.math.BigDecimal; import java.text.DecimalFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Collection; import java.util.Date; import java.util.EnumMap; @@ -28,6 +29,7 @@ import java.util.Map; import javax.script.Bindings; import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.DateTimeType; import com.healthmarketscience.jackcess.JackcessException; import com.healthmarketscience.jackcess.expr.EvalContext; import com.healthmarketscience.jackcess.expr.EvalException; @@ -82,12 +84,12 @@ public abstract class BaseEvalContext implements EvalContext return _dbCtx.getTemporalConfig(); } - public SimpleDateFormat createDateFormat(String formatStr) { - return _dbCtx.createDateFormat(formatStr); + public DateTimeFormatter createDateFormatter(String formatStr) { + return _dbCtx.createDateFormatter(formatStr); } - public Calendar getCalendar() { - return _dbCtx.getCalendar(); + public ZoneId getZoneId() { + return _dbCtx.getZoneId(); } public NumericConfig getNumericConfig() { @@ -146,7 +148,10 @@ public abstract class BaseEvalContext implements EvalContext protected Value toValue(Object val, DataType dType) { try { - val = ColumnImpl.toInternalValue(dType, val, getDatabase()); + // expression engine always uses LocalDateTime, so force that date/time + // type + val = ColumnImpl.toInternalValue(dType, val, getDatabase(), + ColumnImpl.LDT_DATE_TIME_FACTORY); if(val == null) { return ValueSupport.NULL_VAL; } @@ -158,7 +163,7 @@ public abstract class BaseEvalContext implements EvalContext case DATE: case TIME: case DATE_TIME: - return ValueSupport.toValue(vType, (Date)val); + return ValueSupport.toValue(vType, (LocalDateTime)val); case LONG: Integer i = ((val instanceof Integer) ? (Integer)val : ((Number)val).intValue()); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index 423b19e..273a62a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -40,7 +40,6 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.time.temporal.Temporal; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalQueries; import java.util.Calendar; @@ -108,14 +107,14 @@ public class ColumnImpl implements Column, Comparable { static final long MILLIS_BETWEEN_EPOCH_AND_1900 = 25569L * MILLISECONDS_PER_DAY; - static final LocalDate BASE_LD = LocalDate.of(1899, 12, 30); - static final LocalTime BASE_LT = LocalTime.of(0, 0); - static final LocalDateTime BASE_LDT = LocalDateTime.of(BASE_LD, BASE_LT); + public static final LocalDate BASE_LD = LocalDate.of(1899, 12, 30); + public static final LocalTime BASE_LT = LocalTime.of(0, 0); + public static final LocalDateTime BASE_LDT = LocalDateTime.of(BASE_LD, BASE_LT); private static final DateTimeFactory DEF_DATE_TIME_FACTORY = new DefaultDateTimeFactory(); - private static final DateTimeFactory LDT_DATE_TIME_FACTORY = + static final DateTimeFactory LDT_DATE_TIME_FACTORY = new LDTDateTimeFactory(); /** @@ -929,30 +928,12 @@ public class ColumnImpl implements Column, Comparable { return fromDateDouble(value, getTimeZone()); } - /** - * 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.getTimeZone()); - } - - /** - * Returns a java long time value converted from an access date double. - * @usage _advanced_method_ - */ - @Deprecated - public static long fromDateDouble(double value, Calendar c) { - // FIXME, remove me - return fromDateDouble(value, c.getTimeZone()); - } - - public static long fromDateDouble(double value, TimeZone tz) { + private static long fromDateDouble(double value, TimeZone tz) { long localTime = fromLocalDateDouble(value); return localTime - getFromLocalTimeZoneOffset(localTime, tz); } - public static long fromLocalDateDouble(double value) { + static long fromLocalDateDouble(double value) { long datePart = ((long)value) * MILLISECONDS_PER_DAY; // the fractional part of the double represents the time. it is always @@ -968,11 +949,11 @@ public class ColumnImpl implements Column, Comparable { } public static LocalDateTime ldtFromLocalDateDouble(double value) { - Duration dateTimeOffset = durationFromLocalDateDouble1900(value); + Duration dateTimeOffset = durationFromLocalDateDouble(value); return BASE_LDT.plus(dateTimeOffset); } - private static Duration durationFromLocalDateDouble1900(double value) { + private static Duration durationFromLocalDateDouble(double value) { long dateSeconds = ((long)value) * SECONDS_PER_DAY; // the fractional part of the double represents the time. it is always @@ -1027,7 +1008,7 @@ public class ColumnImpl implements Column, Comparable { * time value. * @usage _advanced_method_ */ - public static double toDateDouble(Object value, DatabaseImpl db) + private static double toDateDouble(Object value, DatabaseImpl db) { return toDateDouble(value, db.getTimeZone(), db.getZoneId()); } @@ -1037,23 +1018,7 @@ public class ColumnImpl implements Column, Comparable { * Date/Calendar/Number/Temporal time value. * @usage _advanced_method_ */ - @Deprecated - public static double toDateDouble(Object value, Calendar c) { - // FIXME remove me - return toDateDouble(value, c.getTimeZone()); - } - - public static double toDateDouble(Object value, TimeZone tz) - { - return toDateDouble(value, tz, null); - } - - /** - * Returns an access date double converted from a java - * Date/Calendar/Number/Temporal time value. - * @usage _advanced_method_ - */ - public static double toDateDouble(Object value, TimeZone tz, ZoneId zoneId) + private static double toDateDouble(Object value, TimeZone tz, ZoneId zoneId) { if(value instanceof TemporalAccessor) { return toDateDouble( @@ -1139,10 +1104,10 @@ public class ColumnImpl implements Column, Comparable { public static double toDateDouble(LocalDateTime ldt) { Duration dateTimeOffset = Duration.between(BASE_LDT, ldt); - return toLocalDateDouble1900(dateTimeOffset); + return toLocalDateDouble(dateTimeOffset); } - private static double toLocalDateDouble1900(Duration time) { + private static double toLocalDateDouble(Duration time) { long dateTimeSeconds = time.getSeconds(); long timeSeconds = dateTimeSeconds % SECONDS_PER_DAY; if(timeSeconds < 0) { @@ -1782,6 +1747,8 @@ public class ColumnImpl implements Column, Comparable { return ((Boolean)value) ? BigDecimal.valueOf(-1) : BigDecimal.ZERO; } else if(value instanceof Date) { return new BigDecimal(toDateDouble(value, db)); + } else if(value instanceof LocalDateTime) { + return new BigDecimal(toDateDouble((LocalDateTime)value)); } return new BigDecimal(value.toString()); } @@ -1812,6 +1779,8 @@ public class ColumnImpl implements Column, Comparable { return ((Boolean)value) ? -1 : 0; } else if(value instanceof Date) { return toDateDouble(value, db); + } else if(value instanceof LocalDateTime) { + return toDateDouble((LocalDateTime)value); } return Double.valueOf(value.toString()); } @@ -2146,6 +2115,14 @@ public class ColumnImpl implements Column, Comparable { public static Object toInternalValue(DataType dataType, Object value, DatabaseImpl db) throws IOException + { + return toInternalValue(dataType, value, db, null); + } + + static Object toInternalValue(DataType dataType, Object value, + DatabaseImpl db, + ColumnImpl.DateTimeFactory factory) + throws IOException { if(value == null) { return null; @@ -2171,7 +2148,10 @@ public class ColumnImpl implements Column, Comparable { return ((value instanceof Double) ? value : toNumber(value, db).doubleValue()); case SHORT_DATE_TIME: - return db.getDateTimeFactory().toInternalValue(db, value); + if(factory == null) { + factory = db.getDateTimeFactory(); + } + return factory.toInternalValue(db, value); case TEXT: case MEMO: case GUID: diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java index 7f50f68..2fbec97 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java @@ -17,8 +17,8 @@ limitations under the License. package com.healthmarketscience.jackcess.impl; import java.text.DecimalFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.Map; import javax.script.Bindings; import javax.script.SimpleBindings; @@ -42,7 +42,7 @@ public class DBEvalContext implements Expressionator.ParseContext, EvalConfig private final DatabaseImpl _db; private FunctionLookup _funcs = DefaultFunctions.LOOKUP; - private Map _sdfs; + private Map _sdfs; private Map _dfs; private TemporalConfig _temporal = TemporalConfig.US_TEMPORAL_CONFIG; private NumericConfig _numeric = NumericConfig.US_NUMERIC_CONFIG; @@ -68,8 +68,8 @@ public class DBEvalContext implements Expressionator.ParseContext, EvalConfig } } - public Calendar getCalendar() { - return _db.getCalendar(); + public ZoneId getZoneId() { + return _db.getZoneId(); } public NumericConfig getNumericConfig() { @@ -99,14 +99,13 @@ public class DBEvalContext implements Expressionator.ParseContext, EvalConfig _bindings = bindings; } - public SimpleDateFormat createDateFormat(String formatStr) { + public DateTimeFormatter createDateFormatter(String formatStr) { if(_sdfs == null) { - _sdfs = new SimpleCache(MAX_CACHE_SIZE); + _sdfs = new SimpleCache(MAX_CACHE_SIZE); } - SimpleDateFormat sdf = _sdfs.get(formatStr); + DateTimeFormatter sdf = _sdfs.get(formatStr); if(sdf == null) { - sdf = _db.createDateFormat(formatStr); - sdf.setDateFormatSymbols(_temporal.getDateFormatSymbols()); + sdf = DateTimeFormatter.ofPattern(formatStr, _temporal.getLocale()); _sdfs.put(formatStr, sdf); } return sdf; @@ -128,8 +127,4 @@ public class DBEvalContext implements Expressionator.ParseContext, EvalConfig public float getRandom(Integer seed) { return _rndCtx.getRandom(seed); } - - void resetDateTimeConfig() { - _sdfs = null; - } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index f132a99..2ff624e 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -342,8 +342,6 @@ public class DatabaseImpl implements Database /** shared state used when enforcing foreign keys */ private final FKEnforcer.SharedState _fkEnforcerSharedState = FKEnforcer.initSharedState(); - /** Calendar for use interpreting dates/times in Columns */ - private Calendar _calendar; /** shared context for evaluating expressions */ private DBEvalContext _evalCtx; /** factory for the appropriate date/time type */ @@ -535,7 +533,7 @@ public class DatabaseImpl implements Database _evaluateExpressions = getDefaultEvaluateExpressions(); _fileFormat = fileFormat; _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync); - _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone); + setZoneInfo(timeZone, null); if(provider == null) { provider = DefaultCodecProvider.INSTANCE; } @@ -682,12 +680,6 @@ public class DatabaseImpl implements Database _timeZone = newTimeZone; _zoneId = newZoneId; - - // clear cached calendar(s) when timezone is changed - _calendar = null; - if(_evalCtx != null) { - _evalCtx.resetDateTimeConfig(); - } } @Override @@ -778,17 +770,6 @@ public class DatabaseImpl implements Database return _fkEnforcerSharedState; } - /** - * @usage _advanced_method_ - */ - Calendar getCalendar() { - if(_calendar == null) { - _calendar = DatabaseBuilder.toCompatibleCalendar( - Calendar.getInstance(_timeZone)); - } - return _calendar; - } - public EvalConfig getEvalConfig() { return getEvalContext(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java index be3a249..dfcbb9d 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/PropertyMapImpl.java @@ -17,6 +17,7 @@ limitations under the License. package com.healthmarketscience.jackcess.impl; import java.io.IOException; +import java.time.LocalDateTime; import java.util.Date; import java.util.HashMap; import java.util.Iterator; @@ -208,7 +209,7 @@ public class PropertyMapImpl implements PropertyMap type = DataType.FLOAT; } else if(value instanceof Double) { type = DataType.DOUBLE; - } else if(value instanceof Date) { + } else if((value instanceof Date) || (value instanceof LocalDateTime)) { type = DataType.SHORT_DATE_TIME; } else if(value instanceof byte[]) { type = DataType.OLE; diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java index d527c69..1e99e64 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseDelayedValue.java @@ -17,7 +17,7 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.util.Date; +import java.time.LocalDateTime; import com.healthmarketscience.jackcess.expr.LocaleContext; import com.healthmarketscience.jackcess.expr.Value; @@ -60,8 +60,8 @@ public abstract class BaseDelayedValue implements Value return getDelegate().getAsString(ctx); } - public Date getAsDateTime(LocaleContext ctx) { - return getDelegate().getAsDateTime(ctx); + public LocalDateTime getAsLocalDateTime(LocaleContext ctx) { + return getDelegate().getAsLocalDateTime(ctx); } public Value getAsDateTimeValue(LocaleContext ctx) { diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java index 299cd2a..cae689d 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseNumericValue.java @@ -44,7 +44,7 @@ public abstract class BaseNumericValue extends BaseValue @Override public Value getAsDateTimeValue(LocaleContext ctx) { Value dateValue = DefaultDateFunctions.numberToDateValue( - ctx, getNumber().doubleValue()); + getNumber().doubleValue()); if(dateValue == null) { throw invalidConversion(Value.Type.DATE_TIME); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java index 2b172d3..35e6ccf 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java @@ -17,11 +17,11 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.util.Date; +import java.time.LocalDateTime; -import com.healthmarketscience.jackcess.expr.Value; import com.healthmarketscience.jackcess.expr.EvalException; import com.healthmarketscience.jackcess.expr.LocaleContext; +import com.healthmarketscience.jackcess.expr.Value; /** * @@ -41,8 +41,8 @@ public abstract class BaseValue implements Value throw invalidConversion(Type.STRING); } - public Date getAsDateTime(LocaleContext ctx) { - return (Date)getAsDateTimeValue(ctx).get(); + public LocalDateTime getAsLocalDateTime(LocaleContext ctx) { + return (LocalDateTime)getAsDateTimeValue(ctx).get(); } public Value getAsDateTimeValue(LocaleContext ctx) { 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 5131a93..5f63dad 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java @@ -76,7 +76,7 @@ public class BuiltinOperators case DATE_TIME: // dates/times get converted to date doubles for arithmetic double result = -param1.getAsDouble(ctx); - return toDateValue(ctx, mathType, result); + return toDateValueIfPossible(mathType, result); case LONG: return toValue(-param1.getAsLongInt(ctx)); case DOUBLE: @@ -108,7 +108,7 @@ public class BuiltinOperators case DATE_TIME: // dates/times get converted to date doubles for arithmetic double result = param1.getAsDouble(ctx) + param2.getAsDouble(ctx); - return toDateValue(ctx, mathType, result); + return toDateValueIfPossible(mathType, result); case LONG: return toValue(param1.getAsLongInt(ctx) + param2.getAsLongInt(ctx)); case DOUBLE: @@ -138,7 +138,7 @@ public class BuiltinOperators case DATE_TIME: // dates/times get converted to date doubles for arithmetic double result = param1.getAsDouble(ctx) - param2.getAsDouble(ctx); - return toDateValue(ctx, mathType, result); + return toDateValueIfPossible(mathType, result); case LONG: return toValue(param1.getAsLongInt(ctx) - param2.getAsLongInt(ctx)); case DOUBLE: diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java index e2de36d..f9d1e7b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DateTimeValue.java @@ -17,11 +17,11 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.util.Date; +import java.time.LocalDateTime; -import com.healthmarketscience.jackcess.impl.ColumnImpl; import com.healthmarketscience.jackcess.expr.LocaleContext; import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.ColumnImpl; /** * @@ -30,9 +30,9 @@ import com.healthmarketscience.jackcess.expr.Value; public class DateTimeValue extends BaseValue { private final Type _type; - private final Date _val; + private final LocalDateTime _val; - public DateTimeValue(Type type, Date val) { + public DateTimeValue(Type type, LocalDateTime val) { if(!type.isTemporal()) { throw new IllegalArgumentException("invalid date/time type"); } @@ -49,7 +49,7 @@ public class DateTimeValue extends BaseValue } protected Double getNumber(LocaleContext ctx) { - return ColumnImpl.toDateDouble(_val, ctx.getCalendar()); + return ColumnImpl.toDateDouble(_val); } @Override @@ -64,7 +64,7 @@ public class DateTimeValue extends BaseValue } @Override - public Date getAsDateTime(LocaleContext ctx) { + public LocalDateTime getAsLocalDateTime(LocaleContext ctx) { return _val; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java index a19ab0a..2ac67d2 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java @@ -17,11 +17,20 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; -import java.math.BigDecimal; -import java.text.DateFormat; -import java.text.DateFormatSymbols; -import java.util.Calendar; -import java.util.Date; +import java.time.DateTimeException; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Month; +import java.time.MonthDay; +import java.time.Year; +import java.time.format.DateTimeFormatter; +import java.time.format.TextStyle; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.WeekFields; import com.healthmarketscience.jackcess.expr.EvalContext; import com.healthmarketscience.jackcess.expr.EvalException; @@ -44,12 +53,6 @@ public class DefaultDateFunctions // max, valid, recognizable date: December 31, 9999 A.D. 23:59:59 private static final double MAX_DATE = 2958465.999988426d; - private static final long SECONDS_PER_DAY = 24L * 60L * 60L; - private static final double DSECONDS_PER_DAY = SECONDS_PER_DAY; - - private static final long SECONDS_PER_HOUR = 60L * 60L; - private static final long SECONDS_PER_MINUTE = 60L; - private static final String INTV_YEAR = "yyyy"; private static final String INTV_QUARTER = "q"; private static final String INTV_MONTH = "m"; @@ -61,7 +64,8 @@ public class DefaultDateFunctions private static final String INTV_MINUTE = "n"; private static final String INTV_SECOND = "s"; - private enum WeekOpType { GET_WEEK, GET_NUM_WEEKS } + private static final WeekFields SUNDAY_FIRST = + WeekFields.of(DayOfWeek.SUNDAY, 1); private DefaultDateFunctions() {} @@ -72,8 +76,7 @@ public class DefaultDateFunctions public static final Function DATE = registerFunc(new Func0("Date") { @Override protected Value eval0(EvalContext ctx) { - double dd = dateOnly(currentTimeDouble(ctx)); - return ValueSupport.toDateValue(ctx, Value.Type.DATE, dd); + return ValueSupport.toValue(LocalDate.now()); } }); @@ -84,8 +87,7 @@ public class DefaultDateFunctions if(dv.getType() == Value.Type.DATE) { return dv; } - double dd = dateOnly(dv.getAsDouble(ctx)); - return ValueSupport.toDateValue(ctx, Value.Type.DATE, dd); + return ValueSupport.toValue(dv.getAsLocalDateTime(ctx).toLocalDate()); } }); @@ -101,15 +103,11 @@ public class DefaultDateFunctions year += ((year <= 29) ? 2000 : 1900); } - Calendar cal = ctx.getCalendar(); - cal.clear(); + // we have to construct incrementatlly to handle out of range values + LocalDate ld = LocalDate.of(year,1,1).plusMonths(month - 1) + .plusDays(day - 1); - cal.set(Calendar.YEAR, year); - // convert to 0 based value - cal.set(Calendar.MONTH, month - 1); - cal.set(Calendar.DAY_OF_MONTH, day); - - return ValueSupport.toValue(Value.Type.DATE, cal.getTime()); + return ValueSupport.toValue(ld); } }); @@ -127,27 +125,27 @@ public class DefaultDateFunctions String intv = params[0].getAsString(ctx).trim(); int result = -1; if(intv.equalsIgnoreCase(INTV_YEAR)) { - result = nonNullToCalendarField(ctx, param2, Calendar.YEAR); + result = param2.getAsLocalDateTime(ctx).getYear(); } else if(intv.equalsIgnoreCase(INTV_QUARTER)) { - result = getQuarter(nonNullToCalendar(ctx, param2)); + result = getQuarter(param2.getAsLocalDateTime(ctx)); } else if(intv.equalsIgnoreCase(INTV_MONTH)) { - // convert from 0 based to 1 based value - result = nonNullToCalendarField(ctx, param2, Calendar.MONTH) + 1; + result = param2.getAsLocalDateTime(ctx).getMonthValue(); } else if(intv.equalsIgnoreCase(INTV_DAY_OF_YEAR)) { - result = nonNullToCalendarField(ctx, param2, Calendar.DAY_OF_YEAR); + result = param2.getAsLocalDateTime(ctx).getDayOfYear(); } else if(intv.equalsIgnoreCase(INTV_DAY)) { - result = nonNullToCalendarField(ctx, param2, Calendar.DAY_OF_MONTH); + result = param2.getAsLocalDateTime(ctx).getDayOfMonth(); } else if(intv.equalsIgnoreCase(INTV_WEEKDAY)) { - int dayOfWeek = nonNullToCalendarField(ctx, param2, Calendar.DAY_OF_WEEK); + int dayOfWeek = param2.getAsLocalDateTime(ctx) + .get(SUNDAY_FIRST.dayOfWeek()); result = dayOfWeekToWeekDay(dayOfWeek, firstDay); } else if(intv.equalsIgnoreCase(INTV_WEEK)) { result = weekOfYear(ctx, param2, firstDay, firstWeekType); } else if(intv.equalsIgnoreCase(INTV_HOUR)) { - result = nonNullToCalendarField(ctx, param2, Calendar.HOUR_OF_DAY); + result = param2.getAsLocalDateTime(ctx).getHour(); } else if(intv.equalsIgnoreCase(INTV_MINUTE)) { - result = nonNullToCalendarField(ctx, param2, Calendar.MINUTE); + result = param2.getAsLocalDateTime(ctx).getMinute(); } else if(intv.equalsIgnoreCase(INTV_SECOND)) { - result = nonNullToCalendarField(ctx, param2, Calendar.SECOND); + result = param2.getAsLocalDateTime(ctx).getSecond(); } else { throw new EvalException("Invalid interval " + intv); } @@ -167,33 +165,31 @@ public class DefaultDateFunctions String intv = param1.getAsString(ctx).trim(); int val = param2.getAsLongInt(ctx); - Calendar cal = nonNullToCalendar(ctx, param3); + LocalDateTime ldt = param3.getAsLocalDateTime(ctx); if(intv.equalsIgnoreCase(INTV_YEAR)) { - cal.add(Calendar.YEAR, val); + ldt = ldt.plus(val, ChronoUnit.YEARS); } else if(intv.equalsIgnoreCase(INTV_QUARTER)) { - cal.add(Calendar.MONTH, val * 3); + ldt = ldt.plus(val * 3, ChronoUnit.MONTHS); } else if(intv.equalsIgnoreCase(INTV_MONTH)) { - cal.add(Calendar.MONTH, val); - } else if(intv.equalsIgnoreCase(INTV_DAY_OF_YEAR)) { - cal.add(Calendar.DAY_OF_YEAR, val); - } else if(intv.equalsIgnoreCase(INTV_DAY)) { - cal.add(Calendar.DAY_OF_YEAR, val); - } else if(intv.equalsIgnoreCase(INTV_WEEKDAY)) { - cal.add(Calendar.DAY_OF_WEEK, val); + ldt = ldt.plus(val, ChronoUnit.MONTHS); + } else if(intv.equalsIgnoreCase(INTV_DAY_OF_YEAR) || + intv.equalsIgnoreCase(INTV_DAY) || + intv.equalsIgnoreCase(INTV_WEEKDAY)) { + ldt = ldt.plus(val, ChronoUnit.DAYS); } else if(intv.equalsIgnoreCase(INTV_WEEK)) { - cal.add(Calendar.WEEK_OF_YEAR, val); + ldt = ldt.plus(val, ChronoUnit.WEEKS); } else if(intv.equalsIgnoreCase(INTV_HOUR)) { - cal.add(Calendar.HOUR, val); + ldt = ldt.plus(val, ChronoUnit.HOURS); } else if(intv.equalsIgnoreCase(INTV_MINUTE)) { - cal.add(Calendar.MINUTE, val); + ldt = ldt.plus(val, ChronoUnit.MINUTES); } else if(intv.equalsIgnoreCase(INTV_SECOND)) { - cal.add(Calendar.SECOND, val); + ldt = ldt.plus(val, ChronoUnit.SECONDS); } else { throw new EvalException("Invalid interval " + intv); } - return ValueSupport.toValue(cal); + return ValueSupport.toValue(ldt); } }); @@ -212,14 +208,14 @@ public class DefaultDateFunctions String intv = params[0].getAsString(ctx).trim(); - Calendar cal1 = nonNullToCalendar(ctx, param2); - Calendar cal2 = nonNullToCalendar(ctx, param3); + LocalDateTime ldt1 = param2.getAsLocalDateTime(ctx); + LocalDateTime ldt2 = param3.getAsLocalDateTime(ctx); int sign = 1; - if(cal1.after(cal2)) { - Calendar tmp = cal1; - cal1 = cal2; - cal2 = tmp; + if(ldt1.isAfter(ldt2)) { + LocalDateTime tmp = ldt1; + ldt1 = ldt2; + ldt2 = tmp; sign = -1; } @@ -229,22 +225,22 @@ public class DefaultDateFunctions int result = -1; if(intv.equalsIgnoreCase(INTV_YEAR)) { - result = cal2.get(Calendar.YEAR) - cal1.get(Calendar.YEAR); + result = ldt2.getYear() - ldt1.getYear(); } else if(intv.equalsIgnoreCase(INTV_QUARTER)) { - int y1 = cal1.get(Calendar.YEAR); - int q1 = getQuarter(cal1); - int y2 = cal2.get(Calendar.YEAR); - int q2 = getQuarter(cal2); + int y1 = ldt1.getYear(); + int q1 = getQuarter(ldt1); + int y2 = ldt2.getYear(); + int q2 = getQuarter(ldt2); while(y2 > y1) { q2 += 4; --y2; } result = q2 - q1; } else if(intv.equalsIgnoreCase(INTV_MONTH)) { - int y1 = cal1.get(Calendar.YEAR); - int m1 = cal1.get(Calendar.MONTH); - int y2 = cal2.get(Calendar.YEAR); - int m2 = cal2.get(Calendar.MONTH); + int y1 = ldt1.getYear(); + int m1 = ldt1.getMonthValue(); + int y2 = ldt2.getYear(); + int m2 = ldt2.getMonthValue(); while(y2 > y1) { m2 += 12; --y2; @@ -252,30 +248,30 @@ public class DefaultDateFunctions result = m2 - m1; } else if(intv.equalsIgnoreCase(INTV_DAY_OF_YEAR) || intv.equalsIgnoreCase(INTV_DAY)) { - result = getDayDiff(cal1, cal2); + result = getDayDiff(ldt1, ldt2); } else if(intv.equalsIgnoreCase(INTV_WEEKDAY)) { // this calulates number of 7 day periods between two dates - result = getDayDiff(cal1, cal2) / 7; + result = getDayDiff(ldt1, ldt2) / 7; } else if(intv.equalsIgnoreCase(INTV_WEEK)) { // this counts number of "week of year" intervals between two dates - int w1 = weekOfYear(cal1, firstDay, firstWeekType); - int y1 = getWeekOfYearYear(cal1, w1); - int w2 = weekOfYear(cal2, firstDay, firstWeekType); - int y2 = getWeekOfYearYear(cal2, w2); + WeekFields weekFields = weekFields(firstDay, firstWeekType); + int w1 = ldt1.get(weekFields.weekOfWeekBasedYear()); + int y1 = ldt1.get(weekFields.weekBasedYear()); + int w2 = ldt2.get(weekFields.weekOfWeekBasedYear()); + int y2 = ldt2.get(weekFields.weekBasedYear()); while(y2 > y1) { - cal2.add(Calendar.YEAR, -1); - w2 += weeksInYear(cal2, firstDay, firstWeekType); - y2 = cal2.get(Calendar.YEAR); + --y2; + w2 += weeksInYear(y2, weekFields); } result = w2 - w1; } else if(intv.equalsIgnoreCase(INTV_HOUR)) { - result = getHourDiff(cal1, cal2); + result = getHourDiff(ldt1, ldt2); } else if(intv.equalsIgnoreCase(INTV_MINUTE)) { - result = getMinuteDiff(cal1, cal2); + result = getMinuteDiff(ldt1, ldt2); } else if(intv.equalsIgnoreCase(INTV_SECOND)) { - int s1 = cal1.get(Calendar.SECOND); - int s2 = cal2.get(Calendar.SECOND); - int minuteDiff = getMinuteDiff(cal1, cal2); + int s1 = ldt1.getSecond(); + int s2 = ldt2.getSecond(); + int minuteDiff = getMinuteDiff(ldt1, ldt2); result = (s2 + (60 * minuteDiff)) - s1; } else { throw new EvalException("Invalid interval " + intv); @@ -288,15 +284,15 @@ public class DefaultDateFunctions public static final Function NOW = registerFunc(new Func0("Now") { @Override protected Value eval0(EvalContext ctx) { - return ValueSupport.toValue(Value.Type.DATE_TIME, new Date()); + return ValueSupport.toValue(Value.Type.DATE_TIME, + LocalDateTime.now(ctx.getZoneId())); } }); public static final Function TIME = registerFunc(new Func0("Time") { @Override protected Value eval0(EvalContext ctx) { - double dd = timeOnly(currentTimeDouble(ctx)); - return ValueSupport.toDateValue(ctx, Value.Type.TIME, dd); + return ValueSupport.toValue(LocalTime.now(ctx.getZoneId())); } }); @@ -307,15 +303,15 @@ public class DefaultDateFunctions if(dv.getType() == Value.Type.TIME) { return dv; } - double dd = timeOnly(dv.getAsDouble(ctx)); - return ValueSupport.toDateValue(ctx, Value.Type.TIME, dd); + return ValueSupport.toValue(dv.getAsLocalDateTime(ctx).toLocalTime()); } }); public static final Function TIMER = registerFunc(new Func0("Timer") { @Override protected Value eval0(EvalContext ctx) { - double dd = timeOnly(currentTimeDouble(ctx)) * DSECONDS_PER_DAY; + double dd = LocalTime.now(ctx.getZoneId()) + .get(ChronoField.MILLI_OF_DAY) / 1000d; return ValueSupport.toValue(dd); } }); @@ -327,59 +323,46 @@ public class DefaultDateFunctions int minutes = param2.getAsLongInt(ctx); int seconds = param3.getAsLongInt(ctx); - long totalSeconds = (hours * SECONDS_PER_HOUR) + - (minutes * SECONDS_PER_MINUTE) + seconds; - if(totalSeconds < 0L) { - do { - totalSeconds += SECONDS_PER_DAY; - } while(totalSeconds < 0L); - } else if(totalSeconds > SECONDS_PER_DAY) { - totalSeconds %= SECONDS_PER_DAY; - } + // we have to construct incrementatlly to handle out of range values + LocalTime lt = ColumnImpl.BASE_LT.plusHours(hours).plusMinutes(minutes) + .plusSeconds(seconds); - double dd = totalSeconds / DSECONDS_PER_DAY; - return ValueSupport.toDateValue(ctx, Value.Type.TIME, dd); + return ValueSupport.toValue(lt); } }); public static final Function HOUR = registerFunc(new Func1NullIsNull("Hour") { @Override protected Value eval1(EvalContext ctx, Value param1) { - return ValueSupport.toValue( - nonNullToCalendarField(ctx, param1, Calendar.HOUR_OF_DAY)); + return ValueSupport.toValue(param1.getAsLocalDateTime(ctx).getHour()); } }); public static final Function MINUTE = registerFunc(new Func1NullIsNull("Minute") { @Override protected Value eval1(EvalContext ctx, Value param1) { - return ValueSupport.toValue( - nonNullToCalendarField(ctx, param1, Calendar.MINUTE)); + return ValueSupport.toValue(param1.getAsLocalDateTime(ctx).getMinute()); } }); public static final Function SECOND = registerFunc(new Func1NullIsNull("Second") { @Override protected Value eval1(EvalContext ctx, Value param1) { - return ValueSupport.toValue( - nonNullToCalendarField(ctx, param1, Calendar.SECOND)); + return ValueSupport.toValue(param1.getAsLocalDateTime(ctx).getSecond()); } }); public static final Function YEAR = registerFunc(new Func1NullIsNull("Year") { @Override protected Value eval1(EvalContext ctx, Value param1) { - return ValueSupport.toValue( - nonNullToCalendarField(ctx, param1, Calendar.YEAR)); + return ValueSupport.toValue(param1.getAsLocalDateTime(ctx).getYear()); } }); public static final Function MONTH = registerFunc(new Func1NullIsNull("Month") { @Override protected Value eval1(EvalContext ctx, Value param1) { - // convert from 0 based to 1 based value - return ValueSupport.toValue( - nonNullToCalendarField(ctx, param1, Calendar.MONTH) + 1); + return ValueSupport.toValue(param1.getAsLocalDateTime(ctx).getMonthValue()); } }); @@ -390,16 +373,12 @@ public class DefaultDateFunctions if(param1.isNull()) { return ValueSupport.NULL_VAL; } - // convert from 1 based to 0 based value - int month = param1.getAsLongInt(ctx) - 1; - - boolean abbreviate = getOptionalBooleanParam(ctx, params, 1); + Month month = Month.of(param1.getAsLongInt(ctx)); - DateFormatSymbols syms = ctx.getTemporalConfig().getDateFormatSymbols(); - String[] monthNames = (abbreviate ? - syms.getShortMonths() : syms.getMonths()); - // note, the array is 1 based - return ValueSupport.toValue(monthNames[month]); + TextStyle textStyle = getTextStyle(ctx, params, 1); + String monthName = month.getDisplayName( + textStyle, ctx.getTemporalConfig().getLocale()); + return ValueSupport.toValue(monthName); } }); @@ -407,7 +386,7 @@ public class DefaultDateFunctions @Override protected Value eval1(EvalContext ctx, Value param1) { return ValueSupport.toValue( - nonNullToCalendarField(ctx, param1, Calendar.DAY_OF_MONTH)); + param1.getAsLocalDateTime(ctx).getDayOfMonth()); } }); @@ -418,7 +397,8 @@ public class DefaultDateFunctions if(param1.isNull()) { return ValueSupport.NULL_VAL; } - int dayOfWeek = nonNullToCalendarField(ctx, param1, Calendar.DAY_OF_WEEK); + int dayOfWeek = param1.getAsLocalDateTime(ctx) + .get(SUNDAY_FIRST.dayOfWeek()); int firstDay = getFirstDayParam(ctx, params, 1); @@ -435,31 +415,17 @@ public class DefaultDateFunctions } int weekday = param1.getAsLongInt(ctx); - boolean abbreviate = getOptionalBooleanParam(ctx, params, 1); + TextStyle textStyle = getTextStyle(ctx, params, 1); int firstDay = getFirstDayParam(ctx, params, 2); int dayOfWeek = weekDayToDayOfWeek(weekday, firstDay); - - DateFormatSymbols syms = ctx.getTemporalConfig().getDateFormatSymbols(); - String[] weekdayNames = (abbreviate ? - syms.getShortWeekdays() : syms.getWeekdays()); - // note, the array is 1 based - return ValueSupport.toValue(weekdayNames[dayOfWeek]); + String weekdayName = dayOfWeek(dayOfWeek).getDisplayName( + textStyle, ctx.getTemporalConfig().getLocale()); + return ValueSupport.toValue(weekdayName); } }); - private static int nonNullToCalendarField(EvalContext ctx, Value param, - int field) { - return nonNullToCalendar(ctx, param).get(field); - } - - private static Calendar nonNullToCalendar(EvalContext ctx, Value param) { - Calendar cal = ctx.getCalendar(); - cal.setTime(param.getAsDateTime(ctx)); - return cal; - } - static Value stringToDateValue(LocaleContext ctx, String valStr) { // see if we can coerce to date/time TemporalConfig.Type valTempType = ExpressionTokenizer.determineDateType( @@ -467,26 +433,31 @@ public class DefaultDateFunctions if(valTempType != null) { - DateFormat parseDf = ExpressionTokenizer.createParseDateTimeFormat( - valTempType, ctx); + DateTimeFormatter parseDf = ctx.createDateFormatter( + ctx.getTemporalConfig().getDateTimeFormat(valTempType)); try { - Date dateVal = ExpressionTokenizer.parseComplete(parseDf, valStr); - return ValueSupport.toValue(valTempType.getValueType(), dateVal); - } catch(java.text.ParseException pe) { + TemporalAccessor parsedInfo = parseDf.parse(valStr); + LocalDate ld = ColumnImpl.BASE_LD; if(valTempType.includesDate()) { - // the date may not include a year value, in which case it means - // to use the "current" year. see if this is an implicit year date - parseDf = ExpressionTokenizer.createParseImplicitYearDateTimeFormat( - valTempType, ctx); - try { - Date dateVal = ExpressionTokenizer.parseComplete(parseDf, valStr); - return ValueSupport.toValue(valTempType.getValueType(), dateVal); - } catch(java.text.ParseException pe2) { - // guess not, continue on to failure + // the year may not be explicitly specified + if(parsedInfo.isSupported(ChronoField.YEAR)) { + ld = LocalDate.from(parsedInfo); + } else { + ld = MonthDay.from(parsedInfo).atYear( + Year.now(ctx.getZoneId()).getValue()); } } + + LocalTime lt = ColumnImpl.BASE_LT; + if(valTempType.includesTime()) { + lt = LocalTime.from(parsedInfo); + } + + return ValueSupport.toValue(LocalDateTime.of(ld, lt)); + } catch(DateTimeException de) { + // note a valid date/time } } @@ -494,35 +465,18 @@ public class DefaultDateFunctions return null; } - static Value numberToDateValue(LocaleContext ctx, double dd) { - if((dd < MIN_DATE) || (dd > MAX_DATE)) { + static boolean isValidDateDouble(double dd) { + return ((dd >= MIN_DATE) && (dd <= MAX_DATE)); + } + + static Value numberToDateValue(double dd) { + if(!isValidDateDouble(dd)) { // outside valid date range return null; } - boolean hasDate = (dateOnly(dd) != 0.0d); - boolean hasTime = (timeOnly(dd) != 0.0d); - - Value.Type type = (hasDate ? (hasTime ? Value.Type.DATE_TIME : - Value.Type.DATE) : - Value.Type.TIME); - return ValueSupport.toDateValue(ctx, type, dd); - } - - private static double dateOnly(double dd) { - // the integral part of the date/time double is the date value. discard - // the fractional portion - return (long)dd; - } - - private static double timeOnly(double dd) { - // the fractional part of the date/time double is the time value. discard - // the integral portion - return new BigDecimal(dd).remainder(BigDecimal.ONE).doubleValue(); - } - - private static double currentTimeDouble(LocaleContext ctx) { - return ColumnImpl.toDateDouble(System.currentTimeMillis(), ctx.getCalendar()); + LocalDateTime ldt = ColumnImpl.ldtFromLocalDateDouble(dd); + return ValueSupport.toValue(ldt); } private static int dayOfWeekToWeekDay(int day, int firstDay) { @@ -551,114 +505,85 @@ public class DefaultDateFunctions return getOptionalIntParam(ctx, params, idx, 1, 0); } - private static int weekOfYear(EvalContext ctx, Value param, int firstDay, - int firstWeekType) { - return doWeekOp(nonNullToCalendar(ctx, param), firstDay, firstWeekType, - WeekOpType.GET_WEEK); - } + private static WeekFields weekFields(int firstDay, int firstWeekType) { + + int minDays = 1; + switch(firstWeekType) { + case 1: + // vbUseSystem 0 + // vbFirstJan1 1 (default) + break; + case 2: + // vbFirstFourDays 2 + minDays = 4; + break; + case 3: + // vbFirstFullWeek 3 + minDays = 7; + break; + default: + throw new EvalException("Invalid first week of year type " + + firstWeekType); + } - private static int weekOfYear(Calendar cal, int firstDay, int firstWeekType) { - return doWeekOp(cal, firstDay, firstWeekType, WeekOpType.GET_WEEK); + return WeekFields.of(dayOfWeek(firstDay), minDays); } - private static int weeksInYear(Calendar cal, int firstDay, int firstWeekType) { - return doWeekOp(cal, firstDay, firstWeekType, WeekOpType.GET_NUM_WEEKS); + private static DayOfWeek dayOfWeek(int dayOfWeek) { + return DayOfWeek.SUNDAY.plus(dayOfWeek - 1); } - private static int doWeekOp(Calendar cal, int firstDay, int firstWeekType, - WeekOpType opType) { - // need to mess with some calendar settings, but they need to be restored - // when done because the Calendar instance may be shared - int origFirstDay = cal.getFirstDayOfWeek(); - int origMinDays = cal.getMinimalDaysInFirstWeek(); - try { - - int minDays = 1; - switch(firstWeekType) { - case 1: - // vbUseSystem 0 - // vbFirstJan1 1 (default) - break; - case 2: - // vbFirstFourDays 2 - minDays = 4; - break; - case 3: - // vbFirstFullWeek 3 - minDays = 7; - break; - default: - throw new EvalException("Invalid first week of year type " + - firstWeekType); - } - - cal.setFirstDayOfWeek(firstDay); - cal.setMinimalDaysInFirstWeek(minDays); + private static TextStyle getTextStyle(EvalContext ctx, Value[] params, + int idx) { + boolean abbreviate = getOptionalBooleanParam(ctx, params, 1); + return (abbreviate ? TextStyle.SHORT : TextStyle.FULL); + } - switch(opType) { - case GET_WEEK: - return cal.get(Calendar.WEEK_OF_YEAR); - case GET_NUM_WEEKS: - return cal.getActualMaximum(Calendar.WEEK_OF_YEAR); - default: - throw new RuntimeException("Unknown op type " + opType); - } + private static int weekOfYear(EvalContext ctx, Value param, int firstDay, + int firstWeekType) { + return weekOfYear(param.getAsLocalDateTime(ctx), firstDay, firstWeekType); + } - } finally { - cal.setFirstDayOfWeek(origFirstDay); - cal.setMinimalDaysInFirstWeek(origMinDays); - } + private static int weekOfYear(LocalDateTime ldt, int firstDay, + int firstWeekType) { + WeekFields weekFields = weekFields(firstDay, firstWeekType); + return ldt.get(weekFields.weekOfWeekBasedYear()); } - private static int getQuarter(Calendar cal) { - // month is 0 based - int month = cal.get(Calendar.MONTH); - return (month / 3) + 1; + private static int weeksInYear(int year, WeekFields weekFields) { + return (int)LocalDate.of(year,2,1).range(weekFields.weekOfWeekBasedYear()) + .getMaximum(); } - private static int getWeekOfYearYear(Calendar cal, int weekOfYear) { - // the "week of year" gets weird at the beginning/end of the year. - // e.g. 12/31 might be int the first week of the next year, and 1/1 might - // be in the last week of the previous year. we need to detect this and - // adjust the intervals accordingly - if(cal.get(Calendar.MONTH) == Calendar.JANUARY) { - if(weekOfYear >= 52) { - // this week of year is effectively for the previous year - cal.add(Calendar.YEAR, -1); - } - } else { - if(weekOfYear == 1) { - // this week of year is effectively for next year - cal.add(Calendar.YEAR, 1); - } - } - return cal.get(Calendar.YEAR); + private static int getQuarter(LocalDateTime ldt) { + int month = ldt.getMonthValue() - 1; + return (month / 3) + 1; } - private static int getDayDiff(Calendar cal1, Calendar cal2) { - int y1 = cal1.get(Calendar.YEAR); - int d1 = cal1.get(Calendar.DAY_OF_YEAR); - int y2 = cal2.get(Calendar.YEAR); - int d2 = cal2.get(Calendar.DAY_OF_YEAR); - while(y2 > y1) { - cal2.add(Calendar.YEAR, -1); - d2 += cal2.getActualMaximum(Calendar.DAY_OF_YEAR); - y2 = cal2.get(Calendar.YEAR); + private static int getDayDiff(LocalDateTime ldt1, LocalDateTime ldt2) { + int y1 = ldt1.getYear(); + int d1 = ldt1.getDayOfYear(); + int y2 = ldt2.getYear(); + int d2 = ldt2.getDayOfYear(); + while(y2 > y1) { + ldt2 = ldt2.minusYears(1); + d2 += ldt2.range(ChronoField.DAY_OF_YEAR).getMaximum(); + y2 = ldt2.getYear(); } return d2 - d1; } - private static int getHourDiff(Calendar cal1, Calendar cal2) { - int h1 = cal1.get(Calendar.HOUR_OF_DAY); - int h2 = cal2.get(Calendar.HOUR_OF_DAY); - int dayDiff = getDayDiff(cal1, cal2); + private static int getHourDiff(LocalDateTime ldt1, LocalDateTime ldt2) { + int h1 = ldt1.getHour(); + int h2 = ldt2.getHour(); + int dayDiff = getDayDiff(ldt1, ldt2); return (h2 + (24 * dayDiff)) - h1; } - private static int getMinuteDiff(Calendar cal1, Calendar cal2) { - int m1 = cal1.get(Calendar.MINUTE); - int m2 = cal2.get(Calendar.MINUTE); - int hourDiff = getHourDiff(cal1, cal2); + private static int getMinuteDiff(LocalDateTime ldt1, LocalDateTime ldt2) { + int m1 = ldt1.getMinute(); + int m2 = ldt2.getMinute(); + int hourDiff = getHourDiff(ldt1, ldt2); return (m2 + (60 * hourDiff)) - m1; } } 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 29c0f71..763d1d5 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java @@ -18,10 +18,9 @@ package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; import java.math.BigInteger; -import java.text.DateFormat; import java.text.DecimalFormat; -import java.util.Calendar; -import java.util.Date; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; @@ -310,16 +309,14 @@ public class DefaultFunctions return ValueSupport.NULL_VAL; } - Date d = param1.getAsDateTime(ctx); + LocalDateTime ldt = param1.getAsLocalDateTime(ctx); int fmtType = getOptionalIntParam(ctx, params, 1, 0); TemporalConfig.Type tempType = null; switch(fmtType) { case 0: // vbGeneralDate - Calendar cal = ctx.getCalendar(); - cal.setTime(d); - Value.Type valType = ValueSupport.getDateTimeType(cal); + Value.Type valType = ValueSupport.getDateTimeType(ldt); switch(valType) { case DATE: tempType = TemporalConfig.Type.SHORT_DATE; @@ -351,9 +348,9 @@ public class DefaultFunctions throw new EvalException("Unknown format " + fmtType); } - DateFormat sdf = ctx.createDateFormat( + DateTimeFormatter dtf = ctx.createDateFormatter( ctx.getTemporalConfig().getDateTimeFormat(tempType)); - return ValueSupport.toValue(sdf.format(d)); + return ValueSupport.toValue(dtf.format(ldt)); } }); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java index 1ec08db..70eb5a9 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java @@ -49,7 +49,7 @@ public class DefaultNumberFunctions case DATE_TIME: // dates/times get converted to date doubles for arithmetic double result = Math.abs(param1.getAsDouble(ctx)); - return ValueSupport.toDateValue(ctx, mathType, result); + return ValueSupport.toDateValueIfPossible(mathType, result); case LONG: return ValueSupport.toValue(Math.abs(param1.getAsLongInt(ctx))); case DOUBLE: 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 cc0fca4..4db436a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java @@ -17,26 +17,27 @@ 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.ParsePosition; +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.TemporalAccessor; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; -import java.util.Calendar; -import java.util.Date; import java.util.EnumMap; 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.LocaleContext; import com.healthmarketscience.jackcess.expr.ParseException; import com.healthmarketscience.jackcess.expr.TemporalConfig; import com.healthmarketscience.jackcess.expr.Value; +import com.healthmarketscience.jackcess.impl.ColumnImpl; /** @@ -53,15 +54,6 @@ class ExpressionTokenizer private static final char DATE_LIT_QUOTE_CHAR = '#'; private static final char EQUALS_CHAR = '='; - // access times are based on this date (not the UTC base) - static final int BASE_DATE_YEAR = 1899; - static final int BASE_DATE_MONTH = 12; - static final int BASE_DATE_DAY = 30; - private static final String BASE_DATE_PREFIX = "1899/12/30 "; - private static final String BASE_DATE_FMT_PREFIX = "yyyy/M/d "; - - private static final String IMPLICIT_YEAR_FMT_PREFIX = "yyyy "; - private static final byte IS_OP_FLAG = 0x01; private static final byte IS_COMP_FLAG = 0x02; private static final byte IS_DELIM_FLAG = 0x04; @@ -305,14 +297,26 @@ class ExpressionTokenizer // note that although we may parse in the time "24" format, we will // display as the default time format - DateFormat parseDf = buf.getParseDateTimeFormat(type); + DateTimeFormatter parseDf = buf.getParseDateTimeFormat(type); try { - return new Token(TokenType.LITERAL, parseComplete(parseDf, dateStr), + TemporalAccessor parsedInfo = parseDf.parse(dateStr); + + LocalDate ld = ColumnImpl.BASE_LD; + if(type.includesDate()) { + ld = LocalDate.from(parsedInfo); + } + + LocalTime lt = ColumnImpl.BASE_LT; + if(type.includesTime()) { + lt = LocalTime.from(parsedInfo); + } + + return new Token(TokenType.LITERAL, LocalDateTime.of(ld, lt), dateStr, type.getValueType()); - } catch(java.text.ParseException pe) { + } catch(DateTimeException de) { throw new ParseException( - "Invalid date/time literal " + dateStr + " " + buf, pe); + "Invalid date/time literal " + dateStr + " " + buf, de); } } @@ -325,7 +329,7 @@ class ExpressionTokenizer boolean hasAmPm = false; if(hasTime) { - String[] amPmStrs = cfg.getDateFormatSymbols().getAmPmStrings(); + String[] amPmStrs = cfg.getAmPmStrings(); String amStr = " " + amPmStrs[0]; String pmStr = " " + amPmStrs[1]; hasAmPm = (hasSuffix(dateStr, amStr) || hasSuffix(dateStr, pmStr)); @@ -352,23 +356,6 @@ class ExpressionTokenizer suffStr, 0, suffStrLen)); } - static DateFormat createParseDateTimeFormat(TemporalConfig.Type type, - LocaleContext ctx) - { - if(type.isTimeOnly()) { - return new ParseTimeFormat(type, ctx); - } - - TemporalConfig cfg = ctx.getTemporalConfig(); - return ctx.createDateFormat(cfg.getDateTimeFormat(type)); - } - - static DateFormat createParseImplicitYearDateTimeFormat( - TemporalConfig.Type type, LocaleContext ctx) - { - return new ParseImplicitYearFormat(type, ctx); - } - private static Token maybeParseNumberLiteral(char firstChar, ExprBuf buf) { StringBuilder sb = buf.getScratchBuffer().append(firstChar); boolean hasDigit = isDigit(firstChar); @@ -464,29 +451,14 @@ class ExpressionTokenizer return new AbstractMap.SimpleImmutableEntry(a, b); } - static Date parseComplete(DateFormat df, String str) - throws java.text.ParseException - { - // the java parsers will parse "successfully" even if there is leftover - // information. we only want to consider a parse operation successful if - // it parses the entire string (ignoring surrounding whitespace) - str = str.trim(); - ParsePosition pp = new ParsePosition(0); - Object d = df.parse(str, pp); - if(pp.getIndex() < str.length()) { - throw new java.text.ParseException("Failed parsing '" + str + "'", - pp.getIndex()); - } - return (Date)d; - } - private static final class ExprBuf { private final String _str; private final ParseContext _ctx; private int _pos; - private final Map _dateTimeFmts = - new EnumMap(TemporalConfig.Type.class); + private final Map _dateTimeFmts = + new EnumMap( + TemporalConfig.Type.class); private final StringBuilder _scratch = new StringBuilder(); private ExprBuf(String str, ParseContext ctx) { @@ -538,10 +510,11 @@ class ExpressionTokenizer return _ctx; } - public DateFormat getParseDateTimeFormat(TemporalConfig.Type type) { - DateFormat df = _dateTimeFmts.get(type); + public DateTimeFormatter getParseDateTimeFormat(TemporalConfig.Type type) { + DateTimeFormatter df = _dateTimeFmts.get(type); if(df == null) { - df = createParseDateTimeFormat(type, _ctx); + df = _ctx.createDateFormatter( + _ctx.getTemporalConfig().getDateTimeFormat(type)); _dateTimeFmts.put(type, df); } return df; @@ -605,98 +578,4 @@ class ExpressionTokenizer } } - /** - * Base DateFormat implementation for parsing date/time formats where - * additional information is added on to the format in order for it to be - * parsed correctly. - */ - private static abstract class ParsePrefixFormat extends DateFormat - { - private static final long serialVersionUID = 0L; - - private final DateFormat _parseDelegate; - - private ParsePrefixFormat(String formatPrefix, String formatStr, - LocaleContext ctx) { - _parseDelegate = ctx.createDateFormat(formatPrefix + formatStr); - } - - @Override - public StringBuffer format(Date date, StringBuffer toAppendTo, - FieldPosition fieldPosition) { - throw new UnsupportedOperationException(); - } - - @Override - public Date parse(String source, ParsePosition pos) { - String prefix = getPrefix(); - - Date result = _parseDelegate.parse(prefix + source, pos); - - // adjust index for original string - pos.setIndex(pos.getIndex() - prefix.length()); - - return result; - } - - @Override - public Calendar getCalendar() { - return _parseDelegate.getCalendar(); - } - - @Override - public TimeZone getTimeZone() { - return _parseDelegate.getTimeZone(); - } - - protected abstract String getPrefix(); - } - - /** - * Special date/time format which will parse time-only strings "correctly" - * according to how access handles time-only values. - */ - private static final class ParseTimeFormat extends ParsePrefixFormat - { - private static final long serialVersionUID = 0L; - - private ParseTimeFormat(TemporalConfig.Type timeType, LocaleContext ctx) { - super(BASE_DATE_FMT_PREFIX, - ctx.getTemporalConfig().getDateTimeFormat(timeType), ctx); - } - - @Override - protected String getPrefix() { - // we parse as a full date/time in order to get the correct "base date" - // used by access - return BASE_DATE_PREFIX; - } - } - - /** - * Special date/time format which will parse dates with implicit (current) - * years. - */ - private static final class ParseImplicitYearFormat extends ParsePrefixFormat - { - private static final long serialVersionUID = 0L; - - private ParseImplicitYearFormat(TemporalConfig.Type type, - LocaleContext ctx) { - super(IMPLICIT_YEAR_FMT_PREFIX, - ctx.getTemporalConfig().getImplicitYearDateTimeFormat(type), - ctx); - } - - @Override - protected String getPrefix() { - // need to get the current year - Calendar cal = getCalendar(); - cal.setTimeInMillis(System.currentTimeMillis()); - int year = cal.get(Calendar.YEAR); - // return a value matching IMPLICIT_YEAR_FMT_PREFIX - return year + " "; - } - } - } 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 bee27ca..92da3f6 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java @@ -17,13 +17,11 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.text.DateFormat; -import java.text.SimpleDateFormat; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; @@ -43,7 +41,6 @@ import com.healthmarketscience.jackcess.expr.FunctionLookup; import com.healthmarketscience.jackcess.expr.Identifier; import com.healthmarketscience.jackcess.expr.LocaleContext; import com.healthmarketscience.jackcess.expr.ParseException; -import com.healthmarketscience.jackcess.expr.TemporalConfig; import com.healthmarketscience.jackcess.expr.Value; import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.Token; import com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.TokenType; @@ -68,8 +65,6 @@ public class Expressionator } public interface ParseContext extends LocaleContext { - public TemporalConfig getTemporalConfig(); - public SimpleDateFormat createDateFormat(String formatStr); public FunctionLookup getFunctionLookup(); } @@ -1318,7 +1313,7 @@ public class Expressionator case DATE: case TIME: case DATE_TIME: - return ValueSupport.toValue(valType, (Date)value); + return ValueSupport.toValue(valType, (LocalDateTime)value); case LONG: return ValueSupport.toValue((Integer)value); case DOUBLE: @@ -2081,7 +2076,7 @@ public class Expressionator case DATE: case TIME: case DATE_TIME: - return val.getAsDateTime(ctx); + return val.getAsLocalDateTime(ctx); case LONG: return val.getAsLongInt(ctx); case DOUBLE: diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java index 2c475eb..a21cd88 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java @@ -17,9 +17,9 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.text.DateFormat; import java.text.DecimalFormat; import java.text.NumberFormat; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Map; @@ -172,10 +172,10 @@ public class FormatUtil @Override public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - DateFormat sdf = ctx.createDateFormat( + int firstDay, int firstWeekType) { + DateTimeFormatter dtf = ctx.createDateFormatter( ctx.getTemporalConfig().getDateTimeFormat(_type)); - return ValueSupport.toValue(sdf.format(expr.getAsDateTime(ctx))); + return ValueSupport.toValue(dtf.format(expr.getAsLocalDateTime(ctx))); } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java index 197d8b5..ca74f35 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java @@ -17,7 +17,6 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.math.BigInteger; import java.text.DecimalFormatSymbols; import com.healthmarketscience.jackcess.expr.EvalException; @@ -84,7 +83,7 @@ public class StringValue extends BaseValue // numberToDateValue may return null for out of range numbers) try { dateValue = DefaultDateFunctions.numberToDateValue( - ctx, getNumber(ctx).doubleValue()); + getNumber(ctx).doubleValue()); } catch(EvalException ignored) { // not a number, not a date/time } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ValueSupport.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ValueSupport.java index 3040920..805cc32 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ValueSupport.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ValueSupport.java @@ -18,9 +18,10 @@ package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; import java.math.BigInteger; -import java.text.DateFormat; -import java.util.Calendar; -import java.util.Date; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import java.util.regex.Pattern; import com.healthmarketscience.jackcess.expr.EvalException; @@ -99,36 +100,42 @@ public class ValueSupport return new BigDecimalValue(normalize(s)); } - public static Value toDateValue(LocaleContext ctx, Value.Type type, double dd) - { - return toValue(type, new Date( - ColumnImpl.fromDateDouble(dd, ctx.getCalendar()))); + static Value toDateValueIfPossible(Value.Type dateType, double dd) { + if(DefaultDateFunctions.isValidDateDouble(dd)) { + return ValueSupport.toValue( + dateType, ColumnImpl.ldtFromLocalDateDouble(dd)); + } + return ValueSupport.toValue(dd); + } + + public static Value toValue(LocalDate ld) { + return new DateTimeValue( + Value.Type.DATE, LocalDateTime.of(ld, ColumnImpl.BASE_LT)); } - public static Value toValue(Calendar cal) { - return new DateTimeValue(getDateTimeType(cal), cal.getTime()); + public static Value toValue(LocalTime lt) { + return new DateTimeValue( + Value.Type.TIME, LocalDateTime.of(ColumnImpl.BASE_LD, lt)); } - public static Value.Type getDateTimeType(Calendar cal) { - boolean hasTime = ((cal.get(Calendar.HOUR_OF_DAY) != 0) || - (cal.get(Calendar.MINUTE) != 0) || - (cal.get(Calendar.SECOND) != 0)); + public static Value toValue(LocalDateTime ldt) { + return new DateTimeValue(getDateTimeType(ldt), ldt); + } - boolean hasDate = - ((cal.get(Calendar.YEAR) != ExpressionTokenizer.BASE_DATE_YEAR) || - ((cal.get(Calendar.MONTH) + 1) != ExpressionTokenizer.BASE_DATE_MONTH) || - (cal.get(Calendar.DAY_OF_MONTH) != ExpressionTokenizer.BASE_DATE_DAY)); + public static Value.Type getDateTimeType(LocalDateTime ldt) { + boolean hasDate = !ColumnImpl.BASE_LD.equals(ldt.toLocalDate()); + boolean hasTime = !ColumnImpl.BASE_LT.equals(ldt.toLocalTime()); return (hasDate ? (hasTime ? Value.Type.DATE_TIME : Value.Type.DATE) : Value.Type.TIME); } - public static Value toValue(Value.Type type, Date d) { - return new DateTimeValue(type, d); + public static Value toValue(Value.Type type, LocalDateTime ldt) { + return new DateTimeValue(type, ldt); } - public static DateFormat getDateFormatForType(LocaleContext ctx, Value.Type type) { + public static DateTimeFormatter getDateFormatForType(LocaleContext ctx, Value.Type type) { String fmtStr = null; switch(type) { case DATE: @@ -143,7 +150,7 @@ public class ValueSupport default: throw new EvalException("Unexpected date/time type " + type); } - return ctx.createDateFormat(fmtStr); + return ctx.createDateFormatter(fmtStr); } /** diff --git a/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java b/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java index 384386e..bb26719 100644 --- a/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java @@ -294,8 +294,8 @@ public class PropertyExpressionTest extends TestCase public static void testCustomEvalConfig() throws Exception { - TemporalConfig tempConf = new TemporalConfig("yyyy/M/d", "M/d", - "yyyy-MMM-d", + TemporalConfig tempConf = new TemporalConfig("[uuuu/]M/d", + "uuuu-MMM-d", "hh.mm.ss a", "HH.mm.ss", '/', '.', Locale.US); diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/DatabaseReadWriteTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/DatabaseReadWriteTest.java index 561f1e8..104b266 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/DatabaseReadWriteTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/DatabaseReadWriteTest.java @@ -16,6 +16,9 @@ limitations under the License. package com.healthmarketscience.jackcess.impl; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -53,7 +56,7 @@ public class DatabaseReadWriteTest extends TestCase db.close(); } } - + public void testWriteAndReadInMem() throws Exception { for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { Database db = createMem(fileFormat); @@ -61,7 +64,7 @@ public class DatabaseReadWriteTest extends TestCase db.close(); } } - + private static void doTestWriteAndRead(Database db) throws Exception { createTestTable(db); Object[] row = createTestRow(); @@ -117,7 +120,7 @@ public class DatabaseReadWriteTest extends TestCase } } - public void testUpdateRow() throws Exception + public void testUpdateRow() throws Exception { for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { Database db = createMem(fileFormat); @@ -250,11 +253,18 @@ public class DatabaseReadWriteTest extends TestCase final long timeRange = 100000000L; final long timeStep = 37L; - for(long time = testTime - timeRange; time < testTime + timeRange; + for(long time = testTime - timeRange; time < testTime + timeRange; time += timeStep) { double accTime = ColumnImpl.toLocalDateDouble(time); long newTime = ColumnImpl.fromLocalDateDouble(accTime); assertEquals(time, newTime); + + Instant inst = Instant.ofEpochMilli(time); + LocalDateTime ldt = LocalDateTime.ofInstant(inst, ZoneOffset.UTC); + + accTime = ColumnImpl.toDateDouble(ldt); + LocalDateTime newLdt = ColumnImpl.ldtFromLocalDateDouble(accTime); + assertEquals(ldt, newLdt); } } } diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java index b50f2de..895bbed 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java @@ -17,6 +17,7 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; +import java.time.LocalDateTime; import java.util.Calendar; import java.util.Date; @@ -78,9 +79,9 @@ public class DefaultFunctionsTest extends TestCase eval("=CSng(\"57.12345\")")); assertEval("9786", "=CStr(9786)"); assertEval("-42", "=CStr(-42)"); - assertEval(new Date(1041483600000L), "=CDate('01/02/2003')"); - assertEval(new Date(1041508800000L), "=CDate('01/02/2003 7:00:00 AM')"); - assertEval(new Date(-1948781520000L), "=CDate(3013.45)"); + assertEval(LocalDateTime.of(2003,1,2,0,0), "=CDate('01/02/2003')"); + assertEval(LocalDateTime.of(2003,1,2,7,0), "=CDate('01/02/2003 7:00:00 AM')"); + assertEval(LocalDateTime.of(1908,3,31,10,48), "=CDate(3013.45)"); assertEval(-1, "=IsNull(Null)"); 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 c67dfe7..67ad20b 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java @@ -20,14 +20,13 @@ import java.io.BufferedReader; import java.io.FileReader; import java.math.BigDecimal; import java.text.DecimalFormat; -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import javax.script.Bindings; import javax.script.SimpleBindings; import com.healthmarketscience.jackcess.DataType; -import com.healthmarketscience.jackcess.DatabaseBuilder; import com.healthmarketscience.jackcess.TestUtil; import com.healthmarketscience.jackcess.expr.EvalContext; import com.healthmarketscience.jackcess.expr.Expression; @@ -38,7 +37,6 @@ import com.healthmarketscience.jackcess.expr.ParseException; import com.healthmarketscience.jackcess.expr.TemporalConfig; import com.healthmarketscience.jackcess.expr.Value; import com.healthmarketscience.jackcess.impl.BaseEvalContext; -import com.healthmarketscience.jackcess.impl.expr.NumberFormatter; import junit.framework.TestCase; /** @@ -318,11 +316,11 @@ public class ExpressionatorTest extends TestCase public void testDateArith() throws Exception { - assertEquals(new Date(1041508800000L), eval("=#01/02/2003# + #7:00:00 AM#")); - assertEquals(new Date(1041458400000L), eval("=#01/02/2003# - #7:00:00 AM#")); - assertEquals(new Date(1044680400000L), eval("=#01/02/2003# + '37'")); - assertEquals(new Date(1044680400000L), eval("='37' + #01/02/2003#")); - assertEquals(new Date(1041508800000L), eval("=#01/02/2003 7:00:00 AM#")); + assertEquals(LocalDateTime.of(2003,1,2,7,0), eval("=#01/02/2003# + #7:00:00 AM#")); + assertEquals(LocalDateTime.of(2003,1,1,17,0), eval("=#01/02/2003# - #7:00:00 AM#")); + assertEquals(LocalDateTime.of(2003,2,8,0,0), eval("=#01/02/2003# + '37'")); + assertEquals(LocalDateTime.of(2003,2,8,0,0), eval("='37' + #01/02/2003#")); + assertEquals(LocalDateTime.of(2003,1,2,7,0), eval("=#01/02/2003 7:00:00 AM#")); assertEquals("2/8/2003", eval("=CStr(#01/02/2003# + '37')")); assertEquals("9:24:00 AM", eval("=CStr(#7:00:00 AM# + 0.1)")); @@ -404,7 +402,7 @@ public class ExpressionatorTest extends TestCase assertEquals("foo37", eval("=\"foo\" + (12 + 25)")); assertEquals("25foo12", eval("=\"25foo\" + 12")); - assertEquals(new Date(1485579600000L), eval("=#1/1/2017# + 27")); + assertEquals(LocalDateTime.of(2017,1,28,0,0), eval("=#1/1/2017# + 27")); assertEquals(128208, eval("=#1/1/2017# * 3")); } @@ -590,15 +588,14 @@ public class ExpressionatorTest extends TestCase return TemporalConfig.US_TEMPORAL_CONFIG; } - public SimpleDateFormat createDateFormat(String formatStr) { - SimpleDateFormat sdf = DatabaseBuilder.createDateFormat(formatStr); - sdf.setTimeZone(TestUtil.TEST_TZ); - return sdf; + public DateTimeFormatter createDateFormatter(String formatStr) { + DateTimeFormatter dtf = DateTimeFormatter.ofPattern( + formatStr, TemporalConfig.US_TEMPORAL_CONFIG.getLocale()); + return dtf; } - public Calendar getCalendar() { - return createDateFormat(getTemporalConfig().getDefaultDateTimeFormat()) - .getCalendar(); + public ZoneId getZoneId() { + return TestUtil.TEST_TZ.toZoneId(); } public NumericConfig getNumericConfig() { -- cgit v1.2.3 From 21fe1417bb49bbdba8591377c221a4417a8851ee Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Sat, 15 Dec 2018 16:09:46 +0000 Subject: use interface to pass zone info into date/time conversion methods git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1239 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/expr/TemporalConfig.java | 4 +- .../jackcess/impl/ColumnImpl.java | 68 +++++++++++++--------- .../jackcess/impl/DatabaseImpl.java | 2 +- .../jackcess/impl/ZoneContext.java | 32 ++++++++++ .../healthmarketscience/jackcess/DatabaseTest.java | 4 +- .../jackcess/LocalDateTimeTest.java | 4 +- .../healthmarketscience/jackcess/TableTest.java | 2 +- 7 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/healthmarketscience/jackcess/impl/ZoneContext.java (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java b/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java index b441c88..cfe08e1 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java @@ -146,8 +146,8 @@ public class TemporalConfig /** * Instantiates a new TemporalConfig with the given configuration. Note * that the date/time format variants will be created by concatenating the - * relevant date and time formats, separated by a single space, e.g. "<date> - * <time>". + * relevant date and time formats, separated by a single space, + * e.g. "<date> <time>". * * @param dateFormat the date (no time) format * @param timeFormat12 the 12 hour time format diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index 273a62a..8e67d60 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -76,7 +76,8 @@ import org.apache.commons.logging.LogFactory; * @author Tim McCune * @usage _intermediate_class_ */ -public class ColumnImpl implements Column, Comparable { +public class ColumnImpl implements Column, Comparable, ZoneContext +{ protected static final Log LOG = LogFactory.getLog(ColumnImpl.class); @@ -358,10 +359,12 @@ public class ColumnImpl implements Column, Comparable { // base does nothing } + @Override public TableImpl getTable() { return _table; } + @Override public DatabaseImpl getDatabase() { return getTable().getDatabase(); } @@ -380,14 +383,17 @@ public class ColumnImpl implements Column, Comparable { return getDatabase().getPageChannel(); } + @Override public String getName() { return _name; } + @Override public boolean isVariableLength() { return _variableLength; } + @Override public boolean isAutoNumber() { return _autoNumber; } @@ -399,6 +405,7 @@ public class ColumnImpl implements Column, Comparable { return _columnNumber; } + @Override public int getColumnIndex() { return _columnIndex; } @@ -417,22 +424,27 @@ public class ColumnImpl implements Column, Comparable { return _displayIndex; } + @Override public DataType getType() { return _type; } + @Override public int getSQLType() throws SQLException { return _type.getSQLType(); } + @Override public boolean isCompressedUnicode() { return false; } + @Override public byte getPrecision() { return (byte)getType().getDefaultPrecision(); } + @Override public byte getScale() { return (byte)getType().getDefaultScale(); } @@ -451,14 +463,17 @@ public class ColumnImpl implements Column, Comparable { return 0; } + @Override public short getLength() { return _columnLength; } + @Override public short getLengthInUnits() { return (short)getType().toUnitSize(getLength()); } + @Override public boolean isCalculated() { return _calculated; } @@ -481,11 +496,13 @@ public class ColumnImpl implements Column, Comparable { return getDatabase().getCharset(); } - protected TimeZone getTimeZone() { + @Override + public TimeZone getTimeZone() { return getDatabase().getTimeZone(); } - protected ZoneId getZoneId() { + @Override + public ZoneId getZoneId() { return getDatabase().getZoneId(); } @@ -493,10 +510,12 @@ public class ColumnImpl implements Column, Comparable { return getDatabase().getDateTimeFactory(); } + @Override public boolean isAppendOnly() { return (getVersionHistoryColumn() != null); } + @Override public ColumnImpl getVersionHistoryColumn() { return null; } @@ -516,10 +535,12 @@ public class ColumnImpl implements Column, Comparable { throw new UnsupportedOperationException(); } + @Override public boolean isHyperlink() { return false; } + @Override public ComplexColumnInfo getComplexInfo() { return null; } @@ -610,12 +631,14 @@ public class ColumnImpl implements Column, Comparable { reloadPropertiesValidators(); } + @Override public ColumnValidator getColumnValidator() { // unwrap any "internal" validator return ((_validator instanceof InternalColumnValidator) ? ((InternalColumnValidator)_validator).getExternal() : _validator); } + @Override public void setColumnValidator(ColumnValidator newValidator) { if(isAutoNumber()) { @@ -676,6 +699,7 @@ public class ColumnImpl implements Column, Comparable { return _autoNumberGenerator; } + @Override public PropertyMap getProperties() throws IOException { if(_props == null) { _props = getTable().getPropertyMaps().get(getName()); @@ -683,20 +707,24 @@ public class ColumnImpl implements Column, Comparable { return _props; } + @Override public Object setRowValue(Object[] rowArray, Object value) { rowArray[_columnIndex] = value; return value; } + @Override public Object setRowValue(Map rowMap, Object value) { rowMap.put(_name, value); return value; } + @Override public Object getRowValue(Object[] rowArray) { return rowArray[_columnIndex]; } + @Override public Object getRowValue(Map rowMap) { return rowMap.get(_name); } @@ -997,43 +1025,32 @@ public class ColumnImpl implements Column, Comparable { throws InvalidValueException { try { - return toDateDouble(value, getTimeZone(), getZoneId()); + return toDateDouble(value, this); } catch(IllegalArgumentException iae) { throw new InvalidValueException(withErrorContext(iae.getMessage()), iae); } } - /** - * Returns an access date double converted from a java Date/Calendar/Number - * time value. - * @usage _advanced_method_ - */ - private static double toDateDouble(Object value, DatabaseImpl db) - { - return toDateDouble(value, db.getTimeZone(), db.getZoneId()); - } - /** * Returns an access date double converted from a java * Date/Calendar/Number/Temporal time value. * @usage _advanced_method_ */ - private static double toDateDouble(Object value, TimeZone tz, ZoneId zoneId) + private static double toDateDouble(Object value, ZoneContext zc) { if(value instanceof TemporalAccessor) { - return toDateDouble( - toLocalDateTime((TemporalAccessor)value, tz, zoneId)); + return toDateDouble(toLocalDateTime((TemporalAccessor)value, zc)); } // 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, tz); + time += getToLocalTimeZoneOffset(time, zc.getTimeZone()); return toLocalDateDouble(time); } private static LocalDateTime toLocalDateTime( - TemporalAccessor value, TimeZone tz, ZoneId zoneId) { + TemporalAccessor value, ZoneContext zc) { // handle some common Temporal types if(value instanceof LocalDateTime) { @@ -1042,10 +1059,10 @@ public class ColumnImpl implements Column, Comparable { if(value instanceof ZonedDateTime) { // if the temporal value has a timezone, convert it to this db's timezone return ((ZonedDateTime)value).withZoneSameInstant( - getZoneId(tz, zoneId)).toLocalDateTime(); + zc.getZoneId()).toLocalDateTime(); } if(value instanceof Instant) { - return LocalDateTime.ofInstant((Instant)value, getZoneId(tz, zoneId)); + return LocalDateTime.ofInstant((Instant)value, zc.getZoneId()); } if(value instanceof LocalDate) { return ((LocalDate)value).atTime(BASE_LT); @@ -1069,7 +1086,7 @@ public class ColumnImpl implements Column, Comparable { if(zone != null) { // the Temporal has a zone, see if it is the right zone. if not, // adjust it - zoneId = getZoneId(tz, zoneId); + ZoneId zoneId = zc.getZoneId(); if(!zoneId.equals(zone)) { return ZonedDateTime.of(ld, lt, zone).withZoneSameInstant(zoneId) .toLocalDateTime(); @@ -1084,10 +1101,6 @@ public class ColumnImpl implements Column, Comparable { } } - private static ZoneId getZoneId(TimeZone tz, ZoneId zoneId) { - return ((zoneId != null) ? zoneId : tz.toZoneId()); - } - static double toLocalDateDouble(long time) { time += MILLIS_BETWEEN_EPOCH_AND_1900; @@ -1692,6 +1705,7 @@ public class ColumnImpl implements Column, Comparable { * Orders Columns by column number. * @usage _general_method_ */ + @Override public int compareTo(ColumnImpl other) { if (_columnNumber > other.getColumnNumber()) { return 1; @@ -2707,7 +2721,7 @@ public class ColumnImpl implements Column, Comparable { @Override public Object toInternalValue(DatabaseImpl db, Object value) { if(value instanceof TemporalAccessor) { - return toLocalDateTime((TemporalAccessor)value, null, db.getZoneId()); + return toLocalDateTime((TemporalAccessor)value, db); } Instant inst = Instant.ofEpochMilli(toDateLong(value)); return LocalDateTime.ofInstant(inst, db.getZoneId()); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index 2ff624e..c5f5481 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -89,7 +89,7 @@ import org.apache.commons.logging.LogFactory; * @author Tim McCune * @usage _intermediate_class_ */ -public class DatabaseImpl implements Database +public class DatabaseImpl implements Database, ZoneContext { private static final Log LOG = LogFactory.getLog(DatabaseImpl.class); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ZoneContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/ZoneContext.java new file mode 100644 index 0000000..0134e1f --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ZoneContext.java @@ -0,0 +1,32 @@ +/* +Copyright (c) 2018 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; + +import java.time.ZoneId; +import java.util.TimeZone; + +/** + * Provider of zone related info for date/time conversions. + * + * @author James Ahlborn + */ +interface ZoneContext +{ + public ZoneId getZoneId(); + + public TimeZone getTimeZone(); +} diff --git a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java index de6bd94..209a63d 100644 --- a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -991,9 +991,9 @@ public class DatabaseTest extends TestCase { ColumnImpl col = new ColumnImpl(null, null, DataType.SHORT_DATE_TIME, 0, 0, 0) { @Override - protected TimeZone getTimeZone() { return tz; } + public TimeZone getTimeZone() { return tz; } @Override - protected ZoneId getZoneId() { return null; } + public ZoneId getZoneId() { return null; } }; SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd"); diff --git a/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java index 5dde831..637629a 100644 --- a/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java @@ -127,9 +127,9 @@ public class LocalDateTimeTest extends TestCase final TimeZone tz = TimeZone.getTimeZone(zoneId); ColumnImpl col = new ColumnImpl(null, null, DataType.SHORT_DATE_TIME, 0, 0, 0) { @Override - protected TimeZone getTimeZone() { return tz; } + public TimeZone getTimeZone() { return tz; } @Override - protected ZoneId getZoneId() { return zoneId; } + public ZoneId getZoneId() { return zoneId; } }; SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd"); diff --git a/src/test/java/com/healthmarketscience/jackcess/TableTest.java b/src/test/java/com/healthmarketscience/jackcess/TableTest.java index 3bc2dbd..eaae617 100644 --- a/src/test/java/com/healthmarketscience/jackcess/TableTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/TableTest.java @@ -185,7 +185,7 @@ public class TableTest extends TestCase { return getFormat().CHARSET; } @Override - protected TimeZone getTimeZone() { + public TimeZone getTimeZone() { return TimeZone.getDefault(); } @Override -- cgit v1.2.3 From c1aa151cd41d0b8da5f16ac2b40bdc7f084f7d70 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Mon, 17 Dec 2018 23:10:13 +0000 Subject: add LocalDateTime variants for various public Date based methods git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1241 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../healthmarketscience/jackcess/DateTimeType.java | 4 +- .../java/com/healthmarketscience/jackcess/Row.java | 10 +- .../jackcess/complex/Attachment.java | 28 ++++- .../jackcess/complex/ComplexValueForeignKey.java | 46 ++++++-- .../jackcess/complex/Version.java | 10 ++ .../healthmarketscience/jackcess/impl/RowImpl.java | 1 + .../impl/complex/AttachmentColumnInfoImpl.java | 34 ++++-- .../impl/complex/ComplexValueForeignKeyImpl.java | 123 +++++++++++++++------ .../impl/complex/VersionHistoryColumnInfoImpl.java | 68 +++++++----- .../jackcess/ComplexColumnTest.java | 73 ++++++------ .../healthmarketscience/jackcess/DatabaseTest.java | 1 + .../com/healthmarketscience/jackcess/TestUtil.java | 1 + 12 files changed, 278 insertions(+), 121 deletions(-) (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/DateTimeType.java b/src/main/java/com/healthmarketscience/jackcess/DateTimeType.java index 7f5cdb1..8704350 100644 --- a/src/main/java/com/healthmarketscience/jackcess/DateTimeType.java +++ b/src/main/java/com/healthmarketscience/jackcess/DateTimeType.java @@ -17,7 +17,9 @@ limitations under the License. package com.healthmarketscience.jackcess; /** - * Enum for selecting how a Database returns date/time types. + * Enum for selecting how a Database returns date/time types. Prefer using + * {@link DateTimeType#LOCAL_DATE_TIME} as using Date is being phased out and + * will eventually be removed. * * @author James Ahlborn */ diff --git a/src/main/java/com/healthmarketscience/jackcess/Row.java b/src/main/java/com/healthmarketscience/jackcess/Row.java index 6917628..8fcaf67 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Row.java +++ b/src/main/java/com/healthmarketscience/jackcess/Row.java @@ -91,12 +91,20 @@ public interface Row extends Map /** * Convenience method which gets the value for the row with the given name, * casting it to a Date (DataType SHORT_DATE_TIME). + * @deprecated this is only valid for Database instances configured for the + * legacy {@link DateTimeType#DATE}. Prefer using + * {@link DateTimeType#LOCAL_DATE_TIME} and the corresponding + * {@link #getLocalDateTime} method. Using Date is being phased + * out and will eventually be removed. */ + @Deprecated public Date getDate(String name); /** * Convenience method which gets the value for the row with the given name, - * casting it to a LocalDateTime (DataType SHORT_DATE_TIME). + * casting it to a LocalDateTime (DataType SHORT_DATE_TIME). This method + * will only work for Database instances configured for + * {@link DateTimeType#LOCAL_DATE_TIME}. */ public LocalDateTime getLocalDateTime(String name); diff --git a/src/main/java/com/healthmarketscience/jackcess/complex/Attachment.java b/src/main/java/com/healthmarketscience/jackcess/complex/Attachment.java index d35559e..0047719 100644 --- a/src/main/java/com/healthmarketscience/jackcess/complex/Attachment.java +++ b/src/main/java/com/healthmarketscience/jackcess/complex/Attachment.java @@ -17,14 +17,16 @@ limitations under the License. package com.healthmarketscience.jackcess.complex; import java.io.IOException; +import java.time.LocalDateTime; import java.util.Date; +import com.healthmarketscience.jackcess.DateTimeType; /** * Complex value corresponding to an attachment. * * @author James Ahlborn */ -public interface Attachment extends ComplexValue +public interface Attachment extends ComplexValue { public byte[] getFileData() throws IOException; @@ -37,20 +39,34 @@ public interface Attachment extends ComplexValue public String getFileName(); public void setFileName(String fileName); - + public String getFileUrl(); public void setFileUrl(String fileUrl); - + public String getFileType(); public void setFileType(String fileType); - + + /** + * @deprecated see {@link DateTimeType} for details + */ + @Deprecated public Date getFileTimeStamp(); + /** + * @deprecated see {@link DateTimeType} for details + */ + @Deprecated public void setFileTimeStamp(Date fileTimeStamp); - + + public LocalDateTime getFileLocalTimeStamp(); + + public void setFileLocalTimeStamp(LocalDateTime fileTimeStamp); + + public Object getFileTimeStampObject(); + public Integer getFileFlags(); - public void setFileFlags(Integer fileFlags); + public void setFileFlags(Integer fileFlags); } diff --git a/src/main/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java b/src/main/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java index 150dd07..0e0bc13 100644 --- a/src/main/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java +++ b/src/main/java/com/healthmarketscience/jackcess/complex/ComplexValueForeignKey.java @@ -18,10 +18,13 @@ package com.healthmarketscience.jackcess.complex; import java.io.IOException; import java.io.ObjectStreamException; +import java.time.LocalDateTime; import java.util.Date; import java.util.List; import java.util.Map; + import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.DateTimeType; /** @@ -40,33 +43,33 @@ import com.healthmarketscience.jackcess.Column; */ public abstract class ComplexValueForeignKey extends Number { - private static final long serialVersionUID = 20130319L; + private static final long serialVersionUID = 20130319L; @Override public byte byteValue() { return (byte)get(); } - + @Override public short shortValue() { return (short)get(); } - + @Override public int intValue() { return get(); } - + @Override public long longValue() { return get(); } - + @Override public float floatValue() { return get(); } - + @Override public double doubleValue() { return get(); @@ -78,12 +81,12 @@ public abstract class ComplexValueForeignKey extends Number // of jackcess) return Integer.valueOf(get()); } - + @Override public int hashCode() { return get(); } - + @Override public boolean equals(Object o) { return ((this == o) || @@ -94,7 +97,7 @@ public abstract class ComplexValueForeignKey extends Number @Override public String toString() { return String.valueOf(get()); - } + } public abstract int get(); @@ -122,25 +125,50 @@ public abstract class ComplexValueForeignKey extends Number public abstract Version addVersion(String value) throws IOException; + /** + * @deprecated see {@link DateTimeType} for details + */ + @Deprecated public abstract Version addVersion(String value, Date modifiedDate) throws IOException; + public abstract Version addVersion(String value, LocalDateTime modifiedDate) + throws IOException; + public abstract Attachment addAttachment(byte[] data) throws IOException; + /** + * @deprecated see {@link DateTimeType} for details + */ + @Deprecated public abstract Attachment addAttachment( String url, String name, String type, byte[] data, Date timeStamp, Integer flags) throws IOException; + public abstract Attachment addAttachment( + String url, String name, String type, byte[] data, + LocalDateTime timeStamp, Integer flags) + throws IOException; + public abstract Attachment addEncodedAttachment(byte[] encodedData) throws IOException; + /** + * @deprecated see {@link DateTimeType} for details + */ + @Deprecated public abstract Attachment addEncodedAttachment( String url, String name, String type, byte[] encodedData, Date timeStamp, Integer flags) throws IOException; + public abstract Attachment addEncodedAttachment( + String url, String name, String type, byte[] encodedData, + LocalDateTime timeStamp, Integer flags) + throws IOException; + public abstract Attachment updateAttachment(Attachment attachment) throws IOException; diff --git a/src/main/java/com/healthmarketscience/jackcess/complex/Version.java b/src/main/java/com/healthmarketscience/jackcess/complex/Version.java index a1ace1b..374e047 100644 --- a/src/main/java/com/healthmarketscience/jackcess/complex/Version.java +++ b/src/main/java/com/healthmarketscience/jackcess/complex/Version.java @@ -16,7 +16,9 @@ limitations under the License. package com.healthmarketscience.jackcess.complex; +import java.time.LocalDateTime; import java.util.Date; +import com.healthmarketscience.jackcess.DateTimeType; /** * Complex value corresponding to a version of a memo column. @@ -27,5 +29,13 @@ public interface Version extends ComplexValue, Comparable { public String getValue(); + /** + * @deprecated see {@link DateTimeType} for details + */ + @Deprecated public Date getModifiedDate(); + + public LocalDateTime getModifiedLocalDate(); + + public Object getModifiedDateObject(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java index 0e6fe6e..9b9ee7a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/RowImpl.java @@ -91,6 +91,7 @@ public class RowImpl extends LinkedHashMap implements Row return (Double)get(name); } + @SuppressWarnings("deprecation") public Date getDate(String name) { return (Date)get(name); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java index 6642a69..1789e92 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/complex/AttachmentColumnInfoImpl.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.Date; import java.util.HashSet; @@ -166,7 +167,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl String name = (String)getFileNameColumn().getRowValue(rawValue); String type = (String)getFileTypeColumn().getRowValue(rawValue); Integer flags = (Integer)getFileFlagsColumn().getRowValue(rawValue); - Date ts = (Date)getFileTimeStampColumn().getRowValue(rawValue); + Object ts = getFileTimeStampColumn().getRowValue(rawValue); byte[] data = (byte[])getFileDataColumn().getRowValue(rawValue); return new AttachmentImpl(id, complexValueFk, url, name, type, null, @@ -182,7 +183,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl getFileNameColumn().setRowValue(row, attachment.getFileName()); getFileTypeColumn().setRowValue(row, attachment.getFileType()); getFileFlagsColumn().setRowValue(row, attachment.getFileFlags()); - getFileTimeStampColumn().setRowValue(row, attachment.getFileTimeStamp()); + getFileTimeStampColumn().setRowValue(row, attachment.getFileTimeStampObject()); getFileDataColumn().setRowValue(row, attachment.getEncodedFileData()); return row; } @@ -198,7 +199,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl public static Attachment newAttachment( String url, String name, String type, byte[] data, - Date timeStamp, Integer flags) + Object timeStamp, Integer flags) { return newAttachment(INVALID_FK, url, name, type, data, timeStamp, flags); @@ -206,7 +207,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl public static Attachment newAttachment( ComplexValueForeignKey complexValueFk, String url, String name, - String type, byte[] data, Date timeStamp, Integer flags) + String type, byte[] data, Object timeStamp, Integer flags) { return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type, data, timeStamp, flags, null); @@ -224,7 +225,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl public static Attachment newEncodedAttachment( String url, String name, String type, byte[] encodedData, - Date timeStamp, Integer flags) + Object timeStamp, Integer flags) { return newEncodedAttachment(INVALID_FK, url, name, type, encodedData, timeStamp, flags); @@ -232,13 +233,14 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl public static Attachment newEncodedAttachment( ComplexValueForeignKey complexValueFk, String url, String name, - String type, byte[] encodedData, Date timeStamp, Integer flags) + String type, byte[] encodedData, Object timeStamp, Integer flags) { return new AttachmentImpl(INVALID_ID, complexValueFk, url, name, type, null, timeStamp, flags, encodedData); } + @SuppressWarnings("deprecation") private static class AttachmentImpl extends ComplexValueImpl implements Attachment { @@ -246,13 +248,13 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl private String _name; private String _type; private byte[] _data; - private Date _timeStamp; + private Object _timeStamp; private Integer _flags; private byte[] _encodedData; private AttachmentImpl(Id id, ComplexValueForeignKey complexValueFk, String url, String name, String type, byte[] data, - Date timeStamp, Integer flags, byte[] encodedData) + Object timeStamp, Integer flags, byte[] encodedData) { super(id, complexValueFk); _url = url; @@ -313,13 +315,25 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl } public Date getFileTimeStamp() { - return _timeStamp; + return (Date)_timeStamp; } public void setFileTimeStamp(Date fileTimeStamp) { _timeStamp = fileTimeStamp; } + public LocalDateTime getFileLocalTimeStamp() { + return (LocalDateTime)_timeStamp; + } + + public void setFileLocalTimeStamp(LocalDateTime fileTimeStamp) { + _timeStamp = fileTimeStamp; + } + + public Object getFileTimeStampObject() { + return _timeStamp; + } + public Integer getFileFlags() { return _flags; } @@ -348,7 +362,7 @@ public class AttachmentColumnInfoImpl extends ComplexColumnInfoImpl return "Attachment(" + getComplexValueForeignKey() + "," + getId() + ") " + getFileUrl() + ", " + getFileName() + ", " + getFileType() - + ", " + getFileTimeStamp() + ", " + getFileFlags() + ", " + + + ", " + getFileTimeStampObject() + ", " + getFileFlags() + ", " + dataStr; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java index a73d3ed..06c0cd7 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/complex/ComplexValueForeignKeyImpl.java @@ -17,11 +17,14 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.complex; import java.io.IOException; +import java.time.LocalDateTime; import java.util.Date; import java.util.List; import java.util.Map; import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.Database; +import com.healthmarketscience.jackcess.DateTimeType; import com.healthmarketscience.jackcess.Row; import com.healthmarketscience.jackcess.complex.Attachment; import com.healthmarketscience.jackcess.complex.AttachmentColumnInfo; @@ -50,14 +53,15 @@ import com.healthmarketscience.jackcess.complex.VersionHistoryColumnInfo; * * @author James Ahlborn */ +@SuppressWarnings("deprecation") public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey { - private static final long serialVersionUID = 20110805L; - + private static final long serialVersionUID = 20110805L; + private transient final Column _column; private final int _value; private transient List _values; - + public ComplexValueForeignKeyImpl(Column column, int value) { _column = column; _value = value; @@ -72,12 +76,12 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey public Column getColumn() { return _column; } - + @Override public ComplexDataType getComplexType() { return getComplexInfo().getType(); } - + protected ComplexColumnInfo getComplexInfo() { return _column.getComplexInfo(); } @@ -85,7 +89,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey protected VersionHistoryColumnInfo getVersionInfo() { return (VersionHistoryColumnInfo)getComplexInfo(); } - + protected AttachmentColumnInfo getAttachmentInfo() { return (AttachmentColumnInfo)getComplexInfo(); } @@ -93,27 +97,27 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey protected MultiValueColumnInfo getMultiValueInfo() { return (MultiValueColumnInfo)getComplexInfo(); } - + protected UnsupportedColumnInfo getUnsupportedInfo() { return (UnsupportedColumnInfo)getComplexInfo(); } - + @Override public int countValues() throws IOException { return getComplexInfo().countValues(get()); } - + public List getRawValues() throws IOException { return getComplexInfo().getRawValues(get()); - } - + } + @Override public List getValues() throws IOException { if(_values == null) { _values = getComplexInfo().getValues(this); } return _values; - } + } @Override @SuppressWarnings("unchecked") @@ -123,7 +127,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey } return (List)getValues(); } - + @Override @SuppressWarnings("unchecked") public List getAttachments() throws IOException { @@ -132,7 +136,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey } return (List)getValues(); } - + @Override @SuppressWarnings("unchecked") public List getMultiValues() throws IOException { @@ -141,7 +145,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey } return (List)getValues(); } - + @Override @SuppressWarnings("unchecked") public List getUnsupportedValues() throws IOException { @@ -150,20 +154,29 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey } return (List)getValues(); } - + @Override public void reset() { // discard any cached values _values = null; } - + @Override public Version addVersion(String value) throws IOException { - return addVersion(value, new Date()); + return addVersionImpl(value, now()); } - + @Override public Version addVersion(String value, Date modifiedDate) throws IOException { + return addVersionImpl(value, modifiedDate); + } + + @Override + public Version addVersion(String value, LocalDateTime modifiedDate) throws IOException { + return addVersionImpl(value, modifiedDate); + } + + private Version addVersionImpl(String value, Object modifiedDate) throws IOException { reset(); Version v = VersionHistoryColumnInfoImpl.newVersion(this, value, modifiedDate); getVersionInfo().addValue(v); @@ -172,14 +185,31 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey @Override public Attachment addAttachment(byte[] data) throws IOException { - return addAttachment(null, null, null, data, null, null); + return addAttachmentImpl(null, null, null, data, null, null); } - + @Override public Attachment addAttachment( String url, String name, String type, byte[] data, Date timeStamp, Integer flags) throws IOException + { + return addAttachmentImpl(url, name, type, data, timeStamp, flags); + } + + @Override + public Attachment addAttachment( + String url, String name, String type, byte[] data, + LocalDateTime timeStamp, Integer flags) + throws IOException + { + return addAttachmentImpl(url, name, type, data, timeStamp, flags); + } + + private Attachment addAttachmentImpl( + String url, String name, String type, byte[] data, + Object timeStamp, Integer flags) + throws IOException { reset(); Attachment a = AttachmentColumnInfoImpl.newAttachment( @@ -192,14 +222,33 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey public Attachment addEncodedAttachment(byte[] encodedData) throws IOException { - return addEncodedAttachment(null, null, null, encodedData, null, null); + return addEncodedAttachmentImpl(null, null, null, encodedData, null, null); } - + @Override public Attachment addEncodedAttachment( String url, String name, String type, byte[] encodedData, Date timeStamp, Integer flags) throws IOException + { + return addEncodedAttachmentImpl(url, name, type, encodedData, timeStamp, + flags); + } + + @Override + public Attachment addEncodedAttachment( + String url, String name, String type, byte[] encodedData, + LocalDateTime timeStamp, Integer flags) + throws IOException + { + return addEncodedAttachmentImpl(url, name, type, encodedData, timeStamp, + flags); + } + + private Attachment addEncodedAttachmentImpl( + String url, String name, String type, byte[] encodedData, + Object timeStamp, Integer flags) + throws IOException { reset(); Attachment a = AttachmentColumnInfoImpl.newEncodedAttachment( @@ -207,21 +256,21 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey getAttachmentInfo().addValue(a); return a; } - + @Override public Attachment updateAttachment(Attachment attachment) throws IOException { reset(); getAttachmentInfo().updateValue(attachment); return attachment; } - + @Override public Attachment deleteAttachment(Attachment attachment) throws IOException { reset(); getAttachmentInfo().deleteValue(attachment); return attachment; } - + @Override public SingleValue addMultiValue(Object value) throws IOException { reset(); @@ -229,21 +278,21 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey getMultiValueInfo().addValue(v); return v; } - + @Override public SingleValue updateMultiValue(SingleValue value) throws IOException { reset(); getMultiValueInfo().updateValue(value); return value; } - + @Override public SingleValue deleteMultiValue(SingleValue value) throws IOException { reset(); getMultiValueInfo().deleteValue(value); return value; } - + @Override public UnsupportedValue addUnsupportedValue(Map values) throws IOException @@ -253,7 +302,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey getUnsupportedInfo().addValue(v); return v; } - + @Override public UnsupportedValue updateUnsupportedValue(UnsupportedValue value) throws IOException @@ -262,7 +311,7 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey getUnsupportedInfo().updateValue(value); return value; } - + @Override public UnsupportedValue deleteUnsupportedValue(UnsupportedValue value) throws IOException @@ -271,16 +320,24 @@ public class ComplexValueForeignKeyImpl extends ComplexValueForeignKey getUnsupportedInfo().deleteValue(value); return value; } - + @Override public void deleteAllValues() throws IOException { reset(); getComplexInfo().deleteAllValues(this); } - + @Override public boolean equals(Object o) { return(super.equals(o) && (_column == ((ComplexValueForeignKeyImpl)o)._column)); } + + private Object now() { + Database db = getColumn().getDatabase(); + if(db.getDateTimeType() == DateTimeType.DATE) { + return new Date(); + } + return LocalDateTime.now(db.getZoneId()); + } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java index a64788f..14667d0 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/complex/VersionHistoryColumnInfoImpl.java @@ -17,6 +17,7 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.complex; import java.io.IOException; +import java.time.LocalDateTime; import java.util.Collections; import java.util.Date; import java.util.List; @@ -42,14 +43,14 @@ import com.healthmarketscience.jackcess.impl.ColumnImpl; * * @author James Ahlborn */ -public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl +public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl implements VersionHistoryColumnInfo { private final Column _valueCol; private final Column _modifiedCol; - + public VersionHistoryColumnInfoImpl(Column column, int complexId, - Table typeObjTable, Table flatTable) + Table typeObjTable, Table flatTable) throws IOException { super(column, complexId, typeObjTable, flatTable); @@ -83,7 +84,7 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl getValueColumn().getName()); ((ColumnImpl)versionedCol).setVersionHistoryColumn((ColumnImpl)getColumn()); } - + public Column getValueColumn() { return _valueCol; } @@ -91,7 +92,7 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl public Column getModifiedDateColumn() { return _modifiedCol; } - + @Override public ComplexDataType getType() { return ComplexDataType.VERSION_HISTORY; @@ -124,7 +125,7 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl // order versions newest to oldest Collections.sort(versions); - + return versions; } @@ -133,7 +134,7 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl Row rawValue) { ComplexValue.Id id = getValueId(rawValue); String value = (String)getValueColumn().getRowValue(rawValue); - Date modifiedDate = (Date)getModifiedDateColumn().getRowValue(rawValue); + Object modifiedDate = getModifiedDateColumn().getRowValue(rawValue); return new VersionImpl(id, complexValueFk, value, modifiedDate); } @@ -142,47 +143,55 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl protected Object[] asRow(Object[] row, Version version) throws IOException { super.asRow(row, version); getValueColumn().setRowValue(row, version.getValue()); - getModifiedDateColumn().setRowValue(row, version.getModifiedDate()); + getModifiedDateColumn().setRowValue(row, version.getModifiedDateObject()); return row; } - - public static Version newVersion(String value, Date modifiedDate) { + + public static Version newVersion(String value, Object modifiedDate) { return newVersion(INVALID_FK, value, modifiedDate); } - + public static Version newVersion(ComplexValueForeignKey complexValueFk, - String value, Date modifiedDate) { + String value, Object modifiedDate) { return new VersionImpl(INVALID_ID, complexValueFk, value, modifiedDate); } - + @SuppressWarnings("deprecation") private static class VersionImpl extends ComplexValueImpl implements Version { private final String _value; - private final Date _modifiedDate; + private final Object _modifiedDate; private VersionImpl(Id id, ComplexValueForeignKey complexValueFk, - String value, Date modifiedDate) + String value, Object modifiedDate) { super(id, complexValueFk); _value = value; _modifiedDate = modifiedDate; } - + public String getValue() { return _value; } public Date getModifiedDate() { + return (Date)_modifiedDate; + } + + public LocalDateTime getModifiedLocalDate() { + return (LocalDateTime)_modifiedDate; + } + + public Object getModifiedDateObject() { return _modifiedDate; - } - + } + public int compareTo(Version o) { - Date d1 = getModifiedDate(); - Date d2 = o.getModifiedDate(); + Object d1 = getModifiedDateObject(); + Object d2 = o.getModifiedDateObject(); // sort by descending date (newest/greatest first) - int cmp = d2.compareTo(d1); + int cmp = compare(d2, d1); if(cmp != 0) { return cmp; } @@ -200,11 +209,20 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl ((id1 < id2) ? 1 : 0)); } + @SuppressWarnings("unchecked") + private static > int compare(Object o1, Object o2) { + // each date/time type (Date, LocalDateTime) is mutually Comparable, so + // just silence the compiler + C c1 = (C)o1; + C c2 = (C)o2; + return c1.compareTo(c2); + } + public void update() throws IOException { throw new UnsupportedOperationException( "This column does not support value updates"); } - + public void delete() throws IOException { throw new UnsupportedOperationException( "This column does not support value deletes"); @@ -214,8 +232,8 @@ public class VersionHistoryColumnInfoImpl extends ComplexColumnInfoImpl public String toString() { return "Version(" + getComplexValueForeignKey() + "," + getId() + ") " + - getModifiedDate() + ", " + getValue(); - } + getModifiedDateObject() + ", " + getValue(); + } } - + } diff --git a/src/test/java/com/healthmarketscience/jackcess/ComplexColumnTest.java b/src/test/java/com/healthmarketscience/jackcess/ComplexColumnTest.java index b4704bf..c8b81a3 100644 --- a/src/test/java/com/healthmarketscience/jackcess/ComplexColumnTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/ComplexColumnTest.java @@ -39,7 +39,8 @@ import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; * * @author James Ahlborn */ -public class ComplexColumnTest extends TestCase +@SuppressWarnings("deprecation") +public class ComplexColumnTest extends TestCase { public ComplexColumnTest(String name) { @@ -66,7 +67,7 @@ public class ComplexColumnTest extends TestCase (ComplexValueForeignKey)verCol.getRowValue(row); String curValue = (String)col.getRowValue(row); - + if(rowId.equals("row1")) { checkVersions(1, complexValueFk, curValue); } else if(rowId.equals("row2")) { @@ -94,7 +95,7 @@ public class ComplexColumnTest extends TestCase Date upTime = new Date(); row8ValFk.addVersion("row8-memo", upTime); checkVersions(row8ValFk.get(), row8ValFk, "row8-memo", - "row8-memo", upTime); + "row8-memo", upTime); Cursor cursor = CursorBuilder.createCursor(t1); assertTrue(cursor.findFirstRow(t1.getColumn("id"), "row3")); @@ -120,7 +121,7 @@ public class ComplexColumnTest extends TestCase "row3-memo-again", new Date(1315876965382L), "row3-memo-revised", new Date(1315876953077L), "row3-memo", new Date(1315876879126L)); - + try { v.delete(); fail("UnsupportedOperationException should have been thrown"); @@ -133,7 +134,7 @@ public class ComplexColumnTest extends TestCase "row3-memo-again", new Date(1315876965382L), "row3-memo-revised", new Date(1315876953077L), "row3-memo", new Date(1315876879126L)); - + try { v.getComplexValueForeignKey().deleteAllValues(); fail("UnsupportedOperationException should have been thrown"); @@ -146,7 +147,7 @@ public class ComplexColumnTest extends TestCase "row3-memo-again", new Date(1315876965382L), "row3-memo-revised", new Date(1315876953077L), "row3-memo", new Date(1315876879126L)); - + db.close(); } } @@ -154,7 +155,7 @@ public class ComplexColumnTest extends TestCase public void testAttachments() throws Exception { for(final TestDB testDB : TestDB.getSupportedForBasename(Basename.COMPLEX)) { - + Database db = openCopy(testDB); Table t1 = db.getTable("Table1"); @@ -187,12 +188,12 @@ public class ComplexColumnTest extends TestCase ComplexValueForeignKey row8ValFk = (ComplexValueForeignKey) col.getRowValue(row8); row8ValFk.addAttachment(null, "test_data.txt", "txt", - getFileBytes("test_data.txt"), null, null); + getFileBytes("test_data.txt"), (Date)null, null); checkAttachments(row8ValFk.get(), row8ValFk, "test_data.txt"); row8ValFk.addEncodedAttachment(null, "test_data2.txt", "txt", - getEncodedFileBytes("test_data2.txt"), null, - null); - checkAttachments(row8ValFk.get(), row8ValFk, "test_data.txt", + getEncodedFileBytes("test_data2.txt"), + (Date)null, null); + checkAttachments(row8ValFk.get(), row8ValFk, "test_data.txt", "test_data2.txt"); Cursor cursor = CursorBuilder.createCursor(t1); @@ -200,8 +201,8 @@ public class ComplexColumnTest extends TestCase ComplexValueForeignKey row4ValFk = (ComplexValueForeignKey) cursor.getCurrentRowValue(col); Attachment a = row4ValFk.addAttachment(null, "test_data.txt", "txt", - getFileBytes("test_data.txt"), null, - null); + getFileBytes("test_data.txt"), + (Date)null, null); checkAttachments(4, row4ValFk, "test_data2.txt", "test_data.txt"); a.setFileType("zip"); @@ -230,8 +231,8 @@ public class ComplexColumnTest extends TestCase ComplexValueForeignKey row2ValFk = (ComplexValueForeignKey) cursor.getCurrentRowValue(col); row2ValFk.deleteAllValues(); - checkAttachments(2, row2ValFk); - + checkAttachments(2, row2ValFk); + db.close(); } } @@ -239,7 +240,7 @@ public class ComplexColumnTest extends TestCase public void testMultiValues() throws Exception { for(final TestDB testDB : TestDB.getSupportedForBasename(Basename.COMPLEX)) { - + Database db = openCopy(testDB); Table t1 = db.getTable("Table1"); @@ -264,7 +265,7 @@ public class ComplexColumnTest extends TestCase } else { assertTrue(false); } - } + } Object[] row8 = {"row8", Column.AUTO_NUMBER, "some-data", "row8-memo", Column.AUTO_NUMBER, Column.AUTO_NUMBER}; @@ -307,17 +308,17 @@ public class ComplexColumnTest extends TestCase PropertyMap props = col.getProperties(); assertEquals(Boolean.TRUE, props.getValue(PropertyMap.ALLOW_MULTI_VALUE_PROP)); assertEquals("Value List", props.getValue(PropertyMap.ROW_SOURCE_TYPE_PROP)); - assertEquals("\"value1\";\"value2\";\"value3\";\"value4\"", + assertEquals("\"value1\";\"value2\";\"value3\";\"value4\"", props.getValue(PropertyMap.ROW_SOURCE_PROP)); - + db.close(); } } - + public void testUnsupported() throws Exception { for(final TestDB testDB : TestDB.getSupportedForBasename(Basename.UNSUPPORTED)) { - + Database db = openCopy(testDB); Table t1 = db.getTable("Test"); @@ -331,7 +332,7 @@ public class ComplexColumnTest extends TestCase (ComplexValueForeignKey)col.getRowValue(row); if(rowId.equals(1)) { - checkUnsupportedValues(1, complexValueFk, + checkUnsupportedValues(1, complexValueFk, "RawData[(5) FF FE 62 61 7A]"); } else if(rowId.equals(2)) { checkUnsupportedValues(2, complexValueFk, "RawData[(5) FF FE 66 6F 6F]", "RawData[(5) FF FE 62 61 7A]"); @@ -340,12 +341,12 @@ public class ComplexColumnTest extends TestCase } else { assertTrue(false); } - } - + } + db.close(); } } - + private static void checkVersions( int cValId, ComplexValueForeignKey complexValueFk, String curValue, Object... versionInfos) @@ -376,7 +377,7 @@ public class ComplexColumnTest extends TestCase throws Exception { assertEquals(cValId, complexValueFk.get()); - + List attachments = complexValueFk.getAttachments(); if(fileNames.length == 0) { assertTrue(attachments.isEmpty()); @@ -388,12 +389,12 @@ public class ComplexColumnTest extends TestCase assertEquals(fname, a.getFileName()); assertEquals("txt", a.getFileType()); assertTrue(Arrays.equals(getFileBytes(fname), a.getFileData())); - assertTrue(Arrays.equals(getEncodedFileBytes(fname), + assertTrue(Arrays.equals(getEncodedFileBytes(fname), a.getEncodedFileData())); } } } - + private static void checkMultiValues( int cValId, ComplexValueForeignKey complexValueFk, Object... expectedValues) @@ -411,7 +412,7 @@ public class ComplexColumnTest extends TestCase SingleValue v = values.get(i); assertEquals(value, v.get()); } - } + } } private static void checkUnsupportedValues( @@ -434,7 +435,7 @@ public class ComplexColumnTest extends TestCase assertTrue(ColumnImpl.isRawData(rv)); assertEquals(value, rv.toString()); } - } + } } private static byte[] getFileBytes(String fname) throws Exception @@ -447,7 +448,7 @@ public class ComplexColumnTest extends TestCase } throw new RuntimeException("unexpected bytes"); } - + private static byte[] getEncodedFileBytes(String fname) throws Exception { if("test_data.txt".equals(fname)) { @@ -458,9 +459,9 @@ public class ComplexColumnTest extends TestCase } throw new RuntimeException("unexpected bytes"); } - + private static byte b(int i) { return (byte)i; } - + private static byte[] getAsciiBytes(String str) { try { return str.getBytes("US-ASCII"); @@ -468,7 +469,7 @@ public class ComplexColumnTest extends TestCase throw new RuntimeException(e); } } - + private static final byte[] TEST_ENC_BYTES = new byte[] { b(0x01),b(0x00),b(0x00),b(0x00),b(0x3A),b(0x00),b(0x00),b(0x00),b(0x78),b(0x5E),b(0x13),b(0x61),b(0x60),b(0x60),b(0x60),b(0x04),b(0x62),b(0x16),b(0x20),b(0x2E),b(0x61),b(0xA8),b(0x00),b(0x62), b(0x20),b(0x9D),b(0x91),b(0x59),b(0xAC),b(0x00),b(0x44),b(0xC5),b(0xF9),b(0xB9),b(0xA9),b(0x0A),b(0x25),b(0xA9),b(0xC5),b(0x25),b(0x0A),b(0x29),b(0x89),b(0x25),b(0x89),b(0x0A),b(0x69),b(0xF9), @@ -476,7 +477,7 @@ public class ComplexColumnTest extends TestCase }; private static final byte[] TEST_BYTES = getAsciiBytes("this is some test data for attachment."); - + private static final byte[] TEST2_ENC_BYTES = new byte[] { b(0x01),b(0x00),b(0x00),b(0x00),b(0x3F),b(0x00),b(0x00),b(0x00),b(0x78),b(0x5E),b(0x13),b(0x61),b(0x60),b(0x60),b(0x60),b(0x04),b(0x62),b(0x16),b(0x20),b(0x2E),b(0x61),b(0xA8),b(0x00),b(0x62), b(0x20),b(0x9D),b(0x91),b(0x59),b(0xAC),b(0x00),b(0x44),b(0xC5),b(0xF9),b(0xB9),b(0xA9),b(0x0A),b(0xB9),b(0xF9),b(0x45),b(0xA9),b(0x0A),b(0x25),b(0xA9),b(0xC5),b(0x25),b(0x0A),b(0x29),b(0x89), @@ -484,5 +485,5 @@ public class ComplexColumnTest extends TestCase }; private static final byte[] TEST2_BYTES = getAsciiBytes("this is some more test data for attachment."); - + } diff --git a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java index 209a63d..1aa2483 100644 --- a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -52,6 +52,7 @@ import static com.healthmarketscience.jackcess.TestUtil.*; /** * @author Tim McCune */ +@SuppressWarnings("deprecation") public class DatabaseTest extends TestCase { public DatabaseTest(String name) throws Exception { diff --git a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java index 0bef2e0..b5db277 100644 --- a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java +++ b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java @@ -53,6 +53,7 @@ import org.junit.Assert; * * @author James Ahlborn */ +@SuppressWarnings("deprecation") public class TestUtil { public static final TimeZone TEST_TZ = -- cgit v1.2.3 From 0f568a46209da6557cfeab5f351bc6ee432eb3ea Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Fri, 28 Dec 2018 04:19:21 +0000 Subject: add system prop for date/time type; rework how date/times are written based on date/time type git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1253 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../com/healthmarketscience/jackcess/Database.java | 9 ++- .../jackcess/impl/ColumnImpl.java | 75 ++++++++++++++++------ .../jackcess/impl/DatabaseImpl.java | 47 +++++++++----- .../jackcess/impl/DateTimeContext.java | 34 ++++++++++ .../jackcess/impl/ZoneContext.java | 32 --------- .../healthmarketscience/jackcess/DatabaseTest.java | 4 ++ .../jackcess/LocalDateTimeTest.java | 70 +++++++++++++++++++- .../com/healthmarketscience/jackcess/TestUtil.java | 20 ++++++ 8 files changed, 221 insertions(+), 70 deletions(-) create mode 100644 src/main/java/com/healthmarketscience/jackcess/impl/DateTimeContext.java delete mode 100644 src/main/java/com/healthmarketscience/jackcess/impl/ZoneContext.java (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/Database.java b/src/main/java/com/healthmarketscience/jackcess/Database.java index 13e45a5..4fa8741 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Database.java +++ b/src/main/java/com/healthmarketscience/jackcess/Database.java @@ -106,7 +106,7 @@ public interface Database extends Iterable
, Closeable, Flushable "com.healthmarketscience.jackcess.brokenNio"; /** system property which can be used to set the default sort order for - * table columns. Value should be one {@link Table.ColumnOrder} enum + * table columns. Value should be one of {@link Table.ColumnOrder} enum * values. * @usage _intermediate_field_ */ @@ -134,6 +134,13 @@ public interface Database extends Iterable
, Closeable, Flushable public static final String ENABLE_EXPRESSION_EVALUATION_PROPERTY = "com.healthmarketscience.jackcess.enableExpressionEvaluation"; + /** system property which can be used to set the default date/Time type. + * Value should be one of {@link DateTimeType} enum values. + * @usage _general_field_ + */ + public static final String DATE_TIME_TYPE_PROPERTY = + "com.healthmarketscience.jackcess.dateTimeType"; + /** * Enum which indicates which version of Access created the database. * @usage _general_class_ diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index 6ab88ab..4172874 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -76,7 +76,7 @@ import org.apache.commons.logging.LogFactory; * @author Tim McCune * @usage _intermediate_class_ */ -public class ColumnImpl implements Column, Comparable, ZoneContext +public class ColumnImpl implements Column, Comparable, DateTimeContext { protected static final Log LOG = LogFactory.getLog(ColumnImpl.class); @@ -506,7 +506,8 @@ public class ColumnImpl implements Column, Comparable, ZoneContext return getDatabase().getZoneId(); } - protected DateTimeFactory getDateTimeFactory() { + @Override + public DateTimeFactory getDateTimeFactory() { return getDatabase().getDateTimeFactory(); } @@ -1042,21 +1043,12 @@ public class ColumnImpl implements Column, Comparable, ZoneContext * Date/Calendar/Number/Temporal time value. * @usage _advanced_method_ */ - private static double toDateDouble(Object value, ZoneContext zc) - { - if(value instanceof TemporalAccessor) { - return toDateDouble(toLocalDateTime((TemporalAccessor)value, zc)); - } - - // 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, zc.getTimeZone()); - return toLocalDateDouble(time); + private static double toDateDouble(Object value, DateTimeContext dtc) { + return dtc.getDateTimeFactory().toDateDouble(value, dtc); } private static LocalDateTime toLocalDateTime( - TemporalAccessor value, ZoneContext zc) { + TemporalAccessor value, DateTimeContext dtc) { // handle some common Temporal types if(value instanceof LocalDateTime) { @@ -1065,10 +1057,10 @@ public class ColumnImpl implements Column, Comparable, ZoneContext if(value instanceof ZonedDateTime) { // if the temporal value has a timezone, convert it to this db's timezone return ((ZonedDateTime)value).withZoneSameInstant( - zc.getZoneId()).toLocalDateTime(); + dtc.getZoneId()).toLocalDateTime(); } if(value instanceof Instant) { - return LocalDateTime.ofInstant((Instant)value, zc.getZoneId()); + return LocalDateTime.ofInstant((Instant)value, dtc.getZoneId()); } if(value instanceof LocalDate) { return ((LocalDate)value).atTime(BASE_LT); @@ -1092,7 +1084,7 @@ public class ColumnImpl implements Column, Comparable, ZoneContext if(zone != null) { // the Temporal has a zone, see if it is the right zone. if not, // adjust it - ZoneId zoneId = zc.getZoneId(); + ZoneId zoneId = dtc.getZoneId(); if(!zoneId.equals(zone)) { return ZonedDateTime.of(ld, lt, zone).withZoneSameInstant(zoneId) .toLocalDateTime(); @@ -1107,6 +1099,16 @@ public class ColumnImpl implements Column, Comparable, ZoneContext } } + private static Instant toInstant(TemporalAccessor value, DateTimeContext dtc) { + if(value instanceof ZonedDateTime) { + return ((ZonedDateTime)value).toInstant(); + } + if(value instanceof Instant) { + return (Instant)value; + } + return toLocalDateTime(value, dtc).atZone(dtc.getZoneId()).toInstant(); + } + static double toLocalDateDouble(long time) { time += MILLIS_BETWEEN_EPOCH_AND_1900; @@ -2191,7 +2193,7 @@ public class ColumnImpl implements Column, Comparable, ZoneContext } } - static DateTimeFactory getDateTimeFactory(DateTimeType type) { + protected static DateTimeFactory getDateTimeFactory(DateTimeType type) { return ((type == DateTimeType.LOCAL_DATE_TIME) ? LDT_DATE_TIME_FACTORY : DEF_DATE_TIME_FACTORY); } @@ -2676,19 +2678,21 @@ public class ColumnImpl implements Column, Comparable, ZoneContext /** * Factory which handles date/time values appropriately for a DateTimeType. */ - static abstract class DateTimeFactory + protected static abstract class DateTimeFactory { public abstract DateTimeType getType(); public abstract Object fromDateBits(ColumnImpl col, long dateBits); + public abstract double toDateDouble(Object value, DateTimeContext dtc); + public abstract Object toInternalValue(DatabaseImpl db, Object value); } /** * Factory impl for legacy Date handling. */ - static final class DefaultDateTimeFactory extends DateTimeFactory + private static final class DefaultDateTimeFactory extends DateTimeFactory { @Override public DateTimeType getType() { @@ -2702,6 +2706,23 @@ public class ColumnImpl implements Column, Comparable, ZoneContext return new DateExt(time, dateBits); } + @Override + public double toDateDouble(Object value, DateTimeContext dtc) { + // ZoneId and TimeZone have different rules for older timezones, so we + // need to consistently use one or the other depending on the date/time + // type + long time = 0L; + if(value instanceof TemporalAccessor) { + time = toInstant((TemporalAccessor)value, dtc).toEpochMilli(); + } else { + time = toDateLong(value); + } + // seems access stores dates in the local timezone. guess you just + // hope you read it in the same timezone in which it was written! + time += getToLocalTimeZoneOffset(time, dtc.getTimeZone()); + return toLocalDateDouble(time); + } + @Override public Object toInternalValue(DatabaseImpl db, Object value) { return ((value instanceof Date) ? value : @@ -2712,7 +2733,7 @@ public class ColumnImpl implements Column, Comparable, ZoneContext /** * Factory impl for LocalDateTime handling. */ - static final class LDTDateTimeFactory extends DateTimeFactory + private static final class LDTDateTimeFactory extends DateTimeFactory { @Override public DateTimeType getType() { @@ -2724,6 +2745,18 @@ public class ColumnImpl implements Column, Comparable, ZoneContext return ldtFromLocalDateDouble(Double.longBitsToDouble(dateBits)); } + @Override + public double toDateDouble(Object value, DateTimeContext dtc) { + // ZoneId and TimeZone have different rules for older timezones, so we + // need to consistently use one or the other depending on the date/time + // type + if(!(value instanceof TemporalAccessor)) { + value = Instant.ofEpochMilli(toDateLong(value)); + } + return ColumnImpl.toDateDouble( + toLocalDateTime((TemporalAccessor)value, dtc)); + } + @Override public Object toInternalValue(DatabaseImpl db, Object value) { if(value instanceof TemporalAccessor) { diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index 1a34dbd..c005651 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -88,7 +88,7 @@ import org.apache.commons.logging.LogFactory; * @author Tim McCune * @usage _intermediate_class_ */ -public class DatabaseImpl implements Database, ZoneContext +public class DatabaseImpl implements Database, DateTimeContext { private static final Log LOG = LogFactory.getLog(DatabaseImpl.class); @@ -346,8 +346,7 @@ public class DatabaseImpl implements Database, ZoneContext /** shared context for evaluating expressions */ private DBEvalContext _evalCtx; /** factory for the appropriate date/time type */ - private ColumnImpl.DateTimeFactory _dtf = - ColumnImpl.getDateTimeFactory(DateTimeType.DATE); + private ColumnImpl.DateTimeFactory _dtf; /** * Open an existing Database. If the existing file is not writeable or the @@ -537,8 +536,9 @@ public class DatabaseImpl implements Database, ZoneContext _allowAutoNumInsert = getDefaultAllowAutoNumberInsert(); _evaluateExpressions = getDefaultEvaluateExpressions(); _fileFormat = fileFormat; - _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync); setZoneInfo(timeZone, null); + _dtf = ColumnImpl.getDateTimeFactory(getDefaultDateTimeType()); + _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync); if(provider == null) { provider = DefaultCodecProvider.INSTANCE; } @@ -710,7 +710,8 @@ public class DatabaseImpl implements Database, ZoneContext _dtf = ColumnImpl.getDateTimeFactory(dateTimeType); } - protected ColumnImpl.DateTimeFactory getDateTimeFactory() { + @Override + public ColumnImpl.DateTimeFactory getDateTimeFactory() { return _dtf; } @@ -2043,16 +2044,8 @@ public class DatabaseImpl implements Database, ZoneContext */ public static Table.ColumnOrder getDefaultColumnOrder() { - String coProp = System.getProperty(COLUMN_ORDER_PROPERTY); - if(coProp != null) { - coProp = coProp.trim(); - if(coProp.length() > 0) { - return Table.ColumnOrder.valueOf(coProp); - } - } - - // use default order - return DEFAULT_COLUMN_ORDER; + return getEnumSystemProperty(Table.ColumnOrder.class, COLUMN_ORDER_PROPERTY, + DEFAULT_COLUMN_ORDER); } /** @@ -2100,6 +2093,17 @@ public class DatabaseImpl implements Database, ZoneContext return false; } + /** + * Returns the default DateTimeType. This defaults to + * {@link DateTimeType#DATE}, but can be overridden using the system + * property {@value com.healthmarketscience.jackcess.Database#DATE_TIME_TYPE_PROPERTY}. + * @usage _advanced_method_ + */ + public static DateTimeType getDefaultDateTimeType() { + return getEnumSystemProperty(DateTimeType.class, DATE_TIME_TYPE_PROPERTY, + DateTimeType.DATE); + } + /** * Copies the given db InputStream to the given channel using the most * efficient means possible. @@ -2196,6 +2200,19 @@ public class DatabaseImpl implements Database, ZoneContext return msg + " (Db=" + dbName + ")"; } + private static > E getEnumSystemProperty( + Class enumClass, String propName, E defaultValue) + { + String prop = System.getProperty(propName); + if(prop != null) { + prop = prop.trim().toUpperCase(); + if(!prop.isEmpty()) { + return Enum.valueOf(enumClass, prop); + } + } + return defaultValue; + } + /** * Utility class for storing table page number and actual name. */ diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DateTimeContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/DateTimeContext.java new file mode 100644 index 0000000..8045755 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DateTimeContext.java @@ -0,0 +1,34 @@ +/* +Copyright (c) 2018 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; + +import java.time.ZoneId; +import java.util.TimeZone; + +/** + * Provider of zone related info for date/time conversions. + * + * @author James Ahlborn + */ +interface DateTimeContext +{ + public ZoneId getZoneId(); + + public TimeZone getTimeZone(); + + public ColumnImpl.DateTimeFactory getDateTimeFactory(); +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ZoneContext.java b/src/main/java/com/healthmarketscience/jackcess/impl/ZoneContext.java deleted file mode 100644 index 0134e1f..0000000 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ZoneContext.java +++ /dev/null @@ -1,32 +0,0 @@ -/* -Copyright (c) 2018 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; - -import java.time.ZoneId; -import java.util.TimeZone; - -/** - * Provider of zone related info for date/time conversions. - * - * @author James Ahlborn - */ -interface ZoneContext -{ - public ZoneId getZoneId(); - - public TimeZone getTimeZone(); -} diff --git a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java index 1aa2483..0a33a99 100644 --- a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -995,6 +995,10 @@ public class DatabaseTest extends TestCase public TimeZone getTimeZone() { return tz; } @Override public ZoneId getZoneId() { return null; } + @Override + public ColumnImpl.DateTimeFactory getDateTimeFactory() { + return getDateTimeFactory(DateTimeType.DATE); + } }; SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd"); diff --git a/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java index 637629a..e17bbfb 100644 --- a/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java @@ -62,7 +62,71 @@ public class LocalDateTimeTest extends TestCase super(name); } - public void testAncientDates() throws Exception + public void testWriteAndReadLocalDate() throws Exception { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = createMem(fileFormat); + + db.setDateTimeType(DateTimeType.LOCAL_DATE_TIME); + + Table table = new TableBuilder("test") + .addColumn(new ColumnBuilder("name", DataType.TEXT)) + .addColumn(new ColumnBuilder("date", DataType.SHORT_DATE_TIME)) + .toTable(db); + + // since jackcess does not really store millis, shave them off before + // storing the current date/time + long curTimeNoMillis = (System.currentTimeMillis() / 1000L); + curTimeNoMillis *= 1000L; + + DateFormat df = new SimpleDateFormat("yyyyMMdd HH:mm:ss"); + List dates = + new ArrayList( + Arrays.asList( + df.parse("19801231 00:00:00"), + df.parse("19930513 14:43:27"), + null, + df.parse("20210102 02:37:00"), + new Date(curTimeNoMillis))); + + Calendar c = Calendar.getInstance(); + for(int year = 1801; year < 2050; year +=3) { + for(int month = 0; month <= 12; ++month) { + for(int day = 1; day < 29; day += 3) { + c.clear(); + c.set(Calendar.YEAR, year); + c.set(Calendar.MONTH, month); + c.set(Calendar.DAY_OF_MONTH, day); + dates.add(c.getTime()); + } + } + } + + ((DatabaseImpl)db).getPageChannel().startWrite(); + try { + for(Date d : dates) { + table.addRow("row " + d, d); + } + } finally { + ((DatabaseImpl)db).getPageChannel().finishWrite(); + } + + List foundDates = new ArrayList(); + for(Row row : table) { + foundDates.add(row.getLocalDateTime("date")); + } + + assertEquals(dates.size(), foundDates.size()); + for(int i = 0; i < dates.size(); ++i) { + Date expected = dates.get(i); + LocalDateTime found = foundDates.get(i); + assertSameDate(expected, found); + } + + db.close(); + } + } + + public void testAncientLocalDates() throws Exception { ZoneId zoneId = ZoneId.of("America/New_York"); DateTimeFormatter sdf = DateTimeFormatter.ofPattern("uuuu-MM-dd"); @@ -130,6 +194,10 @@ public class LocalDateTimeTest extends TestCase public TimeZone getTimeZone() { return tz; } @Override public ZoneId getZoneId() { return zoneId; } + @Override + public ColumnImpl.DateTimeFactory getDateTimeFactory() { + return getDateTimeFactory(DateTimeType.LOCAL_DATE_TIME); + } }; SimpleDateFormat df = new SimpleDateFormat("yyyy.MM.dd"); diff --git a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java index b5db277..83b2d7d 100644 --- a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java +++ b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java @@ -27,6 +27,10 @@ import java.io.PrintWriter; import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -379,6 +383,22 @@ public class TestUtil } } + static void assertSameDate(Date expected, LocalDateTime found) + { + if((expected == null) && (found == null)) { + return; + } + if((expected == null) || (found == null)) { + throw new AssertionError("Expected " + expected + ", found " + found); + } + + LocalDateTime expectedLdt = LocalDateTime.ofInstant( + Instant.ofEpochMilli(expected.getTime()), + ZoneId.systemDefault()); + + Assert.assertEquals(expectedLdt, found); + } + static void copyFile(File srcFile, File dstFile) throws IOException { -- cgit v1.2.3 From 8dae0688daad08a789e9357c8245c4a20ef2b212 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Fri, 28 Dec 2018 04:40:07 +0000 Subject: more ldt tests git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1254 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/LocalDateTimeTest.java | 62 ++++++++++++++++++++++ 1 file changed, 62 insertions(+) (limited to 'src/test') diff --git a/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java index e17bbfb..0760b95 100644 --- a/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java @@ -26,6 +26,7 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.temporal.TemporalAccessor; import java.util.ArrayList; @@ -225,4 +226,65 @@ public class LocalDateTimeTest extends TestCase } } + public void testWriteAndReadTemporals() throws Exception { + ZoneId zoneId = ZoneId.of("America/New_York"); + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = createMem(fileFormat); + db.setZoneId(zoneId); + db.setDateTimeType(DateTimeType.LOCAL_DATE_TIME); + + Table table = new TableBuilder("test") + .addColumn(new ColumnBuilder("name", DataType.TEXT)) + .addColumn(new ColumnBuilder("date", DataType.SHORT_DATE_TIME)) + .toTable(db); + + // since jackcess does not really store millis, shave them off before + // storing the current date/time + long curTimeNoMillis = (System.currentTimeMillis() / 1000L); + curTimeNoMillis *= 1000L; + + DateFormat df = new SimpleDateFormat("yyyyMMdd HH:mm:ss"); + List tmpDates = + new ArrayList( + Arrays.asList( + df.parse("19801231 00:00:00"), + df.parse("19930513 14:43:27"), + df.parse("20210102 02:37:00"), + new Date(curTimeNoMillis))); + + List objs = new ArrayList(); + List expected = new ArrayList(); + for(Date d : tmpDates) { + Instant inst = Instant.ofEpochMilli(d.getTime()); + objs.add(inst); + ZonedDateTime zdt = inst.atZone(zoneId); + objs.add(zdt); + LocalDateTime ldt = zdt.toLocalDateTime(); + objs.add(ldt); + + for(int i = 0; i < 3; ++i) { + expected.add(ldt); + } + } + + ((DatabaseImpl)db).getPageChannel().startWrite(); + try { + for(Object o : objs) { + table.addRow("row " + o, o); + } + } finally { + ((DatabaseImpl)db).getPageChannel().finishWrite(); + } + + List foundDates = new ArrayList(); + for(Row row : table) { + foundDates.add(row.getLocalDateTime("date")); + } + + assertEquals(expected, foundDates); + + db.close(); + } + } + } -- cgit v1.2.3 From 4b11b375e8b01600edba55b1d296a43f512ad08a Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Fri, 28 Dec 2018 04:41:33 +0000 Subject: cleanup git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1255 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../healthmarketscience/jackcess/LocalDateTimeTest.java | 15 --------------- 1 file changed, 15 deletions(-) (limited to 'src/test') diff --git a/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java index 0760b95..1eb11db 100644 --- a/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/LocalDateTimeTest.java @@ -16,10 +16,6 @@ limitations under the License. package com.healthmarketscience.jackcess; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.math.BigDecimal; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.Instant; @@ -28,26 +24,15 @@ import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.time.temporal.TemporalAccessor; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; -import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.TimeZone; -import java.util.TreeSet; -import java.util.UUID; import com.healthmarketscience.jackcess.impl.ColumnImpl; import com.healthmarketscience.jackcess.impl.DatabaseImpl; -import com.healthmarketscience.jackcess.impl.RowIdImpl; -import com.healthmarketscience.jackcess.impl.RowImpl; -import com.healthmarketscience.jackcess.impl.TableImpl; -import com.healthmarketscience.jackcess.util.LinkResolver; import junit.framework.TestCase; import static com.healthmarketscience.jackcess.TestUtil.*; import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; -- cgit v1.2.3 From 55531bcd0502dde1fea00957a1b4ec3f32b1e7f8 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Mon, 31 Dec 2018 14:58:03 +0000 Subject: fix channel open options for new files git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1257 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../healthmarketscience/jackcess/impl/DatabaseImpl.java | 16 +++++++++++----- .../healthmarketscience/jackcess/impl/JetFormatTest.java | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index c005651..1db1c3b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -204,9 +204,13 @@ public class DatabaseImpl implements Database, DateTimeContext /** read-only channel access mode */ public static final OpenOption[] RO_CHANNEL_OPTS = {StandardOpenOption.READ}; - /** read/write channel access mode */ + /** read/write channel access mode for existing files */ public static final OpenOption[] RW_CHANNEL_OPTS = {StandardOpenOption.READ, StandardOpenOption.WRITE}; + /** read/write/create channel access mode for new files */ + public static final OpenOption[] RWC_CHANNEL_OPTS = + {StandardOpenOption.READ, StandardOpenOption.WRITE, + StandardOpenOption.CREATE}; /** Name of the system object that is the parent of all tables */ private static final String SYSTEM_OBJECT_NAME_TABLES = "Tables"; @@ -388,7 +392,7 @@ public class DatabaseImpl implements Database, DateTimeContext readOnly |= !Files.isWritable(mdbFile); // open file channel - channel = openChannel(mdbFile, readOnly); + channel = openChannel(mdbFile, readOnly, false); closeChannel = true; } @@ -459,7 +463,7 @@ public class DatabaseImpl implements Database, DateTimeContext boolean closeChannel = false; if(channel == null) { - channel = openChannel(mdbFile, false); + channel = openChannel(mdbFile, false, true); closeChannel = true; } @@ -494,10 +498,12 @@ public class DatabaseImpl implements Database, DateTimeContext * that name cannot be created, or if some other error occurs * while opening or creating the file */ - static FileChannel openChannel(Path mdbFile, boolean readOnly) + static FileChannel openChannel( + Path mdbFile, boolean readOnly, boolean create) throws IOException { - OpenOption[] opts = (readOnly ? RO_CHANNEL_OPTS : RW_CHANNEL_OPTS); + OpenOption[] opts = (readOnly ? RO_CHANNEL_OPTS : + (create ? RWC_CHANNEL_OPTS : RW_CHANNEL_OPTS)); return FileChannel.open(mdbFile, opts); } diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java index 36ab9bd..66c8a71 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java @@ -194,7 +194,7 @@ public class JetFormatTest extends TestCase { for (final TestDB testDB : SUPPORTED_DBS_TEST_FOR_READ) { final FileChannel channel = DatabaseImpl.openChannel( - testDB.dbFile.toPath(), false); + testDB.dbFile.toPath(), false, false); try { JetFormat fmtActual = JetFormat.getFormat(channel); -- cgit v1.2.3 From 85c0657595d331299da4fe9175bf882746688608 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Sat, 19 Jan 2019 06:03:37 +0000 Subject: fix parsing of escaped double quote; implement euro format; implement most custom formatting git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1265 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/expr/NumericConfig.java | 8 +- .../jackcess/impl/expr/DefaultDateFunctions.java | 2 +- .../jackcess/impl/expr/ExpressionTokenizer.java | 27 +- .../jackcess/impl/expr/FormatUtil.java | 1096 ++++++++++++++++++-- .../jackcess/impl/expr/DefaultFunctionsTest.java | 17 +- 5 files changed, 1054 insertions(+), 96 deletions(-) (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/expr/NumericConfig.java b/src/main/java/com/healthmarketscience/jackcess/expr/NumericConfig.java index 74dd06e..d231522 100644 --- a/src/main/java/com/healthmarketscience/jackcess/expr/NumericConfig.java +++ b/src/main/java/com/healthmarketscience/jackcess/expr/NumericConfig.java @@ -35,7 +35,7 @@ public class NumericConfig 2, true, false, true, 3, Locale.US); public enum Type { - CURRENCY, FIXED, STANDARD, PERCENT, SCIENTIFIC; + CURRENCY, FIXED, STANDARD, PERCENT, SCIENTIFIC, EURO; } private final int _numDecDigits; @@ -49,6 +49,7 @@ public class NumericConfig private final String _standardFormat; private final String _percentFormat; private final String _scientificFormat; + private final String _euroFormat; public NumericConfig(int numDecDigits, boolean incLeadingDigit, boolean useNegParens, boolean useNegCurrencyParens, @@ -75,6 +76,9 @@ public class NumericConfig _scientificFormat = FormatUtil.createNumberFormatPattern( FormatUtil.NumPatternType.SCIENTIFIC, _numDecDigits, true, false, 0); + _euroFormat = FormatUtil.createNumberFormatPattern( + FormatUtil.NumPatternType.EURO, _numDecDigits, _incLeadingDigit, + _useNegCurrencyParens, _numGroupDigits); } public int getNumDecimalDigits() { @@ -109,6 +113,8 @@ public class NumericConfig return _percentFormat; case SCIENTIFIC: return _scientificFormat; + case EURO: + return _euroFormat; default: throw new IllegalArgumentException("unknown number type " + type); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java index 2ac67d2..31d919b 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java @@ -505,7 +505,7 @@ public class DefaultDateFunctions return getOptionalIntParam(ctx, params, idx, 1, 0); } - private static WeekFields weekFields(int firstDay, int firstWeekType) { + static WeekFields weekFields(int firstDay, int firstWeekType) { int minDays = 1; switch(firstWeekType) { 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 4db436a..39f7050 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java @@ -239,29 +239,34 @@ class ExpressionTokenizer } private static String parseQuotedString(ExprBuf buf, char quoteChar) { - return parseStringUntil(buf, quoteChar, null, true); + return parseStringUntil(buf, null, quoteChar, true); } private static String parseObjNameString(ExprBuf buf) { - return parseStringUntil(buf, OBJ_NAME_END_CHAR, OBJ_NAME_START_CHAR, false); + return parseStringUntil(buf, OBJ_NAME_START_CHAR, OBJ_NAME_END_CHAR, false); } private static String parseDateLiteralString(ExprBuf buf) { - return parseStringUntil(buf, DATE_LIT_QUOTE_CHAR, null, false); + return parseStringUntil(buf, null, DATE_LIT_QUOTE_CHAR, false); } - private static String parseStringUntil(ExprBuf buf, char endChar, - Character startChar, - boolean allowDoubledEscape) + static String parseStringUntil(ExprBuf buf, Character startChar, + char endChar, boolean allowDoubledEscape) { - StringBuilder sb = buf.getScratchBuffer(); + return parseStringUntil(buf, startChar, endChar, allowDoubledEscape, + buf.getScratchBuffer()) + .toString(); + } + static StringBuilder parseStringUntil( + ExprBuf buf, Character startChar, char endChar, boolean allowDoubledEscape, + StringBuilder sb) + { boolean complete = false; while(buf.hasNext()) { char c = buf.next(); if(c == endChar) { if(allowDoubledEscape && (buf.peekNext() == endChar)) { - sb.append(endChar); buf.next(); } else { complete = true; @@ -281,7 +286,7 @@ class ExpressionTokenizer "' for quoted string " + buf); } - return sb.toString(); + return sb; } private static Token parseDateLiteral(ExprBuf buf) @@ -451,7 +456,7 @@ class ExpressionTokenizer return new AbstractMap.SimpleImmutableEntry(a, b); } - private static final class ExprBuf + static final class ExprBuf { private final String _str; private final ParseContext _ctx; @@ -461,7 +466,7 @@ class ExpressionTokenizer TemporalConfig.Type.class); private final StringBuilder _scratch = new StringBuilder(); - private ExprBuf(String str, ParseContext ctx) { + ExprBuf(String str, ParseContext ctx) { _str = str; _ctx = ctx; } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java index a21cd88..1cd7b3c 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java @@ -17,16 +17,30 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; -import java.text.DecimalFormat; import java.text.NumberFormat; +import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalField; +import java.time.temporal.WeekFields; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; import com.healthmarketscience.jackcess.expr.EvalContext; +import com.healthmarketscience.jackcess.expr.EvalException; import com.healthmarketscience.jackcess.expr.NumericConfig; import com.healthmarketscience.jackcess.expr.TemporalConfig; import com.healthmarketscience.jackcess.expr.Value; +import static com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.ExprBuf; /** * @@ -35,10 +49,21 @@ import com.healthmarketscience.jackcess.expr.Value; public class FormatUtil { public enum NumPatternType { - GENERAL, CURRENCY { + GENERAL, + CURRENCY { @Override protected void appendPrefix(StringBuilder fmt) { - fmt.append("\u00A4"); + fmt.append('\u00A4'); + } + @Override + protected boolean useParensForNegatives(NumericConfig cfg) { + return cfg.useParensForCurrencyNegatives(); + } + }, + EURO { + @Override + protected void appendPrefix(StringBuilder fmt) { + fmt.append('\u20AC'); } @Override protected boolean useParensForNegatives(NumericConfig cfg) { @@ -48,7 +73,7 @@ public class FormatUtil PERCENT { @Override protected void appendSuffix(StringBuilder fmt) { - fmt.append("%"); + fmt.append('%'); } }, SCIENTIFIC { @@ -67,10 +92,29 @@ public class FormatUtil } } + private enum TextCase { + NONE, + UPPER { + @Override public char apply(char c) { + return Character.toUpperCase(c); + } + }, + LOWER { + @Override public char apply(char c) { + return Character.toLowerCase(c); + } + }; + + public char apply(char c) { + return c; + } + } + private static final Map PREDEF_FMTS = new HashMap(); static { - PREDEF_FMTS.put("General Date", new GenPredefDateFmt()); + PREDEF_FMTS.put("General Date", args -> ValueSupport.toValue( + args.coerceToDateTimeValue().getAsString())); PREDEF_FMTS.put("Long Date", new PredefDateFmt(TemporalConfig.Type.LONG_DATE)); PREDEF_FMTS.put("Medium Date", @@ -84,12 +128,11 @@ public class FormatUtil PREDEF_FMTS.put("Short Time", new PredefDateFmt(TemporalConfig.Type.SHORT_TIME)); - PREDEF_FMTS.put("General Number", new GenPredefNumberFmt()); + PREDEF_FMTS.put("General Number", args -> ValueSupport.toValue( + args.coerceToNumberValue().getAsString())); PREDEF_FMTS.put("Currency", new PredefNumberFmt(NumericConfig.Type.CURRENCY)); - // FIXME ? - // PREDEF_FMTS.put("Euro", - // new PredefNumberFmt(???)); + PREDEF_FMTS.put("Euro", new PredefNumberFmt(NumericConfig.Type.EURO)); PREDEF_FMTS.put("Fixed", new PredefNumberFmt(NumericConfig.Type.FIXED)); PREDEF_FMTS.put("Standard", @@ -103,25 +146,703 @@ public class FormatUtil PREDEF_FMTS.put("On/Off", new PredefBoolFmt("On", "Off")); } + private static final Fmt NULL_FMT = args -> ValueSupport.EMPTY_STR_VAL; + + private static final char QUOTE_CHAR = '"'; + private static final char ESCAPE_CHAR = '\\'; + private static final char LEFT_ALIGN_CHAR = '!'; + private static final char START_COLOR_CHAR = '['; + private static final char END_COLOR_CHAR = ']'; + private static final char CHOICE_SEP_CHAR = ';'; + // this only seems to be useful if you have fixed length string fields which + // isn't a normal thing in ms access + private static final char FILL_ESCAPE_CHAR = '*'; + private static final char REQ_PLACEHOLDER_CHAR = '@'; + private static final char OPT_PLACEHOLDER_CHAR = '&'; + private static final char TO_UPPER_CHAR = '>'; + private static final char TO_LOWER_CHAR = '<'; + private static final char DT_LIT_COLON_CHAR = ':'; + private static final char DT_LIT_SLASH_CHAR = '/'; + private static final int NO_CHAR = -1; + + private static final byte FCT_UNKNOWN = 0; + private static final byte FCT_LITERAL = 1; + private static final byte FCT_GENERAL = 2; + private static final byte FCT_DATE = 3; + private static final byte FCT_NUMBER = 4; + private static final byte FCT_TEXT = 5; + + private static final byte[] FORMAT_CODE_TYPES = new byte[127]; + static { + setFormatCodeTypes(" $+-()", FCT_LITERAL); + setFormatCodeTypes("\"!*\\[];", FCT_GENERAL); + setFormatCodeTypes(":/cdwmqyhnstampmAMPM", FCT_DATE); + setFormatCodeTypes(".,0#%Ee", FCT_NUMBER); + setFormatCodeTypes("@&<>", FCT_TEXT); + } + + @FunctionalInterface + interface Fmt { + public Value format(Args args); + } + + @FunctionalInterface + interface DateFormatBuilder { + public void build(DateTimeFormatterBuilder dtfb, Args args); + } + + private static final DateFormatBuilder PARTIAL_PREFIX = + (dtfb, args) -> { + throw new UnsupportedOperationException(); + }; + + private static final Map DATE_FMT_BUILDERS = + new HashMap<>(); + static { + DATE_FMT_BUILDERS.put("c", + (dtfb, args) -> + dtfb.append(ValueSupport.getDateFormatForType( + args._ctx, args._expr.getType()))); + DATE_FMT_BUILDERS.put("d", new SimpleDFB("d")); + DATE_FMT_BUILDERS.put("dd", new SimpleDFB("dd")); + DATE_FMT_BUILDERS.put("ddd", new SimpleDFB("eee")); + DATE_FMT_BUILDERS.put("dddd", new SimpleDFB("eeee")); + DATE_FMT_BUILDERS.put("ddddd", new PredefDFB(TemporalConfig.Type.SHORT_DATE)); + DATE_FMT_BUILDERS.put("dddddd", new PredefDFB(TemporalConfig.Type.LONG_DATE)); + DATE_FMT_BUILDERS.put("w", new WeekBasedDFB() { + @Override + protected TemporalField getField(WeekFields weekFields) { + return weekFields.dayOfWeek(); + } + }); + DATE_FMT_BUILDERS.put("ww", new WeekBasedDFB() { + @Override + protected TemporalField getField(WeekFields weekFields) { + return weekFields.weekOfWeekBasedYear(); + } + }); + DATE_FMT_BUILDERS.put("m", new SimpleDFB("M")); + DATE_FMT_BUILDERS.put("mm", new SimpleDFB("MM")); + DATE_FMT_BUILDERS.put("mmm", new SimpleDFB("LLL")); + DATE_FMT_BUILDERS.put("mmmm", new SimpleDFB("LLLL")); + DATE_FMT_BUILDERS.put("q", new SimpleDFB("Q")); + DATE_FMT_BUILDERS.put("y", new SimpleDFB("D")); + DATE_FMT_BUILDERS.put("yy", new SimpleDFB("yy")); + DATE_FMT_BUILDERS.put("yyyy", new SimpleDFB("yyyy")); + DATE_FMT_BUILDERS.put("h", new SimpleDFB("H")); + DATE_FMT_BUILDERS.put("hh", new SimpleDFB("HH")); + DATE_FMT_BUILDERS.put("n", new SimpleDFB("m")); + DATE_FMT_BUILDERS.put("nn", new SimpleDFB("mm")); + DATE_FMT_BUILDERS.put("s", new SimpleDFB("s")); + DATE_FMT_BUILDERS.put("ss", new SimpleDFB("ss")); + DATE_FMT_BUILDERS.put("ttttt", new PredefDFB(TemporalConfig.Type.LONG_TIME)); + DATE_FMT_BUILDERS.put("AM/PM", new AmPmDFB("AM", "PM")); + DATE_FMT_BUILDERS.put("am/pm", new AmPmDFB("am", "pm")); + DATE_FMT_BUILDERS.put("A/P", new AmPmDFB("A", "P")); + DATE_FMT_BUILDERS.put("a/p", new AmPmDFB("a", "p")); + DATE_FMT_BUILDERS.put("AMPM", + (dtfb, args) -> { + String[] amPmStrs = args._ctx.getTemporalConfig().getAmPmStrings(); + new AmPmDFB(amPmStrs[0], amPmStrs[1]).build(dtfb, args); + } + ); + fillInPartialPrefixes(); + } + + private static final int NF_POS_IDX = 0; + private static final int NF_NEG_IDX = 1; + private static final int NF_ZERO_IDX = 2; + private static final int NF_NULL_IDX = 3; + private static final int NUM_NF_FMTS = 4; + + private static final class Args + { + private final EvalContext _ctx; + private Value _expr; + private final int _firstDay; + private final int _firstWeekType; + + private Args(EvalContext ctx, Value expr, int firstDay, int firstWeekType) { + _ctx = ctx; + _expr = expr; + _firstDay = firstDay; + _firstWeekType = firstWeekType; + } + + public Args coerceToDateTimeValue() { + if(!_expr.getType().isTemporal()) { + + // format coerces boolean strings to numbers + Value boolExpr = null; + if(_expr.getType().isString() && + ((boolExpr = maybeGetStringAsBooleanValue()) != null)) { + _expr = boolExpr; + } + + // StringValue already handles most String -> Number -> Date/Time, so + // most other convertions work here + _expr = _expr.getAsDateTimeValue(_ctx); + } + return this; + } + + public Args coerceToNumberValue() { + if(!_expr.getType().isNumeric()) { + if(_expr.getType().isString()) { + + // format coerces "true"/"false" to boolean values + Value boolExpr = maybeGetStringAsBooleanValue(); + + if(boolExpr != null) { + _expr = boolExpr; + } else { + BigDecimal bd = DefaultFunctions.maybeGetAsBigDecimal(_ctx, _expr); + if(bd != null) { + _expr = ValueSupport.toValue(bd); + } else { + // convert to date to number. this doesn't happen as part of the + // default value coercion behavior, but the format method tries + // harder + Value maybe = DefaultFunctions.maybeGetAsDateTimeValue( + _ctx, _expr); + if(maybe != null) { + _expr = ValueSupport.toValue(maybe.getAsDouble(_ctx)); + } + } + } + } else { + // convert date to number + _expr = ValueSupport.toValue(_expr.getAsDouble(_ctx)); + } + } + return this; + } + + private Value maybeGetStringAsBooleanValue() { + // format coerces "true"/"false" to boolean values + String val = _expr.getAsString(_ctx); + if("true".equalsIgnoreCase(val)) { + return ValueSupport.TRUE_VAL; + } + if("false".equalsIgnoreCase(val)) { + return ValueSupport.FALSE_VAL; + } + return null; + } + + public BigDecimal getAsBigDecimal() { + coerceToNumberValue(); + return _expr.getAsBigDecimal(_ctx); + } + + public LocalDateTime getAsLocalDateTime() { + coerceToDateTimeValue(); + return _expr.getAsLocalDateTime(_ctx); + } + + public String getAsString() { + return _expr.getAsString(_ctx); + } + } + private FormatUtil() {} public static Value format(EvalContext ctx, Value expr, String fmtStr, int firstDay, int firstWeekType) { - Fmt predefFmt = PREDEF_FMTS.get(fmtStr); - if(predefFmt != null) { - if(expr.isNull()) { - // predefined formats return null for null - return ValueSupport.NULL_VAL; + try { + + Args args = new Args(ctx, expr, firstDay, firstWeekType); + + Fmt predefFmt = PREDEF_FMTS.get(fmtStr); + if(predefFmt != null) { + if(expr.isNull()) { + // predefined formats return empty string for null + return ValueSupport.EMPTY_STR_VAL; + } + return predefFmt.format(args); + } + + return parseCustomFormat(fmtStr, args).format(args); + + } catch(EvalException ee) { + // values which cannot be formatted as the target type are just + // returned "as is" + return expr; + } + } + + private static Fmt parseCustomFormat(String fmtStr, Args args) { + + ExprBuf buf = new ExprBuf(fmtStr, null); + + // do partial pass to determine what type of format this is + byte curFormatType = determineFormatType(buf); + + // reset buffer for real parse + buf.reset(0); + + switch(curFormatType) { + case FCT_GENERAL: + return parseCustomGeneralFormat(buf, args); + case FCT_DATE: + return parseCustomDateFormat(buf, args); + case FCT_NUMBER: + return parseCustomNumberFormat(buf, args); + case FCT_TEXT: + return parseCustomTextFormat(buf, args); + default: + throw new EvalException("Invalid format type " + curFormatType); + } + } + + private static byte determineFormatType(ExprBuf buf) { + + while(buf.hasNext()) { + char c = buf.next(); + byte fmtType = getFormatCodeType(c); + switch(fmtType) { + case FCT_UNKNOWN: + case FCT_LITERAL: + // meaningless, ignore for now + break; + case FCT_GENERAL: + switch(c) { + case QUOTE_CHAR: + parseQuotedString(buf); + break; + case START_COLOR_CHAR: + parseColorString(buf); + break; + case ESCAPE_CHAR: + case FILL_ESCAPE_CHAR: + if(buf.hasNext()) { + buf.next(); + } + break; + default: + // meaningless, ignore for now + } + break; + case FCT_DATE: + case FCT_NUMBER: + case FCT_TEXT: + // found specific type + return fmtType; + default: + throw new EvalException("Invalid format type " + fmtType); } - return predefFmt.format(ctx, expr, null, firstDay, firstWeekType); } - // FIXME, + // no specific type + return FCT_GENERAL; + } + + private static Fmt parseCustomGeneralFormat(ExprBuf buf, Args args) { + + // a "general" format is actually a "yes/no" format which functions almost + // exactly like a number format (without any number format specific chars) + if(!args._expr.isNull()) { + args.coerceToNumberValue(); + } + + StringBuilder sb = new StringBuilder(); + String[] fmtStrs = new String[NUM_NF_FMTS]; + int fmtIdx = 0; + + BUF_LOOP: + while(buf.hasNext()) { + char c = buf.next(); + int fmtType = getFormatCodeType(c); + switch(fmtType) { + case FCT_GENERAL: + switch(c) { + case LEFT_ALIGN_CHAR: + // no effect + break; + case QUOTE_CHAR: + parseQuotedString(buf, sb); + break; + case START_COLOR_CHAR: + // color strings seem to be ignored + parseColorString(buf); + break; + case ESCAPE_CHAR: + if(buf.hasNext()) { + sb.append(buf.next()); + } + break; + case FILL_ESCAPE_CHAR: + // unclear what this actually does. online examples don't seem to + // match with experimental results. for now, ignore + if(buf.hasNext()) { + buf.next(); + } + break; + case CHOICE_SEP_CHAR: + // yes/no (number) format supports up to 4 formats: pos, neg, zero, + // null. after that, ignore the rest + if(fmtIdx == (NUM_NF_FMTS - 1)) { + // last possible format, ignore remaining + break BUF_LOOP; + } + addCustomGeneralFormat(fmtStrs, fmtIdx++, sb); + break; + default: + sb.append(c); + } + break; + default: + sb.append(c); + } + } + + // fill in remaining formats + while(fmtIdx < NUM_NF_FMTS) { + addCustomGeneralFormat(fmtStrs, fmtIdx++, sb); + } + + return new CustomNumberFmt( + createCustomLiteralFormat(fmtStrs[NF_POS_IDX]), + createCustomLiteralFormat(fmtStrs[NF_NEG_IDX]), + createCustomLiteralFormat(fmtStrs[NF_ZERO_IDX]), + createCustomLiteralFormat(fmtStrs[NF_NULL_IDX])); + } + + private static void addCustomGeneralFormat(String[] fmtStrs, int fmtIdx, + StringBuilder sb) + { + if(sb.length() == 0) { + // do special empty format handling on a per-format-type basis + switch(fmtIdx) { + case NF_NEG_IDX: + // re-use "pos" format with '-' prepended + sb.append('-').append(fmtStrs[NF_POS_IDX]); + break; + case NF_ZERO_IDX: + // re-use "pos" format + sb.append(fmtStrs[NF_POS_IDX]); + break; + default: + // use empty string result + } + } + + fmtStrs[fmtIdx] = sb.toString(); + sb.setLength(0); + } + + private static Fmt createCustomLiteralFormat(String str) { + Value literal = ValueSupport.toValue(str); + return args -> literal; + } + + private static Fmt parseCustomDateFormat(ExprBuf buf, Args args) { + + // custom date formats don't have special null handling + if(args._expr.isNull()) { + return NULL_FMT; + } + + // force to temporal value before proceeding (this will throw if we don't + // have a date/time and therefore don't need to proceed with the rest of + // the format translation work) + args.coerceToDateTimeValue(); + + DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder(); + + BUF_LOOP: + while(buf.hasNext()) { + char c = buf.next(); + int fmtType = getFormatCodeType(c); + switch(fmtType) { + case FCT_GENERAL: + switch(c) { + case QUOTE_CHAR: + dtfb.appendLiteral(parseQuotedString(buf)); + break; + case START_COLOR_CHAR: + // color strings seem to be ignored + parseColorString(buf); + break; + case ESCAPE_CHAR: + if(buf.hasNext()) { + dtfb.appendLiteral(buf.next()); + } + break; + case FILL_ESCAPE_CHAR: + // unclear what this actually does. online examples don't seem to + // match with experimental results. for now, ignore + if(buf.hasNext()) { + buf.next(); + } + break; + case CHOICE_SEP_CHAR: + // date/time doesn't use multiple pattern choices, but it does + // respect the char. ignore everything after the first choice + break BUF_LOOP; + default: + dtfb.appendLiteral(c); + } + break; + case FCT_DATE: + parseCustomDateFormatPattern(c, buf, dtfb, args); + break; + default: + dtfb.appendLiteral(c); + } + } + + DateTimeFormatter dtf = dtfb.toFormatter( + args._ctx.getTemporalConfig().getLocale()); + + return argsParam -> ValueSupport.toValue( + dtf.format(argsParam.getAsLocalDateTime())); + } + + private static Fmt parseCustomNumberFormat(ExprBuf buf, Args args) { + + + // force to number value before proceeding (this will throw if we don't + // have a number and therefore don't need to proceed with the rest of + // the format translation work) + if(!args._expr.isNull()) { + args.coerceToNumberValue(); + } + + StringBuilder sb = new StringBuilder(); + String[] fmtStrs = new String[NUM_NF_FMTS]; + int fmtIdx = 0; + + BUF_LOOP: + while(buf.hasNext()) { + char c = buf.next(); + int fmtType = getFormatCodeType(c); + switch(fmtType) { + case FCT_GENERAL: + switch(c) { + case LEFT_ALIGN_CHAR: + // no effect + break; + case QUOTE_CHAR: + parseQuotedString(buf, sb); + break; + case START_COLOR_CHAR: + // color strings seem to be ignored + parseColorString(buf); + break; + case ESCAPE_CHAR: + if(buf.hasNext()) { + sb.append(buf.next()); + } + break; + case FILL_ESCAPE_CHAR: + // unclear what this actually does. online examples don't seem to + // match with experimental results. for now, ignore + if(buf.hasNext()) { + buf.next(); + } + break; + case CHOICE_SEP_CHAR: + // yes/no (number) format supports up to 4 formats: pos, neg, zero, + // null. after that, ignore the rest + if(fmtIdx == (NUM_NF_FMTS - 1)) { + // last possible format, ignore remaining + break BUF_LOOP; + } + addCustomGeneralFormat(fmtStrs, fmtIdx++, sb); + break; + default: + sb.append(c); + } + break; + default: + sb.append(c); + } + } + + // fill in remaining formats + while(fmtIdx < NUM_NF_FMTS) { + addCustomGeneralFormat(fmtStrs, fmtIdx++, sb); + } + + // FIXME writeme throw new UnsupportedOperationException(); } + private static Fmt parseCustomTextFormat(ExprBuf buf, Args args) { + + Fmt fmt = null; + Fmt emptyFmt = null; + + List> subFmts = new ArrayList<>(); + int numPlaceholders = 0; + boolean rightAligned = true; + TextCase textCase = TextCase.NONE; + StringBuilder pendingLiteral = new StringBuilder(); + boolean hasFmtChars = false; + + BUF_LOOP: + while(buf.hasNext()) { + char c = buf.next(); + hasFmtChars = true; + int fmtType = getFormatCodeType(c); + switch(fmtType) { + case FCT_GENERAL: + switch(c) { + case LEFT_ALIGN_CHAR: + rightAligned = false; + break; + case QUOTE_CHAR: + parseQuotedString(buf, pendingLiteral); + break; + case START_COLOR_CHAR: + // color strings seem to be ignored + parseColorString(buf); + break; + case ESCAPE_CHAR: + if(buf.hasNext()) { + pendingLiteral.append(buf.next()); + } + break; + case FILL_ESCAPE_CHAR: + // unclear what this actually does. online examples don't seem to + // match with experimental results. for now, ignore + if(buf.hasNext()) { + buf.next(); + } + break; + case CHOICE_SEP_CHAR: + // text format supports up to 2 formats: normal and empty/null. + // after that, ignore the rest + if(fmt != null) { + // ignore remaining format + break BUF_LOOP; + } + flushPendingTextLiteral(pendingLiteral, subFmts); + fmt = new CharSourceFmt(subFmts, numPlaceholders, rightAligned, + textCase); + // reset for next format + subFmts = new ArrayList<>(); + numPlaceholders = 0; + rightAligned = true; + textCase = TextCase.NONE; + hasFmtChars = false; + break; + default: + pendingLiteral.append(c); + } + break; + case FCT_TEXT: + switch(c) { + case REQ_PLACEHOLDER_CHAR: + flushPendingTextLiteral(pendingLiteral, subFmts); + ++numPlaceholders; + subFmts.add((sb,cs) -> { + int tmp = cs.next(); + sb.append((tmp != NO_CHAR) ? (char)tmp : ' '); + }); + break; + case OPT_PLACEHOLDER_CHAR: + flushPendingTextLiteral(pendingLiteral, subFmts); + ++numPlaceholders; + subFmts.add((sb,cs) -> { + int tmp = cs.next(); + if(tmp != NO_CHAR) { + sb.append((char)tmp); + } + }); + break; + case TO_UPPER_CHAR: + // an uppper and lower symbol cancel each other out + textCase = ((textCase == TextCase.LOWER) ? + TextCase.NONE : TextCase.UPPER); + break; + case TO_LOWER_CHAR: + // an uppper and lower symbol cancel each other out + textCase = ((textCase == TextCase.UPPER) ? + TextCase.NONE : TextCase.LOWER); + break; + default: + pendingLiteral.append(c); + } + break; + default: + pendingLiteral.append(c); + } + } + + flushPendingTextLiteral(pendingLiteral, subFmts); + + if(fmt == null) { + fmt = new CharSourceFmt(subFmts, numPlaceholders, rightAligned, + textCase); + emptyFmt = NULL_FMT; + } else if(emptyFmt == null) { + emptyFmt = (hasFmtChars ? + new CharSourceFmt(subFmts, numPlaceholders, rightAligned, + textCase) : + NULL_FMT); + } + + return new CustomTextFmt(fmt, emptyFmt); + } + + private static void flushPendingTextLiteral( + StringBuilder pendingLiteral, + List> subFmts) { + if(pendingLiteral.length() == 0) { + return; + } + + String literal = pendingLiteral.toString(); + pendingLiteral.setLength(0); + subFmts.add((sb, cs) -> sb.append(literal)); + } + + private static void parseCustomDateFormatPattern( + char c, ExprBuf buf, DateTimeFormatterBuilder dtfb, Args args) { + + if((c == DT_LIT_COLON_CHAR) || (c == DT_LIT_SLASH_CHAR)) { + // date/time literal char, nothing more to do + dtfb.appendLiteral(c); + return; + } + + StringBuilder sb = buf.getScratchBuffer(); + sb.append(c); + + char firstChar = c; + int firstPos = buf.curPos(); + + DateFormatBuilder bestMatch = DATE_FMT_BUILDERS.get(sb.toString()); + int bestPos = firstPos; + while(buf.hasNext()) { + sb.append(buf.next()); + DateFormatBuilder dfb = DATE_FMT_BUILDERS.get(sb.toString()); + if(dfb == null) { + // no more possible matches + break; + } + if(dfb != PARTIAL_PREFIX) { + // this is the longest, valid pattern we have seen so far + bestMatch = dfb; + bestPos = buf.curPos(); + } + } + + if(bestMatch != PARTIAL_PREFIX) { + // apply valid pattern + buf.reset(bestPos); + bestMatch.build(dtfb, args); + } else { + // just consume the first char + buf.reset(firstPos); + dtfb.appendLiteral(firstChar); + } + } + public static String createNumberFormatPattern( NumPatternType numPatType, int numDecDigits, boolean incLeadDigit, boolean negParens, int numGroupDigits) { @@ -153,16 +874,45 @@ public class FormatUtil return fmt.toString(); } + private static byte getFormatCodeType(char c) { + if((c >= 0) && (c < 127)) { + return FORMAT_CODE_TYPES[c]; + } + return FCT_UNKNOWN; + } + + private static void setFormatCodeTypes(String chars, byte type) { + for(char c : chars.toCharArray()) { + FORMAT_CODE_TYPES[c] = type; + } + } - private static abstract class Fmt - { - // FIXME, no null - // FIXME, need fmtStr? - public abstract Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType); + private static String parseQuotedString(ExprBuf buf) { + return ExpressionTokenizer.parseStringUntil(buf, null, QUOTE_CHAR, true); + } + + private static void parseQuotedString(ExprBuf buf, StringBuilder sb) { + ExpressionTokenizer.parseStringUntil(buf, null, QUOTE_CHAR, true, sb); + } + + private static String parseColorString(ExprBuf buf) { + return ExpressionTokenizer.parseStringUntil( + buf, END_COLOR_CHAR, START_COLOR_CHAR, false); } - private static class PredefDateFmt extends Fmt + private static void fillInPartialPrefixes() { + List validPrefixes = new ArrayList<>(DATE_FMT_BUILDERS.keySet()); + for(String validPrefix : validPrefixes) { + int len = validPrefix.length(); + while(len > 1) { + --len; + validPrefix = validPrefix.substring(0, len); + DATE_FMT_BUILDERS.putIfAbsent(validPrefix, PARTIAL_PREFIX); + } + } + } + + private static final class PredefDateFmt implements Fmt { private final TemporalConfig.Type _type; @@ -171,31 +921,14 @@ public class FormatUtil } @Override - public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - DateTimeFormatter dtf = ctx.createDateFormatter( - ctx.getTemporalConfig().getDateTimeFormat(_type)); - return ValueSupport.toValue(dtf.format(expr.getAsLocalDateTime(ctx))); - } - } - - private static class GenPredefDateFmt extends Fmt - { - @Override - public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - Value tempExpr = expr; - if(!expr.getType().isTemporal()) { - Value maybe = DefaultFunctions.maybeGetAsDateTimeValue(ctx, expr); - if(maybe != null) { - tempExpr = maybe; - } - } - return ValueSupport.toValue(tempExpr.getAsString(ctx)); + public Value format(Args args) { + DateTimeFormatter dtf = args._ctx.createDateFormatter( + args._ctx.getTemporalConfig().getDateTimeFormat(_type)); + return ValueSupport.toValue(dtf.format(args.getAsLocalDateTime())); } } - private static class PredefBoolFmt extends Fmt + private static final class PredefBoolFmt implements Fmt { private final Value _trueVal; private final Value _falseVal; @@ -206,13 +939,23 @@ public class FormatUtil } @Override - public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - return(expr.getAsBoolean(ctx) ? _trueVal : _falseVal); + public Value format(Args args) { + return(args._expr.getAsBoolean(args._ctx) ? _trueVal : _falseVal); } } - private static class PredefNumberFmt extends Fmt + private static abstract class BaseNumberFmt implements Fmt + { + @Override + public Value format(Args args) { + NumberFormat df = getNumberFormat(args); + return ValueSupport.toValue(df.format(args.getAsBigDecimal())); + } + + protected abstract NumberFormat getNumberFormat(Args args); + } + + private static final class PredefNumberFmt extends BaseNumberFmt { private final NumericConfig.Type _type; @@ -221,51 +964,240 @@ public class FormatUtil } @Override - public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - DecimalFormat df = ctx.createDecimalFormat( - ctx.getNumericConfig().getNumberFormat(_type)); - return ValueSupport.toValue(df.format(expr.getAsBigDecimal(ctx))); + protected NumberFormat getNumberFormat(Args args) { + return args._ctx.createDecimalFormat( + args._ctx.getNumericConfig().getNumberFormat(_type)); } } - private static class GenPredefNumberFmt extends Fmt + private static final class ScientificPredefNumberFmt extends BaseNumberFmt { @Override - public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - Value numExpr = expr; - if(!expr.getType().isNumeric()) { - if(expr.getType().isString()) { - BigDecimal bd = DefaultFunctions.maybeGetAsBigDecimal(ctx, expr); - if(bd != null) { - numExpr = ValueSupport.toValue(bd); - } else { - // convert to date to number - Value maybe = DefaultFunctions.maybeGetAsDateTimeValue(ctx, expr); - if(maybe != null) { - numExpr = ValueSupport.toValue(maybe.getAsDouble(ctx)); - } - } - } else { - // convert date to number - numExpr = ValueSupport.toValue(expr.getAsDouble(ctx)); + protected NumberFormat getNumberFormat(Args args) { + NumberFormat df = args._ctx.createDecimalFormat( + args._ctx.getNumericConfig().getNumberFormat( + NumericConfig.Type.SCIENTIFIC)); + df = new NumberFormatter.ScientificFormat(df); + return df; + } + } + + private static final class NumberFmt extends BaseNumberFmt + { + private final NumberFormat _df; + + private NumberFmt(NumberFormat df) { + _df = df; + } + + @Override + protected NumberFormat getNumberFormat(Args args) { + return _df; + } + } + + private static final class SimpleDFB implements DateFormatBuilder + { + private final String _pat; + + private SimpleDFB(String pat) { + _pat = pat; + } + @Override + public void build(DateTimeFormatterBuilder dtfb, Args args) { + dtfb.appendPattern(_pat); + } + } + + private static final class PredefDFB implements DateFormatBuilder + { + private final TemporalConfig.Type _type; + + private PredefDFB(TemporalConfig.Type type) { + _type = type; + } + @Override + public void build(DateTimeFormatterBuilder dtfb, Args args) { + dtfb.appendPattern(args._ctx.getTemporalConfig().getDateTimeFormat(_type)); + } + } + + private static abstract class WeekBasedDFB implements DateFormatBuilder + { + @Override + public void build(DateTimeFormatterBuilder dtfb, Args args) { + dtfb.appendValue(getField(DefaultDateFunctions.weekFields( + args._firstDay, args._firstWeekType))); + } + + protected abstract TemporalField getField(WeekFields weekFields); + } + + private static final class AmPmDFB extends AbstractMap + implements DateFormatBuilder + { + private static final Long ZERO_KEY = 0L; + private final String _am; + private final String _pm; + + private AmPmDFB(String am, String pm) { + _am = am; + _pm = pm; + } + @Override + public void build(DateTimeFormatterBuilder dtfb, Args args) { + dtfb.appendText(ChronoField.AMPM_OF_DAY, this); + } + @Override + public int size() { + return 2; + } + @Override + public String get(Object key) { + return(ZERO_KEY.equals(key) ? _am : _pm); + } + @Override + public Set> entrySet() { + return new AbstractSet>() { + @Override + public int size() { + return 2; } + @Override + public Iterator> iterator() { + return Arrays.>asList( + new AbstractMap.SimpleImmutableEntry(0L, _am), + new AbstractMap.SimpleImmutableEntry(1L, _pm)) + .iterator(); + } + }; + } + } + + private static final class CustomTextFmt implements Fmt + { + private final Fmt _fmt; + private final Fmt _emptyFmt; + + private CustomTextFmt(Fmt fmt, Fmt emptyFmt) { + _fmt = fmt; + _emptyFmt = emptyFmt; + } + + private static boolean isEmptyString(Args args) { + // only a string value could ever be an empty string + return (args._expr.getType().isString() && args.getAsString().isEmpty()); + } + + @Override + public Value format(Args args) { + Fmt fmt = _fmt; + if(args._expr.isNull() || isEmptyString(args)) { + fmt = _emptyFmt; + // ensure that we have a non-null value when formatting (null acts + // like empty string in this case) + args._expr = ValueSupport.EMPTY_STR_VAL; } - return ValueSupport.toValue(numExpr.getAsString(ctx)); + return fmt.format(args); } } - private static class ScientificPredefNumberFmt extends Fmt + private static final class CharSourceFmt implements Fmt { + private final List> _subFmts; + private final int _numPlaceholders; + private final boolean _rightAligned; + private final TextCase _textCase; + + private CharSourceFmt(List> subFmts, + int numPlaceholders, boolean rightAligned, TextCase textCase) { + _subFmts = subFmts; + _numPlaceholders = numPlaceholders; + _rightAligned = rightAligned; + _textCase = textCase; + } + @Override - public Value format(EvalContext ctx, Value expr, String fmtStr, - int firstDay, int firstWeekType) { - NumberFormat df = ctx.createDecimalFormat( - ctx.getNumericConfig().getNumberFormat( - NumericConfig.Type.SCIENTIFIC)); - df = new NumberFormatter.ScientificFormat(df); - return ValueSupport.toValue(df.format(expr.getAsBigDecimal(ctx))); + public Value format(Args args) { + CharSource cs = new CharSource(args.getAsString(), _numPlaceholders, _rightAligned, + _textCase); + StringBuilder sb = new StringBuilder(); + _subFmts.stream().forEach(fmt -> fmt.accept(sb, cs)); + cs.appendRemaining(sb); + return ValueSupport.toValue(sb.toString()); + } + } + + private static final class CharSource + { + private int _prefLen; + private final String _str; + private int _strPos; + private final TextCase _textCase; + + private CharSource(String str, int len, boolean rightAligned, + TextCase textCase) { + _str = str; + _textCase = textCase; + int strLen = str.length(); + if(len > strLen) { + if(rightAligned) { + _prefLen = len - strLen; + } + } else if(len < strLen) { + // it doesn't make sense to me, but the meaning of "right aligned" + // seems to flip when the string is longer than the format length + if(!rightAligned) { + _strPos = strLen - len; + } + } + } + + public int next() { + if(_prefLen > 0) { + --_prefLen; + return NO_CHAR; + } + if(_strPos < _str.length()) { + return _textCase.apply(_str.charAt(_strPos++)); + } + return NO_CHAR; + } + + public void appendRemaining(StringBuilder sb) { + int strLen = _str.length(); + while(_strPos < strLen) { + sb.append(_textCase.apply(_str.charAt(_strPos++))); + } + } + } + + private static final class CustomNumberFmt implements Fmt + { + private final Fmt _posFmt; + private final Fmt _negFmt; + private final Fmt _zeroFmt; + private final Fmt _nullFmt; + + private CustomNumberFmt(Fmt posFmt, Fmt negFmt, Fmt zeroFmt, Fmt nullFmt) { + _posFmt = posFmt; + _negFmt = negFmt; + _zeroFmt = zeroFmt; + _nullFmt = nullFmt; + } + + @Override + public Value format(Args args) { + if(args._expr.isNull()) { + return _nullFmt.format(args); + } + BigDecimal bd = args.getAsBigDecimal(); + int cmp = BigDecimal.ZERO.compareTo(bd); + Fmt fmt = ((cmp < 0) ? _posFmt : + ((cmp > 0) ? _negFmt : _zeroFmt)); + return fmt.format(args); } } + + } diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java index 895bbed..49e7b8d 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java @@ -19,7 +19,6 @@ package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Calendar; -import java.util.Date; import com.healthmarketscience.jackcess.expr.EvalException; import junit.framework.TestCase; @@ -145,6 +144,7 @@ public class DefaultFunctionsTest extends TestCase assertEval("FOOO", "=UCase(\"fOoO\")"); assertEval("fooo", "=LCase(\"fOoO\")"); + assertEval(" FOO \" BAR ", "=UCase(\" foo \"\" bar \")"); assertEval("bl", "=Left(\"blah\", 2)"); assertEval("", "=Left(\"blah\", 0)"); @@ -285,6 +285,11 @@ public class DefaultFunctionsTest extends TestCase assertEval("-12345.68", "=Format(-12345.6789, 'Fixed')"); assertEval("-0.12", "=Format(-0.12345, 'Fixed')"); + assertEval("\u20AC12,345.68", "=Format(12345.6789, 'Euro')"); + assertEval("\u20AC0.12", "=Format(0.12345, 'Euro')"); + assertEval("(\u20AC12,345.68)", "=Format(-12345.6789, 'Euro')"); + assertEval("(\u20AC0.12)", "=Format(-0.12345, 'Euro')"); + assertEval("$12,345.68", "=Format(12345.6789, 'Currency')"); assertEval("$0.12", "=Format(0.12345, 'Currency')"); assertEval("($12,345.68)", "=Format(-12345.6789, 'Currency')"); @@ -321,6 +326,16 @@ public class DefaultFunctionsTest extends TestCase assertEval("07:00", "=Format(#01/02/2003 7:00:00 AM#, 'Short Time')"); assertEval("19:00", "=Format(#01/02/2003 7:00:00 PM#, 'Short Time')"); + assertEval("07:00 a", "=Format(#01/10/2003 7:00:00 AM#, 'hh:nn a/p')"); + assertEval("07:00 a 6 2", "=Format(#01/10/2003 7:00:00 AM#, 'hh:nn a/p w ww')"); + assertEval("07:00 a 4 1", "=Format(#01/10/2003 7:00:00 AM#, 'hh:nn a/p w ww', 3, 3)"); + assertEval("1313", "=Format(#01/10/2003 7:13:00 AM#, 'nnnn; foo bar')"); + assertEval("1 1/10/2003 7:13:00 AM ttt this is text", + "=Format(#01/10/2003 7:13:00 AM#, 'q c ttt \"this is text\"')"); + assertEval("1 1/10/2003 ttt this is text", + "=Format(#01/10/2003#, 'q c ttt \"this is text\"')"); + assertEval("4 7:13:00 AM ttt this 'is' \"text\"", + "=Format(#7:13:00 AM#, \"q c ttt \"\"this 'is' \"\"\"\"text\"\"\"\"\"\"\")"); } public void testNumberFuncs() throws Exception -- cgit v1.2.3 From ded1c6e0a1ee8dc6f6e716af048386c55d4dd450 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Thu, 24 Jan 2019 22:32:18 +0000 Subject: implement remaining custom formatting, not tested git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1266 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/impl/expr/FormatUtil.java | 574 +++++++++++++++------ .../jackcess/impl/expr/NumberFormatter.java | 10 + .../jackcess/impl/expr/DefaultFunctionsTest.java | 20 + 3 files changed, 438 insertions(+), 166 deletions(-) (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java index 1cd7b3c..a6c52f0 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java @@ -163,6 +163,11 @@ public class FormatUtil private static final char TO_LOWER_CHAR = '<'; private static final char DT_LIT_COLON_CHAR = ':'; private static final char DT_LIT_SLASH_CHAR = '/'; + private static final char SINGLE_QUOTE_CHAR = '\''; + private static final char EXP_E_CHAR = 'E'; + private static final char EXP_e_CHAR = 'e'; + private static final char PLUS_CHAR = '+'; + private static final char MINUS_CHAR = '-'; private static final int NO_CHAR = -1; private static final byte FCT_UNKNOWN = 0; @@ -188,11 +193,12 @@ public class FormatUtil @FunctionalInterface interface DateFormatBuilder { - public void build(DateTimeFormatterBuilder dtfb, Args args); + public void build(DateTimeFormatterBuilder dtfb, Args args, + boolean hasAmPm, Value.Type dtType); } private static final DateFormatBuilder PARTIAL_PREFIX = - (dtfb, args) -> { + (dtfb, args, hasAmPm, dtType) -> { throw new UnsupportedOperationException(); }; @@ -200,9 +206,9 @@ public class FormatUtil new HashMap<>(); static { DATE_FMT_BUILDERS.put("c", - (dtfb, args) -> + (dtfb, args, hasAmPm, dtType) -> dtfb.append(ValueSupport.getDateFormatForType( - args._ctx, args._expr.getType()))); + args._ctx, dtType))); DATE_FMT_BUILDERS.put("d", new SimpleDFB("d")); DATE_FMT_BUILDERS.put("dd", new SimpleDFB("dd")); DATE_FMT_BUILDERS.put("ddd", new SimpleDFB("eee")); @@ -221,16 +227,16 @@ public class FormatUtil return weekFields.weekOfWeekBasedYear(); } }); - DATE_FMT_BUILDERS.put("m", new SimpleDFB("M")); - DATE_FMT_BUILDERS.put("mm", new SimpleDFB("MM")); - DATE_FMT_BUILDERS.put("mmm", new SimpleDFB("LLL")); - DATE_FMT_BUILDERS.put("mmmm", new SimpleDFB("LLLL")); + DATE_FMT_BUILDERS.put("m", new SimpleDFB("L")); + DATE_FMT_BUILDERS.put("mm", new SimpleDFB("LL")); + DATE_FMT_BUILDERS.put("mmm", new SimpleDFB("MMM")); + DATE_FMT_BUILDERS.put("mmmm", new SimpleDFB("MMMM")); DATE_FMT_BUILDERS.put("q", new SimpleDFB("Q")); DATE_FMT_BUILDERS.put("y", new SimpleDFB("D")); DATE_FMT_BUILDERS.put("yy", new SimpleDFB("yy")); DATE_FMT_BUILDERS.put("yyyy", new SimpleDFB("yyyy")); - DATE_FMT_BUILDERS.put("h", new SimpleDFB("H")); - DATE_FMT_BUILDERS.put("hh", new SimpleDFB("HH")); + DATE_FMT_BUILDERS.put("h", new HourlyDFB("h", "H")); + DATE_FMT_BUILDERS.put("hh", new HourlyDFB("hh", "HH")); DATE_FMT_BUILDERS.put("n", new SimpleDFB("m")); DATE_FMT_BUILDERS.put("nn", new SimpleDFB("mm")); DATE_FMT_BUILDERS.put("s", new SimpleDFB("s")); @@ -241,9 +247,9 @@ public class FormatUtil DATE_FMT_BUILDERS.put("A/P", new AmPmDFB("A", "P")); DATE_FMT_BUILDERS.put("a/p", new AmPmDFB("a", "p")); DATE_FMT_BUILDERS.put("AMPM", - (dtfb, args) -> { + (dtfb, args, hasAmPm, dtType) -> { String[] amPmStrs = args._ctx.getTemporalConfig().getAmPmStrings(); - new AmPmDFB(amPmStrs[0], amPmStrs[1]).build(dtfb, args); + new AmPmDFB(amPmStrs[0], amPmStrs[1]).build(dtfb, args, hasAmPm, dtType); } ); fillInPartialPrefixes(); @@ -255,6 +261,10 @@ public class FormatUtil private static final int NF_NULL_IDX = 3; private static final int NUM_NF_FMTS = 4; + private static final NumberFormatter.NotationType[] NO_EXP_TYPES = + new NumberFormatter.NotationType[NUM_NF_FMTS]; + + private static final class Args { private final EvalContext _ctx; @@ -269,6 +279,22 @@ public class FormatUtil _firstWeekType = firstWeekType; } + public boolean isNullOrEmptyString() { + return(_expr.isNull() || + // only a string value could ever be an empty string + (_expr.getType().isString() && getAsString().isEmpty())); + } + + public boolean maybeCoerceToEmptyString() { + if(isNullOrEmptyString()) { + // ensure that we have a non-null value when formatting (null acts + // like empty string) + _expr = ValueSupport.EMPTY_STR_VAL; + return true; + } + return false; + } + public Args coerceToDateTimeValue() { if(!_expr.getType().isTemporal()) { @@ -280,7 +306,8 @@ public class FormatUtil } // StringValue already handles most String -> Number -> Date/Time, so - // most other convertions work here + // most other convertions work here (and failures are thrown so that + // default handling kicks in) _expr = _expr.getAsDateTimeValue(_ctx); } return this; @@ -292,7 +319,6 @@ public class FormatUtil // format coerces "true"/"false" to boolean values Value boolExpr = maybeGetStringAsBooleanValue(); - if(boolExpr != null) { _expr = boolExpr; } else { @@ -307,6 +333,10 @@ public class FormatUtil _ctx, _expr); if(maybe != null) { _expr = ValueSupport.toValue(maybe.getAsDouble(_ctx)); + } else { + // string which can't be converted to number force failure + // here so default formatting will kick in + throw new EvalException("invalid number value"); } } } @@ -320,7 +350,7 @@ public class FormatUtil private Value maybeGetStringAsBooleanValue() { // format coerces "true"/"false" to boolean values - String val = _expr.getAsString(_ctx); + String val = getAsString(); if("true".equalsIgnoreCase(val)) { return ValueSupport.TRUE_VAL; } @@ -340,6 +370,15 @@ public class FormatUtil return _expr.getAsLocalDateTime(_ctx); } + public boolean getAsBoolean() { + // even though string values have a "boolean" value, for formatting, + // strings which don't convert to valid boolean/number/date are just + // returned as is. so we use coerceToNumberValue to force the exception + // to be thrown which results in the "default" formatting behavior. + coerceToNumberValue(); + return _expr.getAsBoolean(_ctx); + } + public String getAsString() { return _expr.getAsString(_ctx); } @@ -352,18 +391,21 @@ public class FormatUtil int firstDay, int firstWeekType) { try { - Args args = new Args(ctx, expr, firstDay, firstWeekType); Fmt predefFmt = PREDEF_FMTS.get(fmtStr); if(predefFmt != null) { - if(expr.isNull()) { + if(args.isNullOrEmptyString()) { // predefined formats return empty string for null return ValueSupport.EMPTY_STR_VAL; } return predefFmt.format(args); } + // TODO implement caching for custom formats? put into Bindings. use + // special "cache" prefix to know which caches to clear when evalconfig + // is altered (could also cache other Format* functions) + return parseCustomFormat(fmtStr, args).format(args); } catch(EvalException ee) { @@ -443,9 +485,6 @@ public class FormatUtil // a "general" format is actually a "yes/no" format which functions almost // exactly like a number format (without any number format specific chars) - if(!args._expr.isNull()) { - args.coerceToNumberValue(); - } StringBuilder sb = new StringBuilder(); String[] fmtStrs = new String[NUM_NF_FMTS]; @@ -503,54 +542,26 @@ public class FormatUtil addCustomGeneralFormat(fmtStrs, fmtIdx++, sb); } - return new CustomNumberFmt( - createCustomLiteralFormat(fmtStrs[NF_POS_IDX]), - createCustomLiteralFormat(fmtStrs[NF_NEG_IDX]), - createCustomLiteralFormat(fmtStrs[NF_ZERO_IDX]), - createCustomLiteralFormat(fmtStrs[NF_NULL_IDX])); + return new CustomGeneralFmt( + ValueSupport.toValue(fmtStrs[NF_POS_IDX]), + ValueSupport.toValue(fmtStrs[NF_NEG_IDX]), + ValueSupport.toValue(fmtStrs[NF_ZERO_IDX]), + ValueSupport.toValue(fmtStrs[NF_NULL_IDX])); } private static void addCustomGeneralFormat(String[] fmtStrs, int fmtIdx, StringBuilder sb) { - if(sb.length() == 0) { - // do special empty format handling on a per-format-type basis - switch(fmtIdx) { - case NF_NEG_IDX: - // re-use "pos" format with '-' prepended - sb.append('-').append(fmtStrs[NF_POS_IDX]); - break; - case NF_ZERO_IDX: - // re-use "pos" format - sb.append(fmtStrs[NF_POS_IDX]); - break; - default: - // use empty string result - } - } - - fmtStrs[fmtIdx] = sb.toString(); - sb.setLength(0); - } - - private static Fmt createCustomLiteralFormat(String str) { - Value literal = ValueSupport.toValue(str); - return args -> literal; + addCustomNumberFormat(fmtStrs, NO_EXP_TYPES, fmtIdx, sb); } private static Fmt parseCustomDateFormat(ExprBuf buf, Args args) { - // custom date formats don't have special null handling - if(args._expr.isNull()) { - return NULL_FMT; - } - - // force to temporal value before proceeding (this will throw if we don't - // have a date/time and therefore don't need to proceed with the rest of - // the format translation work) - args.coerceToDateTimeValue(); - - DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder(); + // keep track of some extra state while parsing the format, whether or not + // there was an am/pm pattern and whether or not there was a general + // date/time pattern + boolean[] fmtState = new boolean[]{false, false}; + List dfbs = new ArrayList<>(); BUF_LOOP: while(buf.hasNext()) { @@ -560,7 +571,9 @@ public class FormatUtil case FCT_GENERAL: switch(c) { case QUOTE_CHAR: - dtfb.appendLiteral(parseQuotedString(buf)); + String str = parseQuotedString(buf); + dfbs.add((dtfb, argsParam, hasAmPmParam, dtType) -> + dtfb.appendLiteral(str)); break; case START_COLOR_CHAR: // color strings seem to be ignored @@ -568,7 +581,7 @@ public class FormatUtil break; case ESCAPE_CHAR: if(buf.hasNext()) { - dtfb.appendLiteral(buf.next()); + dfbs.add(buildLiteralCharDFB(buf.next())); } break; case FILL_ESCAPE_CHAR: @@ -583,37 +596,142 @@ public class FormatUtil // respect the char. ignore everything after the first choice break BUF_LOOP; default: - dtfb.appendLiteral(c); + dfbs.add(buildLiteralCharDFB(c)); } break; case FCT_DATE: - parseCustomDateFormatPattern(c, buf, dtfb, args); + parseCustomDateFormatPattern(c, buf, dfbs, fmtState, args); break; default: - dtfb.appendLiteral(c); + dfbs.add(buildLiteralCharDFB(c)); } } - DateTimeFormatter dtf = dtfb.toFormatter( - args._ctx.getTemporalConfig().getLocale()); + boolean hasAmPm = fmtState[0]; + boolean hasGeneralFormat = fmtState[1]; + if(!hasGeneralFormat) { + // simple situation, one format for every value + DateTimeFormatter dtf = createDateTimeFormatter(dfbs, args, hasAmPm, null); + return new CustomFmt(argsParam -> ValueSupport.toValue( + dtf.format(argsParam.getAsLocalDateTime()))); + } + + // we need separate formatters for date, time, and date/time values + DateTimeFormatter dateFmt = createDateTimeFormatter(dfbs, args, hasAmPm, + Value.Type.DATE); + DateTimeFormatter timeFmt = createDateTimeFormatter(dfbs, args, hasAmPm, + Value.Type.TIME); + DateTimeFormatter dtFmt = createDateTimeFormatter(dfbs, args, hasAmPm, + Value.Type.DATE_TIME); - return argsParam -> ValueSupport.toValue( - dtf.format(argsParam.getAsLocalDateTime())); + return new CustomFmt(argsParam -> formatDateTime( + argsParam, dateFmt, timeFmt, dtFmt)); } - private static Fmt parseCustomNumberFormat(ExprBuf buf, Args args) { + private static void parseCustomDateFormatPattern( + char c, ExprBuf buf, List dfbs, + boolean[] fmtState, Args args) { + + if((c == DT_LIT_COLON_CHAR) || (c == DT_LIT_SLASH_CHAR)) { + // date/time literal char, nothing more to do + dfbs.add(buildLiteralCharDFB(c)); + return; + } + + StringBuilder sb = buf.getScratchBuffer(); + sb.append(c); + char firstChar = c; + int firstPos = buf.curPos(); + String bestMatchPat = sb.toString(); - // force to number value before proceeding (this will throw if we don't - // have a number and therefore don't need to proceed with the rest of - // the format translation work) - if(!args._expr.isNull()) { - args.coerceToNumberValue(); + DateFormatBuilder bestMatch = DATE_FMT_BUILDERS.get(bestMatchPat); + int bestPos = firstPos; + while(buf.hasNext()) { + sb.append(buf.next()); + String tmpPat = sb.toString(); + DateFormatBuilder dfb = DATE_FMT_BUILDERS.get(tmpPat); + if(dfb == null) { + // no more possible matches + break; + } + if(dfb != PARTIAL_PREFIX) { + // this is the longest, valid pattern we have seen so far + bestMatch = dfb; + bestPos = buf.curPos(); + bestMatchPat = tmpPat; + } + } + + if(bestMatch != PARTIAL_PREFIX) { + + // apply valid pattern + buf.reset(bestPos); + dfbs.add(bestMatch); + + switch(firstChar) { + case 'a': + case 'A': + // this was an am/pm pattern + fmtState[0] = true; + break; + case 'c': + // this was a general date/time format + fmtState[1] = true; + break; + default: + // don't care + } + + } else { + + // just consume the first char + buf.reset(firstPos); + dfbs.add(buildLiteralCharDFB(firstChar)); } + } + + private static DateFormatBuilder buildLiteralCharDFB(char c) { + return (dtfb, args, hasAmPm, dtType) -> dtfb.appendLiteral(c); + } + + private static DateTimeFormatter createDateTimeFormatter( + List dfbs, Args args, boolean hasAmPm, + Value.Type dtType) + { + DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder(); + dfbs.forEach(d -> d.build(dtfb, args, hasAmPm, dtType)); + return dtfb.toFormatter(args._ctx.getTemporalConfig().getLocale()); + } + + private static Value formatDateTime( + Args args, DateTimeFormatter dateFmt, + DateTimeFormatter timeFmt, DateTimeFormatter dtFmt) + { + LocalDateTime ldt = args.getAsLocalDateTime(); + DateTimeFormatter fmt = null; + switch(args._expr.getType()) { + case DATE: + fmt = dateFmt; + break; + case TIME: + fmt = timeFmt; + break; + default: + fmt = dtFmt; + } + + return ValueSupport.toValue(fmt.format(ldt)); + } + + private static Fmt parseCustomNumberFormat(ExprBuf buf, Args args) { StringBuilder sb = new StringBuilder(); String[] fmtStrs = new String[NUM_NF_FMTS]; int fmtIdx = 0; + StringBuilder pendingLiteral = new StringBuilder(); + NumberFormatter.NotationType[] expTypes = + new NumberFormatter.NotationType[NUM_NF_FMTS]; BUF_LOOP: while(buf.hasNext()) { @@ -626,7 +744,7 @@ public class FormatUtil // no effect break; case QUOTE_CHAR: - parseQuotedString(buf, sb); + parseQuotedString(buf, pendingLiteral); break; case START_COLOR_CHAR: // color strings seem to be ignored @@ -634,7 +752,7 @@ public class FormatUtil break; case ESCAPE_CHAR: if(buf.hasNext()) { - sb.append(buf.next()); + pendingLiteral.append(buf.next()); } break; case FILL_ESCAPE_CHAR: @@ -645,30 +763,145 @@ public class FormatUtil } break; case CHOICE_SEP_CHAR: - // yes/no (number) format supports up to 4 formats: pos, neg, zero, - // null. after that, ignore the rest + // number format supports up to 4 formats: pos, neg, zero, null. + // after that, ignore the rest if(fmtIdx == (NUM_NF_FMTS - 1)) { // last possible format, ignore remaining break BUF_LOOP; } - addCustomGeneralFormat(fmtStrs, fmtIdx++, sb); + flushPendingNumberLiteral(pendingLiteral, sb); + addCustomNumberFormat(fmtStrs, expTypes, fmtIdx++, sb); + break; + default: + pendingLiteral.append(c); + } + break; + case FCT_NUMBER: + switch(c) { + case EXP_E_CHAR: + int signChar = buf.peekNext(); + if((signChar == PLUS_CHAR) || (signChar == MINUS_CHAR)) { + buf.next(); + expTypes[fmtIdx] = ((signChar == PLUS_CHAR) ? + NumberFormatter.NotationType.EXP_E_PLUS : + NumberFormatter.NotationType.EXP_E_MINUS); + flushPendingNumberLiteral(pendingLiteral, sb); + sb.append(EXP_E_CHAR); + } else { + pendingLiteral.append(c); + } + break; + case EXP_e_CHAR: + signChar = buf.peekNext(); + if((signChar == PLUS_CHAR) || (signChar == MINUS_CHAR)) { + buf.next(); + expTypes[fmtIdx] = ((signChar == PLUS_CHAR) ? + NumberFormatter.NotationType.EXP_e_PLUS : + NumberFormatter.NotationType.EXP_e_MINUS); + flushPendingNumberLiteral(pendingLiteral, sb); + sb.append(EXP_E_CHAR); + } else { + pendingLiteral.append(c); + } break; default: + // most number format chars pass straight through + flushPendingNumberLiteral(pendingLiteral, sb); sb.append(c); } break; default: - sb.append(c); + pendingLiteral.append(c); } } // fill in remaining formats while(fmtIdx < NUM_NF_FMTS) { - addCustomGeneralFormat(fmtStrs, fmtIdx++, sb); + flushPendingNumberLiteral(pendingLiteral, sb); + addCustomNumberFormat(fmtStrs, expTypes, fmtIdx++, sb); + } + + return new CustomNumberFmt( + createCustomNumberFormat(fmtStrs, expTypes, NF_POS_IDX, args), + createCustomNumberFormat(fmtStrs, expTypes, NF_NEG_IDX, args), + createCustomNumberFormat(fmtStrs, expTypes, NF_ZERO_IDX, args), + createCustomNumberFormat(fmtStrs, expTypes, NF_NULL_IDX, args)); + } + + private static void addCustomNumberFormat( + String[] fmtStrs, NumberFormatter.NotationType[] expTypes, + int fmtIdx, StringBuilder sb) + { + if(sb.length() == 0) { + // do special empty format handling on a per-format-type basis + switch(fmtIdx) { + case NF_NEG_IDX: + // re-use "pos" format + sb.append('-').append(fmtStrs[NF_POS_IDX]); + expTypes[NF_NEG_IDX] = expTypes[NF_POS_IDX]; + break; + case NF_ZERO_IDX: + // re-use "pos" format + sb.append(fmtStrs[NF_POS_IDX]); + expTypes[NF_ZERO_IDX] = expTypes[NF_POS_IDX]; + break; + default: + // use empty string result + } } - // FIXME writeme - throw new UnsupportedOperationException(); + fmtStrs[fmtIdx] = sb.toString(); + sb.setLength(0); + } + + private static void flushPendingNumberLiteral( + StringBuilder pendingLiteral, StringBuilder sb) { + if(pendingLiteral.length() == 0) { + return; + } + + if((pendingLiteral.length() == 1) && + pendingLiteral.charAt(0) == SINGLE_QUOTE_CHAR) { + // handle standalone single quote + sb.append(SINGLE_QUOTE_CHAR).append(SINGLE_QUOTE_CHAR); + pendingLiteral.setLength(0); + return; + } + + sb.append(SINGLE_QUOTE_CHAR); + int startPos = sb.length(); + sb.append(pendingLiteral); + + // we need to quote any single quotes in the literal string + for(int i = startPos; i < sb.length(); ++i) { + char c = sb.charAt(i); + if(c == SINGLE_QUOTE_CHAR) { + sb.insert(++i, SINGLE_QUOTE_CHAR); + } + } + + sb.append(SINGLE_QUOTE_CHAR); + pendingLiteral.setLength(0); + } + + private static NumberFormat createCustomNumberFormat( + String[] fmtStrs, NumberFormatter.NotationType[] expTypes, + int fmtIdx, Args args) { + + String fmtStr = fmtStrs[fmtIdx]; + NumberFormatter.NotationType expType = expTypes[fmtIdx]; + + if(fmtIdx == NF_NEG_IDX) { + // force explicit negative format handling + fmtStr = fmtStr + ";" + fmtStr; + } + + NumberFormat df = args._ctx.createDecimalFormat(fmtStr); + if(expType != null) { + df = new NumberFormatter.ScientificFormat(df, expType); + } + + return df; } private static Fmt parseCustomTextFormat(ExprBuf buf, Args args) { @@ -786,7 +1019,7 @@ public class FormatUtil NULL_FMT); } - return new CustomTextFmt(fmt, emptyFmt); + return new CustomFmt(fmt, emptyFmt); } private static void flushPendingTextLiteral( @@ -801,48 +1034,6 @@ public class FormatUtil subFmts.add((sb, cs) -> sb.append(literal)); } - private static void parseCustomDateFormatPattern( - char c, ExprBuf buf, DateTimeFormatterBuilder dtfb, Args args) { - - if((c == DT_LIT_COLON_CHAR) || (c == DT_LIT_SLASH_CHAR)) { - // date/time literal char, nothing more to do - dtfb.appendLiteral(c); - return; - } - - StringBuilder sb = buf.getScratchBuffer(); - sb.append(c); - - char firstChar = c; - int firstPos = buf.curPos(); - - DateFormatBuilder bestMatch = DATE_FMT_BUILDERS.get(sb.toString()); - int bestPos = firstPos; - while(buf.hasNext()) { - sb.append(buf.next()); - DateFormatBuilder dfb = DATE_FMT_BUILDERS.get(sb.toString()); - if(dfb == null) { - // no more possible matches - break; - } - if(dfb != PARTIAL_PREFIX) { - // this is the longest, valid pattern we have seen so far - bestMatch = dfb; - bestPos = buf.curPos(); - } - } - - if(bestMatch != PARTIAL_PREFIX) { - // apply valid pattern - buf.reset(bestPos); - bestMatch.build(dtfb, args); - } else { - // just consume the first char - buf.reset(firstPos); - dtfb.appendLiteral(firstChar); - } - } - public static String createNumberFormatPattern( NumPatternType numPatType, int numDecDigits, boolean incLeadDigit, boolean negParens, int numGroupDigits) { @@ -940,7 +1131,7 @@ public class FormatUtil @Override public Value format(Args args) { - return(args._expr.getAsBoolean(args._ctx) ? _trueVal : _falseVal); + return(args.getAsBoolean() ? _trueVal : _falseVal); } } @@ -982,30 +1173,36 @@ public class FormatUtil } } - private static final class NumberFmt extends BaseNumberFmt + private static final class SimpleDFB implements DateFormatBuilder { - private final NumberFormat _df; + private final String _pat; - private NumberFmt(NumberFormat df) { - _df = df; + private SimpleDFB(String pat) { + _pat = pat; } - @Override - protected NumberFormat getNumberFormat(Args args) { - return _df; + public void build(DateTimeFormatterBuilder dtfb, Args args, + boolean hasAmPm, Value.Type dtType) { + dtfb.appendPattern(_pat); } } - private static final class SimpleDFB implements DateFormatBuilder + private static final class HourlyDFB implements DateFormatBuilder { - private final String _pat; + private final String _pat12; + private final String _pat24; - private SimpleDFB(String pat) { - _pat = pat; + private HourlyDFB(String pat12, String pat24) { + _pat12 = pat12; + _pat24 = pat24; } @Override - public void build(DateTimeFormatterBuilder dtfb, Args args) { - dtfb.appendPattern(_pat); + public void build(DateTimeFormatterBuilder dtfb, Args args, + boolean hasAmPm, Value.Type dtTypePm) { + // annoyingly the "hour" patterns are the same and depend on the + // existence of the am/pm pattern to determine how they function (12 vs + // 24 hour). + dtfb.appendPattern(hasAmPm ? _pat12 : _pat24); } } @@ -1017,7 +1214,8 @@ public class FormatUtil _type = type; } @Override - public void build(DateTimeFormatterBuilder dtfb, Args args) { + public void build(DateTimeFormatterBuilder dtfb, Args args, + boolean hasAmPm, Value.Type dtType) { dtfb.appendPattern(args._ctx.getTemporalConfig().getDateTimeFormat(_type)); } } @@ -1025,7 +1223,8 @@ public class FormatUtil private static abstract class WeekBasedDFB implements DateFormatBuilder { @Override - public void build(DateTimeFormatterBuilder dtfb, Args args) { + public void build(DateTimeFormatterBuilder dtfb, Args args, + boolean hasAmPm, Value.Type dtType) { dtfb.appendValue(getField(DefaultDateFunctions.weekFields( args._firstDay, args._firstWeekType))); } @@ -1045,7 +1244,8 @@ public class FormatUtil _pm = pm; } @Override - public void build(DateTimeFormatterBuilder dtfb, Args args) { + public void build(DateTimeFormatterBuilder dtfb, Args args, + boolean hasAmPm, Value.Type dtType) { dtfb.appendText(ChronoField.AMPM_OF_DAY, this); } @Override @@ -1074,29 +1274,25 @@ public class FormatUtil } } - private static final class CustomTextFmt implements Fmt + private static final class CustomFmt implements Fmt { private final Fmt _fmt; private final Fmt _emptyFmt; - private CustomTextFmt(Fmt fmt, Fmt emptyFmt) { - _fmt = fmt; - _emptyFmt = emptyFmt; + private CustomFmt(Fmt fmt) { + this(fmt, NULL_FMT); } - private static boolean isEmptyString(Args args) { - // only a string value could ever be an empty string - return (args._expr.getType().isString() && args.getAsString().isEmpty()); + private CustomFmt(Fmt fmt, Fmt emptyFmt) { + _fmt = fmt; + _emptyFmt = emptyFmt; } @Override public Value format(Args args) { Fmt fmt = _fmt; - if(args._expr.isNull() || isEmptyString(args)) { + if(args.maybeCoerceToEmptyString()) { fmt = _emptyFmt; - // ensure that we have a non-null value when formatting (null acts - // like empty string in this case) - args._expr = ValueSupport.EMPTY_STR_VAL; } return fmt.format(args); } @@ -1110,7 +1306,8 @@ public class FormatUtil private final TextCase _textCase; private CharSourceFmt(List> subFmts, - int numPlaceholders, boolean rightAligned, TextCase textCase) { + int numPlaceholders, boolean rightAligned, + TextCase textCase) { _subFmts = subFmts; _numPlaceholders = numPlaceholders; _rightAligned = rightAligned; @@ -1119,8 +1316,8 @@ public class FormatUtil @Override public Value format(Args args) { - CharSource cs = new CharSource(args.getAsString(), _numPlaceholders, _rightAligned, - _textCase); + CharSource cs = new CharSource(args.getAsString(), _numPlaceholders, + _rightAligned, _textCase); StringBuilder sb = new StringBuilder(); _subFmts.stream().forEach(fmt -> fmt.accept(sb, cs)); cs.appendRemaining(sb); @@ -1174,12 +1371,13 @@ public class FormatUtil private static final class CustomNumberFmt implements Fmt { - private final Fmt _posFmt; - private final Fmt _negFmt; - private final Fmt _zeroFmt; - private final Fmt _nullFmt; + private final NumberFormat _posFmt; + private final NumberFormat _negFmt; + private final NumberFormat _zeroFmt; + private final NumberFormat _nullFmt; - private CustomNumberFmt(Fmt posFmt, Fmt negFmt, Fmt zeroFmt, Fmt nullFmt) { + private CustomNumberFmt(NumberFormat posFmt, NumberFormat negFmt, + NumberFormat zeroFmt, NumberFormat nullFmt) { _posFmt = posFmt; _negFmt = negFmt; _zeroFmt = zeroFmt; @@ -1189,13 +1387,57 @@ public class FormatUtil @Override public Value format(Args args) { if(args._expr.isNull()) { - return _nullFmt.format(args); + return ValueSupport.toValue(_nullFmt.format(BigDecimal.ZERO)); } BigDecimal bd = args.getAsBigDecimal(); int cmp = BigDecimal.ZERO.compareTo(bd); - Fmt fmt = ((cmp < 0) ? _posFmt : - ((cmp > 0) ? _negFmt : _zeroFmt)); - return fmt.format(args); + + NumberFormat fmt = null; + if(cmp > 0) { + // in theory we want to use the negative format. however, if, due to + // rounding, we end up with a number equivalent to zero, then we fall + // back to the zero format + fmt = _negFmt; + int maxDecDigits = fmt.getMaximumFractionDigits(); + bd = bd.negate().setScale(maxDecDigits, NumberFormatter.ROUND_MODE); + if(BigDecimal.ZERO.equals(bd)) { + // fall back to zero format + fmt = _zeroFmt; + } + } else { + // positive or zero number + fmt = ((cmp < 0) ? _posFmt : _zeroFmt); + } + + return ValueSupport.toValue(fmt.format(bd)); + } + } + + private static final class CustomGeneralFmt implements Fmt + { + private final Value _posVal; + private final Value _negVal; + private final Value _zeroVal; + private final Value _nullVal; + + private CustomGeneralFmt(Value posVal, Value negVal, + Value zeroVal, Value nullVal) { + _posVal = posVal; + _negVal = negVal; + _zeroVal = zeroVal; + _nullVal = nullVal; + } + + @Override + public Value format(Args args) { + if(args._expr.isNull()) { + return _nullVal; + } + BigDecimal bd = args.getAsBigDecimal(); + int cmp = BigDecimal.ZERO.compareTo(bd); + + return ((cmp < 0) ? _posVal : + ((cmp > 0) ? _negVal : _zeroVal)); } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatter.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatter.java index ce251c2..57a5e35 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatter.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatter.java @@ -222,5 +222,15 @@ public class NumberFormatter FieldPosition pos) { throw new UnsupportedOperationException(); } + + @Override + public int getMaximumFractionDigits() { + return _df.getMaximumFractionDigits(); + } + + @Override + public int getMinimumFractionDigits() { + return _df.getMinimumFractionDigits(); + } } } diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java index 49e7b8d..b9a764d 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java @@ -325,7 +325,10 @@ public class DefaultFunctionsTest extends TestCase assertEval("07:00 AM", "=Format(#01/02/2003 7:00:00 AM#, 'Medium Time')"); assertEval("07:00", "=Format(#01/02/2003 7:00:00 AM#, 'Short Time')"); assertEval("19:00", "=Format(#01/02/2003 7:00:00 PM#, 'Short Time')"); + } + public void testCustomFormat() throws Exception + { assertEval("07:00 a", "=Format(#01/10/2003 7:00:00 AM#, 'hh:nn a/p')"); assertEval("07:00 a 6 2", "=Format(#01/10/2003 7:00:00 AM#, 'hh:nn a/p w ww')"); assertEval("07:00 a 4 1", "=Format(#01/10/2003 7:00:00 AM#, 'hh:nn a/p w ww', 3, 3)"); @@ -336,6 +339,23 @@ public class DefaultFunctionsTest extends TestCase "=Format(#01/10/2003#, 'q c ttt \"this is text\"')"); assertEval("4 7:13:00 AM ttt this 'is' \"text\"", "=Format(#7:13:00 AM#, \"q c ttt \"\"this 'is' \"\"\"\"text\"\"\"\"\"\"\")"); + assertEval("12/29/1899", "=Format('true', 'c')"); + assertEval("Tuesday, 00 Jan 2, 21:36:00", + "=Format('3.9', 'dddd, yy mmm d, hh:nn:ss')"); + assertEval("Tuesday, 00 Jan 01 2, 09:36:00 PM", + "=Format('3.9', 'dddd, yy mmm mm d, hh:nn:ss AMPM')"); + assertEval("foo", + "=Format('foo', 'dddd, yy mmm mm d, hh:nn:ss AMPM')"); + + assertEval("p13.00blah", + "=Format('13', '\"p\"#.00#\"blah\"')"); + assertEval("-p13.00blah", + "=Format('-13', '\"p\"#.00#\"blah\";(\"p\"#.00#\"blah\")')"); + // assertEval("-p13.00blah", + // "=Format('-13', '\"p\"#.00#\"blah\"')"); + + + } public void testNumberFuncs() throws Exception -- cgit v1.2.3 From f3c8bb34a4aad19a5879d193dc54bfe862bd94b1 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Tue, 29 Jan 2019 03:32:55 +0000 Subject: add unit tests, fix bugs in custom formats git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1269 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/impl/expr/FormatUtil.java | 193 ++++++++++++++------ .../jackcess/impl/expr/DefaultFunctionsTest.java | 194 +++++++++++++++++++-- 2 files changed, 326 insertions(+), 61 deletions(-) (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java index a6c52f0..2258591 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java @@ -17,7 +17,10 @@ limitations under the License. package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.FieldPosition; import java.text.NumberFormat; +import java.text.ParsePosition; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; @@ -263,6 +266,7 @@ public class FormatUtil private static final NumberFormatter.NotationType[] NO_EXP_TYPES = new NumberFormatter.NotationType[NUM_NF_FMTS]; + private static final boolean[] NO_FMT_TYPES = new boolean[NUM_NF_FMTS]; private static final class Args @@ -552,7 +556,7 @@ public class FormatUtil private static void addCustomGeneralFormat(String[] fmtStrs, int fmtIdx, StringBuilder sb) { - addCustomNumberFormat(fmtStrs, NO_EXP_TYPES, fmtIdx, sb); + addCustomNumberFormat(fmtStrs, NO_EXP_TYPES, NO_FMT_TYPES, fmtIdx, sb); } private static Fmt parseCustomDateFormat(ExprBuf buf, Args args) { @@ -732,6 +736,7 @@ public class FormatUtil StringBuilder pendingLiteral = new StringBuilder(); NumberFormatter.NotationType[] expTypes = new NumberFormatter.NotationType[NUM_NF_FMTS]; + boolean[] hasFmts = new boolean[NUM_NF_FMTS]; BUF_LOOP: while(buf.hasNext()) { @@ -770,13 +775,14 @@ public class FormatUtil break BUF_LOOP; } flushPendingNumberLiteral(pendingLiteral, sb); - addCustomNumberFormat(fmtStrs, expTypes, fmtIdx++, sb); + addCustomNumberFormat(fmtStrs, expTypes, hasFmts, fmtIdx++, sb); break; default: pendingLiteral.append(c); } break; case FCT_NUMBER: + hasFmts[fmtIdx] = true; switch(c) { case EXP_E_CHAR: int signChar = buf.peekNext(); @@ -818,19 +824,19 @@ public class FormatUtil // fill in remaining formats while(fmtIdx < NUM_NF_FMTS) { flushPendingNumberLiteral(pendingLiteral, sb); - addCustomNumberFormat(fmtStrs, expTypes, fmtIdx++, sb); + addCustomNumberFormat(fmtStrs, expTypes, hasFmts, fmtIdx++, sb); } return new CustomNumberFmt( - createCustomNumberFormat(fmtStrs, expTypes, NF_POS_IDX, args), - createCustomNumberFormat(fmtStrs, expTypes, NF_NEG_IDX, args), - createCustomNumberFormat(fmtStrs, expTypes, NF_ZERO_IDX, args), - createCustomNumberFormat(fmtStrs, expTypes, NF_NULL_IDX, args)); + createCustomNumberFormat(fmtStrs, expTypes, hasFmts, NF_POS_IDX, args), + createCustomNumberFormat(fmtStrs, expTypes, hasFmts, NF_NEG_IDX, args), + createCustomNumberFormat(fmtStrs, expTypes, hasFmts, NF_ZERO_IDX, args), + createCustomNumberFormat(fmtStrs, expTypes, hasFmts, NF_NULL_IDX, args)); } private static void addCustomNumberFormat( String[] fmtStrs, NumberFormatter.NotationType[] expTypes, - int fmtIdx, StringBuilder sb) + boolean[] hasFmts, int fmtIdx, StringBuilder sb) { if(sb.length() == 0) { // do special empty format handling on a per-format-type basis @@ -839,11 +845,13 @@ public class FormatUtil // re-use "pos" format sb.append('-').append(fmtStrs[NF_POS_IDX]); expTypes[NF_NEG_IDX] = expTypes[NF_POS_IDX]; + hasFmts[NF_NEG_IDX] = hasFmts[NF_POS_IDX]; break; case NF_ZERO_IDX: // re-use "pos" format sb.append(fmtStrs[NF_POS_IDX]); expTypes[NF_ZERO_IDX] = expTypes[NF_POS_IDX]; + hasFmts[NF_ZERO_IDX] = hasFmts[NF_POS_IDX]; break; default: // use empty string result @@ -886,22 +894,43 @@ public class FormatUtil private static NumberFormat createCustomNumberFormat( String[] fmtStrs, NumberFormatter.NotationType[] expTypes, - int fmtIdx, Args args) { + boolean[] hasFmts, int fmtIdx, Args args) { String fmtStr = fmtStrs[fmtIdx]; + if(!hasFmts[fmtIdx]) { + // convert the literal string to a dummy number format + if(fmtStr.length() > 0) { + // strip quoting + StringBuilder sb = new StringBuilder(fmtStr) + .deleteCharAt(fmtStr.length() - 1) + .deleteCharAt(0); + if(sb.length() > 0) { + for(int i = 0; i < sb.length(); ++i) { + if(sb.charAt(i) == SINGLE_QUOTE_CHAR) { + // delete next single quote char + sb.deleteCharAt(++i); + } + } + } + fmtStr = sb.toString(); + } + return new LiteralNumberFormat(fmtStr); + } + NumberFormatter.NotationType expType = expTypes[fmtIdx]; + NumberFormat nf = args._ctx.createDecimalFormat(fmtStr); - if(fmtIdx == NF_NEG_IDX) { - // force explicit negative format handling - fmtStr = fmtStr + ";" + fmtStr; + DecimalFormat df = (DecimalFormat)nf; + if(df.getMaximumFractionDigits() > 0) { + // if the decimal is included in the format, access always shows it + df.setDecimalSeparatorAlwaysShown(true); } - NumberFormat df = args._ctx.createDecimalFormat(fmtStr); if(expType != null) { - df = new NumberFormatter.ScientificFormat(df, expType); + nf = new NumberFormatter.ScientificFormat(nf, expType); } - return df; + return nf; } private static Fmt parseCustomTextFormat(ExprBuf buf, Args args) { @@ -1088,7 +1117,7 @@ public class FormatUtil private static String parseColorString(ExprBuf buf) { return ExpressionTokenizer.parseStringUntil( - buf, END_COLOR_CHAR, START_COLOR_CHAR, false); + buf, START_COLOR_CHAR, END_COLOR_CHAR, false); } private static void fillInPartialPrefixes() { @@ -1369,7 +1398,29 @@ public class FormatUtil } } - private static final class CustomNumberFmt implements Fmt + private static abstract class BaseCustomNumberFmt implements Fmt + { + @Override + public Value format(Args args) { + if(args._expr.isNull()) { + return formatNull(args); + } + + BigDecimal bd = args.getAsBigDecimal(); + int cmp = BigDecimal.ZERO.compareTo(bd); + + return ((cmp < 0) ? formatPos(bd, args) : + ((cmp > 0) ? formatNeg(bd, args) : + formatZero(bd, args))); + } + + protected abstract Value formatNull(Args args); + protected abstract Value formatPos(BigDecimal bd, Args args); + protected abstract Value formatNeg(BigDecimal bd, Args args); + protected abstract Value formatZero(BigDecimal bd, Args args); + } + + private static final class CustomNumberFmt extends BaseCustomNumberFmt { private final NumberFormat _posFmt; private final NumberFormat _negFmt; @@ -1384,36 +1435,41 @@ public class FormatUtil _nullFmt = nullFmt; } - @Override - public Value format(Args args) { - if(args._expr.isNull()) { - return ValueSupport.toValue(_nullFmt.format(BigDecimal.ZERO)); + private Value formatMaybeZero(BigDecimal bd, NumberFormat fmt) { + // in theory we want to use the given format. however, if, due to + // rounding, we end up with a number equivalent to zero, then we fall + // back to the zero format + int maxDecDigits = fmt.getMaximumFractionDigits(); + if(maxDecDigits < bd.scale()) { + bd = bd.setScale(maxDecDigits, NumberFormatter.ROUND_MODE); } - BigDecimal bd = args.getAsBigDecimal(); - int cmp = BigDecimal.ZERO.compareTo(bd); - - NumberFormat fmt = null; - if(cmp > 0) { - // in theory we want to use the negative format. however, if, due to - // rounding, we end up with a number equivalent to zero, then we fall - // back to the zero format - fmt = _negFmt; - int maxDecDigits = fmt.getMaximumFractionDigits(); - bd = bd.negate().setScale(maxDecDigits, NumberFormatter.ROUND_MODE); - if(BigDecimal.ZERO.equals(bd)) { - // fall back to zero format - fmt = _zeroFmt; - } - } else { - // positive or zero number - fmt = ((cmp < 0) ? _posFmt : _zeroFmt); + if(BigDecimal.ZERO.compareTo(bd) == 0) { + // fall back to zero format + fmt = _zeroFmt; } return ValueSupport.toValue(fmt.format(bd)); } + + @Override + protected Value formatNull(Args args) { + return ValueSupport.toValue(_nullFmt.format(BigDecimal.ZERO)); + } + @Override + protected Value formatPos(BigDecimal bd, Args args) { + return formatMaybeZero(bd, _posFmt); + } + @Override + protected Value formatNeg(BigDecimal bd, Args args) { + return formatMaybeZero(bd.negate(), _negFmt); + } + @Override + protected Value formatZero(BigDecimal bd, Args args) { + return ValueSupport.toValue(_zeroFmt.format(bd)); + } } - private static final class CustomGeneralFmt implements Fmt + private static final class CustomGeneralFmt extends BaseCustomNumberFmt { private final Value _posVal; private final Value _negVal; @@ -1429,17 +1485,56 @@ public class FormatUtil } @Override - public Value format(Args args) { - if(args._expr.isNull()) { - return _nullVal; - } - BigDecimal bd = args.getAsBigDecimal(); - int cmp = BigDecimal.ZERO.compareTo(bd); - - return ((cmp < 0) ? _posVal : - ((cmp > 0) ? _negVal : _zeroVal)); + protected Value formatNull(Args args) { + return _nullVal; + } + @Override + protected Value formatPos(BigDecimal bd, Args args) { + return _posVal; + } + @Override + protected Value formatNeg(BigDecimal bd, Args args) { + return _negVal; + } + @Override + protected Value formatZero(BigDecimal bd, Args args) { + return _zeroVal; } } + private static final class LiteralNumberFormat extends NumberFormat + { + private static final long serialVersionUID = 0L; + + private final String _str; + + private LiteralNumberFormat(String str) { + _str = str; + } + + @Override + public StringBuffer format(Object number, StringBuffer toAppendTo, + FieldPosition pos) + { + return toAppendTo.append(_str); + } + + @Override + public StringBuffer format(double number, StringBuffer toAppendTo, + FieldPosition pos) { + throw new UnsupportedOperationException(); + } + + @Override + public Number parse(String source, ParsePosition parsePosition) { + throw new UnsupportedOperationException(); + } + + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, + FieldPosition pos) { + throw new UnsupportedOperationException(); + } + } } diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java index b9a764d..24f3c5b 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java @@ -21,6 +21,7 @@ import java.time.LocalDateTime; import java.util.Calendar; import com.healthmarketscience.jackcess.expr.EvalException; +import junit.framework.AssertionFailedError; import junit.framework.TestCase; import static com.healthmarketscience.jackcess.impl.expr.ExpressionatorTest.eval; import static com.healthmarketscience.jackcess.impl.expr.ExpressionatorTest.toBD; @@ -251,6 +252,7 @@ public class DefaultFunctionsTest extends TestCase assertEval("-.123%", "=FormatPercent(-0.0012345,3,False)"); assertEval("$12,345.00", "=FormatCurrency(12345)"); + assertEval("($12,345.00)", "=FormatCurrency(-12345)"); assertEval("-$12.34", "=FormatCurrency(-12.345,-1,True,False)"); assertEval("$12", "=FormatCurrency(12.345,0,True,True)"); assertEval("($.123)", "=FormatCurrency(-0.12345,3,False)"); @@ -317,6 +319,7 @@ public class DefaultFunctionsTest extends TestCase assertEval("7:00:00 AM", "=Format(#7:00:00 AM#, 'General Date')"); assertEval("1/2/2003 7:00:00 AM", "=Format('37623.2916666667', 'General Date')"); assertEval("foo", "=Format('foo', 'General Date')"); + assertEval("", "=Format('', 'General Date')"); assertEval("Thursday, January 02, 2003", "=Format(#01/02/2003 7:00:00 AM#, 'Long Date')"); assertEval("02-Jan-03", "=Format(#01/02/2003 7:00:00 AM#, 'Medium Date')"); @@ -340,22 +343,189 @@ public class DefaultFunctionsTest extends TestCase assertEval("4 7:13:00 AM ttt this 'is' \"text\"", "=Format(#7:13:00 AM#, \"q c ttt \"\"this 'is' \"\"\"\"text\"\"\"\"\"\"\")"); assertEval("12/29/1899", "=Format('true', 'c')"); - assertEval("Tuesday, 00 Jan 2, 21:36:00", - "=Format('3.9', 'dddd, yy mmm d, hh:nn:ss')"); - assertEval("Tuesday, 00 Jan 01 2, 09:36:00 PM", - "=Format('3.9', 'dddd, yy mmm mm d, hh:nn:ss AMPM')"); + assertEval("Tuesday, 00 Jan 2, 21:36:00 Y", + "=Format('3.9', '*~dddd, yy mmm d, hh:nn:ss \\Y[Yellow]')"); + assertEval("Tuesday, 00 Jan 01/2, 09:36:00 PM", + "=Format('3.9', 'dddd, yy mmm mm/d, hh:nn:ss AMPM')"); assertEval("foo", "=Format('foo', 'dddd, yy mmm mm d, hh:nn:ss AMPM')"); - assertEval("p13.00blah", - "=Format('13', '\"p\"#.00#\"blah\"')"); - assertEval("-p13.00blah", - "=Format('-13', '\"p\"#.00#\"blah\";(\"p\"#.00#\"blah\")')"); - // assertEval("-p13.00blah", - // "=Format('-13', '\"p\"#.00#\"blah\"')"); - - + assertEvalFormat("';\\y;\\n'", + "foo", "'foo'", + "", "''", + "y", "True", + "n", "'0'", + "", "Null"); + + assertEvalFormat("';\"y\";!\\n;*~\\z[Blue];'", + "foo", "'foo'", + "", "''", + "y", "True", + "n", "'0'", + "z", "Null"); + + assertEvalFormat("'\"p\"#.00#\"blah\"'", + "p13.00blah", "13", + "-p13.00blah", "-13", + "p.00blah", "0", + "", "''", + "", "Null"); + + assertEvalFormat("'\"p\"#.00#\"blah\";(\"p\"#.00#\"blah\")'", + "p13.00blah", "13", + "(p13.00blah)", "-13", + "p.00blah", "0", + "(p1.00blah)", "True", + "p.00blah", "'false'", + "p37623.292blah", "#01/02/2003 7:00:00 AM#", + "p37623.292blah", "'01/02/2003 7:00:00 AM'", + "NotANumber", "'NotANumber'", + "", "''", + "", "Null"); + + assertEvalFormat("'\"p\"#.00#\"blah\";!(\"p\"#.00#\"blah\")[Red];\"zero\"'", + "p13.00blah", "13", + "(p13.00blah)", "-13", + "zero", "0", + "", "''", + "", "Null"); + + assertEvalFormat("'\"p\"#.00#\"blah\";(\"p\"#.00#\"blah\");\"zero\";\"yuck\"'", + "p13.00blah", "13", + "(p13.00blah)", "-13", + "zero", "0", + "", "''", + "yuck", "Null"); + + assertEvalFormat("'0.##;(0.###);\"zero\";\"yuck\"'", + "0.03", "0.03", + "zero", "0.003", + "(0.003)", "-0.003", + "zero", "-0.0003"); + + // FIXME, need to handle rounding w/ negatives + // FIXME, need to handle dangling decimal + // assertEvalFormat("'0.##;(0.###E+0)'", + // "0.03", "0.03", + // "(0.003)", "-0.0003", + + assertEvalFormat("'0.'", + "13.", "13", + "0.", "0.003", + "-45.", "-45", + "0.", "-0.003", + "0.", "0" + ); + + assertEvalFormat("'0.#'", + "13.", "13", + "0.3", "0.3", + "0.", "0.003", + "-45.", "-45", + "0.", "-0.003", + "0.", "0" + ); + + assertEvalFormat("'0'", + "13", "13", + "0", "0.003", + "-45", "-45", + "0", "-0.003", + "0", "0" + ); + + assertEvalFormat("'#'", + "13", "13", + "0", "0.003", + "-45", "-45", + "0", "-0.003" + // FIXME + // "", "0" + ); + + assertEvalFormat("'$0.0#'", + "$213.0", "213"); + + assertEvalFormat("'@'", + "foo", "'foo'", + "-13", "-13", + "0", "0", + "", "''", + "", "Null"); + + assertEvalFormat("'>@'", + "FOO", "'foo'", + "-13", "-13", + "0", "0", + "", "''", + "", "Null"); + + assertEvalFormat("'<@'", + "foo", "'FOO'", + "-13", "-13", + "0", "0", + "", "''", + "", "Null"); + + assertEvalFormat("'!>@'", + "O", "'foo'", + "3", "-13", + "0", "0", + "", "''", + "", "Null"); + + assertEvalFormat("'!>@[Red];\"empty\"'", + "O", "'foo'", + "3", "-13", + "0", "0", + "empty", "''", + "empty", "Null"); + + assertEvalFormat("'><@'", + "fOo", "'fOo'"); + + assertEvalFormat("'\\x@@@&&&\\y'", + "x fy", "'f'", + "x fooy", "'foo'", + "x foobay", "'fooba'", + "xfoobarybaz", "'foobarbaz'" + ); + + assertEvalFormat("'!\\x@@@&&&\\y'", + "xf y", "'f'", + "xfooy", "'foo'", + "xfoobay", "'fooba'", + "xbarbazy", "'foobarbaz'" + ); + + assertEvalFormat("'\\x&&&@@@\\y'", + "x fy", "'f'", + "xfooy", "'foo'", + "xfoobay", "'fooba'", + "xfoobarybaz", "'foobarbaz'" + ); + + assertEvalFormat("'!\\x&&&@@@\\y'", + "xf y", "'f'", + "xfoo y", "'foo'", + "xfooba y", "'fooba'", + "xbarbazy", "'foobarbaz'" + ); + } + private static void assertEvalFormat(String fmtStr, String... testStrs) { + for(int i = 0; i < testStrs.length; i+=2) { + String expected = testStrs[i]; + String val = testStrs[i + 1]; + + try { + assertEval(expected, + "=Format(" + val + ", " + fmtStr + ")"); + } catch(AssertionFailedError afe) { + throw new AssertionFailedError("Input " + val + ": " + + afe.getMessage()); + } + } } public void testNumberFuncs() throws Exception -- cgit v1.2.3 From 229464aff25cee0ff6941174fefd4e3416800f9f Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Wed, 30 Jan 2019 00:49:32 +0000 Subject: more tests, more bug fixes for custom formats git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1270 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/impl/expr/FormatUtil.java | 20 ++++++--- .../jackcess/impl/expr/DefaultFunctionsTest.java | 50 ++++++++++++++++++---- 2 files changed, 55 insertions(+), 15 deletions(-) (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java index 2258591..34d750a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java @@ -936,7 +936,6 @@ public class FormatUtil private static Fmt parseCustomTextFormat(ExprBuf buf, Args args) { Fmt fmt = null; - Fmt emptyFmt = null; List> subFmts = new ArrayList<>(); int numPlaceholders = 0; @@ -1037,11 +1036,12 @@ public class FormatUtil flushPendingTextLiteral(pendingLiteral, subFmts); + Fmt emptyFmt = null; if(fmt == null) { fmt = new CharSourceFmt(subFmts, numPlaceholders, rightAligned, textCase); emptyFmt = NULL_FMT; - } else if(emptyFmt == null) { + } else { emptyFmt = (hasFmtChars ? new CharSourceFmt(subFmts, numPlaceholders, rightAligned, textCase) : @@ -1438,10 +1438,18 @@ public class FormatUtil private Value formatMaybeZero(BigDecimal bd, NumberFormat fmt) { // in theory we want to use the given format. however, if, due to // rounding, we end up with a number equivalent to zero, then we fall - // back to the zero format - int maxDecDigits = fmt.getMaximumFractionDigits(); - if(maxDecDigits < bd.scale()) { - bd = bd.setScale(maxDecDigits, NumberFormatter.ROUND_MODE); + // back to the zero format. if we are using scientific notation, + // however, then don't worry about this + if(!(fmt instanceof NumberFormatter.ScientificFormat)) { + int maxDecDigits = fmt.getMaximumFractionDigits(); + int mult = ((DecimalFormat)fmt).getMultiplier(); + while(mult > 1) { + ++maxDecDigits; + mult /= 10; + } + if(maxDecDigits < bd.scale()) { + bd = bd.setScale(maxDecDigits, NumberFormatter.ROUND_MODE); + } } if(BigDecimal.ZERO.compareTo(bd) == 0) { // fall back to zero format diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java index 24f3c5b..7275fb2 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java @@ -347,6 +347,8 @@ public class DefaultFunctionsTest extends TestCase "=Format('3.9', '*~dddd, yy mmm d, hh:nn:ss \\Y[Yellow]')"); assertEval("Tuesday, 00 Jan 01/2, 09:36:00 PM", "=Format('3.9', 'dddd, yy mmm mm/d, hh:nn:ss AMPM')"); + assertEval("9:36:00 PM", + "=Format('3.9', 'ttttt')"); assertEval("foo", "=Format('foo', 'dddd, yy mmm mm d, hh:nn:ss AMPM')"); @@ -390,24 +392,46 @@ public class DefaultFunctionsTest extends TestCase "", "''", "", "Null"); - assertEvalFormat("'\"p\"#.00#\"blah\";(\"p\"#.00#\"blah\");\"zero\";\"yuck\"'", + assertEvalFormat("'\\p#.00#\"blah\";*~(\"p\"#.00#\"blah\");\"zero\";\"yuck\"'", "p13.00blah", "13", "(p13.00blah)", "-13", "zero", "0", "", "''", "yuck", "Null"); - assertEvalFormat("'0.##;(0.###);\"zero\";\"yuck\"'", + assertEvalFormat("'0.##;(0.###);\"zero\";\"yuck\";'", "0.03", "0.03", "zero", "0.003", "(0.003)", "-0.003", "zero", "-0.0003"); - // FIXME, need to handle rounding w/ negatives - // FIXME, need to handle dangling decimal - // assertEvalFormat("'0.##;(0.###E+0)'", - // "0.03", "0.03", - // "(0.003)", "-0.0003", + assertEvalFormat("'0.##;(0.###E+0)'", + "0.03", "0.03", + "(3.E-4)", "-0.0003", + "0.", "0", + "34223.", "34223", + "(3.422E+4)", "-34223"); + + assertEvalFormat("'0.###E-0'", + "3.E-4", "0.0003", + "3.422E4", "34223" + ); + + assertEvalFormat("'0.###e+0'", + "3.e-4", "0.0003", + "3.422e+4", "34223" + ); + + assertEvalFormat("'0.###e-0'", + "3.e-4", "0.0003", + "3.422e4", "34223" + ); + + assertEvalFormat("'#,##0.###'", + "0.003", "0.003", + "0.", "0.0003", + "34,223.", "34223" + ); assertEvalFormat("'0.'", "13.", "13", @@ -434,6 +458,14 @@ public class DefaultFunctionsTest extends TestCase "0", "0" ); + assertEvalFormat("'%0'", + "%13", "0.13", + "%0", "0.003", + "-%45", "-0.45", + "%0", "-0.003", + "%0", "0" + ); + assertEvalFormat("'#'", "13", "13", "0", "0.003", @@ -467,14 +499,14 @@ public class DefaultFunctionsTest extends TestCase "", "''", "", "Null"); - assertEvalFormat("'!>@'", + assertEvalFormat("'!>@;'", "O", "'foo'", "3", "-13", "0", "0", "", "''", "", "Null"); - assertEvalFormat("'!>@[Red];\"empty\"'", + assertEvalFormat("'!>*~@[Red];\"empty\";'", "O", "'foo'", "3", "-13", "0", "0", -- cgit v1.2.3 From b2bb4fcdba54b5ecf5d9cbc56f2d3e0fca3326ef Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Thu, 31 Jan 2019 01:28:39 +0000 Subject: more tests and tweaks to custom formatting git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1271 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/impl/expr/FormatUtil.java | 167 ++++++++++++--------- .../jackcess/impl/expr/DefaultFunctionsTest.java | 11 +- 2 files changed, 102 insertions(+), 76 deletions(-) (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java index 34d750a..7f23b4e 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java @@ -18,9 +18,7 @@ package com.healthmarketscience.jackcess.impl.expr; import java.math.BigDecimal; import java.text.DecimalFormat; -import java.text.FieldPosition; import java.text.NumberFormat; -import java.text.ParsePosition; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; @@ -892,7 +890,7 @@ public class FormatUtil pendingLiteral.setLength(0); } - private static NumberFormat createCustomNumberFormat( + private static BDFormat createCustomNumberFormat( String[] fmtStrs, NumberFormatter.NotationType[] expTypes, boolean[] hasFmts, int fmtIdx, Args args) { @@ -911,26 +909,29 @@ public class FormatUtil sb.deleteCharAt(++i); } } + } else { + // this was a single, literal single quote + sb.append(SINGLE_QUOTE_CHAR); } fmtStr = sb.toString(); } - return new LiteralNumberFormat(fmtStr); + return new LiteralBDFormat(fmtStr); } NumberFormatter.NotationType expType = expTypes[fmtIdx]; - NumberFormat nf = args._ctx.createDecimalFormat(fmtStr); + DecimalFormat df = args._ctx.createDecimalFormat(fmtStr); - DecimalFormat df = (DecimalFormat)nf; if(df.getMaximumFractionDigits() > 0) { // if the decimal is included in the format, access always shows it df.setDecimalSeparatorAlwaysShown(true); } if(expType != null) { - nf = new NumberFormatter.ScientificFormat(nf, expType); + return new BaseBDFormat( + new NumberFormatter.ScientificFormat(df, expType)); } - return nf; + return new DecimalBDFormat(df); } private static Fmt parseCustomTextFormat(ExprBuf buf, Args args) { @@ -1289,7 +1290,7 @@ public class FormatUtil public Set> entrySet() { return new AbstractSet>() { @Override - public int size() { + public int size() { return 2; } @Override @@ -1420,40 +1421,66 @@ public class FormatUtil protected abstract Value formatZero(BigDecimal bd, Args args); } + private static final class CustomGeneralFmt extends BaseCustomNumberFmt + { + private final Value _posVal; + private final Value _negVal; + private final Value _zeroVal; + private final Value _nullVal; + + private CustomGeneralFmt(Value posVal, Value negVal, + Value zeroVal, Value nullVal) { + _posVal = posVal; + _negVal = negVal; + _zeroVal = zeroVal; + _nullVal = nullVal; + } + + @Override + protected Value formatNull(Args args) { + return _nullVal; + } + @Override + protected Value formatPos(BigDecimal bd, Args args) { + return _posVal; + } + @Override + protected Value formatNeg(BigDecimal bd, Args args) { + return _negVal; + } + @Override + protected Value formatZero(BigDecimal bd, Args args) { + return _zeroVal; + } + } + private static final class CustomNumberFmt extends BaseCustomNumberFmt { - private final NumberFormat _posFmt; - private final NumberFormat _negFmt; - private final NumberFormat _zeroFmt; - private final NumberFormat _nullFmt; + private final BDFormat _posFmt; + private final BDFormat _negFmt; + private final BDFormat _zeroFmt; + private final BDFormat _nullFmt; - private CustomNumberFmt(NumberFormat posFmt, NumberFormat negFmt, - NumberFormat zeroFmt, NumberFormat nullFmt) { + private CustomNumberFmt(BDFormat posFmt, BDFormat negFmt, + BDFormat zeroFmt, BDFormat nullFmt) { _posFmt = posFmt; _negFmt = negFmt; _zeroFmt = zeroFmt; _nullFmt = nullFmt; } - private Value formatMaybeZero(BigDecimal bd, NumberFormat fmt) { + private Value formatMaybeZero(BigDecimal bd, BDFormat fmt) { // in theory we want to use the given format. however, if, due to // rounding, we end up with a number equivalent to zero, then we fall // back to the zero format. if we are using scientific notation, // however, then don't worry about this - if(!(fmt instanceof NumberFormatter.ScientificFormat)) { - int maxDecDigits = fmt.getMaximumFractionDigits(); - int mult = ((DecimalFormat)fmt).getMultiplier(); - while(mult > 1) { - ++maxDecDigits; - mult /= 10; - } - if(maxDecDigits < bd.scale()) { + int maxDecDigits = fmt.getMaxDecimalDigits(); + if(maxDecDigits < bd.scale()) { bd = bd.setScale(maxDecDigits, NumberFormatter.ROUND_MODE); - } - } - if(BigDecimal.ZERO.compareTo(bd) == 0) { - // fall back to zero format - fmt = _zeroFmt; + if(BigDecimal.ZERO.compareTo(bd) == 0) { + // fall back to zero format + fmt = _zeroFmt; + } } return ValueSupport.toValue(fmt.format(bd)); @@ -1477,72 +1504,62 @@ public class FormatUtil } } - private static final class CustomGeneralFmt extends BaseCustomNumberFmt + private static abstract class BDFormat { - private final Value _posVal; - private final Value _negVal; - private final Value _zeroVal; - private final Value _nullVal; - - private CustomGeneralFmt(Value posVal, Value negVal, - Value zeroVal, Value nullVal) { - _posVal = posVal; - _negVal = negVal; - _zeroVal = zeroVal; - _nullVal = nullVal; + public int getMaxDecimalDigits() { + return Integer.MAX_VALUE; } - @Override - protected Value formatNull(Args args) { - return _nullVal; - } - @Override - protected Value formatPos(BigDecimal bd, Args args) { - return _posVal; - } - @Override - protected Value formatNeg(BigDecimal bd, Args args) { - return _negVal; - } - @Override - protected Value formatZero(BigDecimal bd, Args args) { - return _zeroVal; - } + public abstract String format(BigDecimal bd); } - private static final class LiteralNumberFormat extends NumberFormat + private static final class LiteralBDFormat extends BDFormat { - private static final long serialVersionUID = 0L; - private final String _str; - private LiteralNumberFormat(String str) { + private LiteralBDFormat(String str) { _str = str; } @Override - public StringBuffer format(Object number, StringBuffer toAppendTo, - FieldPosition pos) - { - return toAppendTo.append(_str); + public String format(BigDecimal bd) { + return _str; } + } - @Override - public StringBuffer format(double number, StringBuffer toAppendTo, - FieldPosition pos) { - throw new UnsupportedOperationException(); + private static class BaseBDFormat extends BDFormat + { + private final NumberFormat _nf; + + private BaseBDFormat(NumberFormat nf) { + _nf = nf; } @Override - public Number parse(String source, ParsePosition parsePosition) { - throw new UnsupportedOperationException(); + public String format(BigDecimal bd) { + return _nf.format(bd); + } + } + + private static final class DecimalBDFormat extends BaseBDFormat + { + private final int _maxDecDigits; + + private DecimalBDFormat(DecimalFormat df) { + super(df); + + int maxDecDigits = df.getMaximumFractionDigits(); + int mult = df.getMultiplier(); + while(mult > 1) { + ++maxDecDigits; + mult /= 10; + } + _maxDecDigits = maxDecDigits; } @Override - public StringBuffer format(long number, StringBuffer toAppendTo, - FieldPosition pos) { - throw new UnsupportedOperationException(); + public int getMaxDecimalDigits() { + return _maxDecDigits; } } - } diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java index 7275fb2..b25f150 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java @@ -333,6 +333,7 @@ public class DefaultFunctionsTest extends TestCase public void testCustomFormat() throws Exception { assertEval("07:00 a", "=Format(#01/10/2003 7:00:00 AM#, 'hh:nn a/p')"); + assertEval("07:00 p", "=Format(#01/10/2003 7:00:00 PM#, 'hh:nn a/p')"); assertEval("07:00 a 6 2", "=Format(#01/10/2003 7:00:00 AM#, 'hh:nn a/p w ww')"); assertEval("07:00 a 4 1", "=Format(#01/10/2003 7:00:00 AM#, 'hh:nn a/p w ww', 3, 3)"); assertEval("1313", "=Format(#01/10/2003 7:13:00 AM#, 'nnnn; foo bar')"); @@ -349,6 +350,8 @@ public class DefaultFunctionsTest extends TestCase "=Format('3.9', 'dddd, yy mmm mm/d, hh:nn:ss AMPM')"); assertEval("9:36:00 PM", "=Format('3.9', 'ttttt')"); + assertEval("9:36:00 PM", + "=Format(3.9, 'ttttt')"); assertEval("foo", "=Format('foo', 'dddd, yy mmm mm d, hh:nn:ss AMPM')"); @@ -359,11 +362,12 @@ public class DefaultFunctionsTest extends TestCase "n", "'0'", "", "Null"); - assertEvalFormat("';\"y\";!\\n;*~\\z[Blue];'", + assertEvalFormat("'\\p;\"y\";!\\n;*~\\z[Blue];'", "foo", "'foo'", "", "''", "y", "True", "n", "'0'", + "p", "'10'", "z", "Null"); assertEvalFormat("'\"p\"#.00#\"blah\"'", @@ -475,6 +479,11 @@ public class DefaultFunctionsTest extends TestCase // "", "0" ); + assertEvalFormat("\"#;n'g;'\"", + "5", "5", + "n'g", "-5", + "'", "0"); + assertEvalFormat("'$0.0#'", "$213.0", "213"); -- cgit v1.2.3 From 7e01bae3cd7b64686ee25d11172a8051b37eb799 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Mon, 4 Feb 2019 02:37:47 +0000 Subject: handle number formats with no required digits git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1273 f203690c-595d-4dc9-a70b-905162fa7fd2 --- .../jackcess/impl/expr/FormatUtil.java | 69 ++++++++++++++++++---- .../jackcess/impl/expr/DefaultFunctionsTest.java | 17 ++++-- 2 files changed, 70 insertions(+), 16 deletions(-) (limited to 'src/test') diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java index 7f23b4e..6dfc2e3 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java @@ -169,6 +169,7 @@ public class FormatUtil private static final char EXP_e_CHAR = 'e'; private static final char PLUS_CHAR = '+'; private static final char MINUS_CHAR = '-'; + private static final char REQ_DIGIT_CHAR = '0'; private static final int NO_CHAR = -1; private static final byte FCT_UNKNOWN = 0; @@ -554,7 +555,8 @@ public class FormatUtil private static void addCustomGeneralFormat(String[] fmtStrs, int fmtIdx, StringBuilder sb) { - addCustomNumberFormat(fmtStrs, NO_EXP_TYPES, NO_FMT_TYPES, fmtIdx, sb); + addCustomNumberFormat(fmtStrs, NO_EXP_TYPES, NO_FMT_TYPES, NO_FMT_TYPES, + fmtIdx, sb); } private static Fmt parseCustomDateFormat(ExprBuf buf, Args args) { @@ -735,6 +737,7 @@ public class FormatUtil NumberFormatter.NotationType[] expTypes = new NumberFormatter.NotationType[NUM_NF_FMTS]; boolean[] hasFmts = new boolean[NUM_NF_FMTS]; + boolean[] hasReqDigit = new boolean[NUM_NF_FMTS]; BUF_LOOP: while(buf.hasNext()) { @@ -773,7 +776,8 @@ public class FormatUtil break BUF_LOOP; } flushPendingNumberLiteral(pendingLiteral, sb); - addCustomNumberFormat(fmtStrs, expTypes, hasFmts, fmtIdx++, sb); + addCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit, + fmtIdx++, sb); break; default: pendingLiteral.append(c); @@ -808,6 +812,11 @@ public class FormatUtil pendingLiteral.append(c); } break; + case REQ_DIGIT_CHAR: + hasReqDigit[fmtIdx] = true; + flushPendingNumberLiteral(pendingLiteral, sb); + sb.append(c); + break; default: // most number format chars pass straight through flushPendingNumberLiteral(pendingLiteral, sb); @@ -822,19 +831,24 @@ public class FormatUtil // fill in remaining formats while(fmtIdx < NUM_NF_FMTS) { flushPendingNumberLiteral(pendingLiteral, sb); - addCustomNumberFormat(fmtStrs, expTypes, hasFmts, fmtIdx++, sb); + addCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit, + fmtIdx++, sb); } return new CustomNumberFmt( - createCustomNumberFormat(fmtStrs, expTypes, hasFmts, NF_POS_IDX, args), - createCustomNumberFormat(fmtStrs, expTypes, hasFmts, NF_NEG_IDX, args), - createCustomNumberFormat(fmtStrs, expTypes, hasFmts, NF_ZERO_IDX, args), - createCustomNumberFormat(fmtStrs, expTypes, hasFmts, NF_NULL_IDX, args)); + createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit, + NF_POS_IDX, false, args, buf), + createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit, + NF_NEG_IDX, false, args, buf), + createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit, + NF_ZERO_IDX, true, args, buf), + createCustomNumberFormat(fmtStrs, expTypes, hasFmts, hasReqDigit, + NF_NULL_IDX, true, args, buf)); } private static void addCustomNumberFormat( String[] fmtStrs, NumberFormatter.NotationType[] expTypes, - boolean[] hasFmts, int fmtIdx, StringBuilder sb) + boolean[] hasFmts, boolean[] hasReqDigit, int fmtIdx, StringBuilder sb) { if(sb.length() == 0) { // do special empty format handling on a per-format-type basis @@ -844,12 +858,14 @@ public class FormatUtil sb.append('-').append(fmtStrs[NF_POS_IDX]); expTypes[NF_NEG_IDX] = expTypes[NF_POS_IDX]; hasFmts[NF_NEG_IDX] = hasFmts[NF_POS_IDX]; + hasReqDigit[NF_NEG_IDX] = hasReqDigit[NF_POS_IDX]; break; case NF_ZERO_IDX: // re-use "pos" format sb.append(fmtStrs[NF_POS_IDX]); expTypes[NF_ZERO_IDX] = expTypes[NF_POS_IDX]; hasFmts[NF_ZERO_IDX] = hasFmts[NF_POS_IDX]; + hasReqDigit[NF_ZERO_IDX] = hasReqDigit[NF_POS_IDX]; break; default: // use empty string result @@ -892,14 +908,15 @@ public class FormatUtil private static BDFormat createCustomNumberFormat( String[] fmtStrs, NumberFormatter.NotationType[] expTypes, - boolean[] hasFmts, int fmtIdx, Args args) { + boolean[] hasFmts, boolean[] hasReqDigit, int fmtIdx, + boolean isZeroFmt, Args args, ExprBuf buf) { String fmtStr = fmtStrs[fmtIdx]; if(!hasFmts[fmtIdx]) { // convert the literal string to a dummy number format if(fmtStr.length() > 0) { // strip quoting - StringBuilder sb = new StringBuilder(fmtStr) + StringBuilder sb = buf.getScratchBuffer().append(fmtStr) .deleteCharAt(fmtStr.length() - 1) .deleteCharAt(0); if(sb.length() > 0) { @@ -927,8 +944,36 @@ public class FormatUtil } if(expType != null) { - return new BaseBDFormat( - new NumberFormatter.ScientificFormat(df, expType)); + NumberFormat nf = new NumberFormatter.ScientificFormat(df, expType); + if(isZeroFmt) { + return new LiteralBDFormat(nf.format(BigDecimal.ZERO)); + } + return new BaseBDFormat(nf); + } + + if(!hasReqDigit[fmtIdx]) { + // java likes to force extra 0's while access doesn't + df.setMinimumIntegerDigits(0); + } + + if(isZeroFmt) { + + String zeroVal = df.format(BigDecimal.ZERO); + if(!hasReqDigit[fmtIdx]) { + // java forces a 0 but access doesn't. delete any 0 chars which were + // inserted by the java format + int prefLen = df.getPositivePrefix().length(); + int len = zeroVal.length() - df.getPositiveSuffix().length(); + StringBuilder sb = buf.getScratchBuffer().append(zeroVal); + for(int i = len - 1; i >= prefLen; --i) { + if(sb.charAt(i) == '0') { + sb.deleteCharAt(i); + } + } + zeroVal = sb.toString(); + } + + return new LiteralBDFormat(zeroVal); } return new DecimalBDFormat(df); diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java index b25f150..dc4cabb 100644 --- a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java @@ -472,11 +472,20 @@ public class DefaultFunctionsTest extends TestCase assertEvalFormat("'#'", "13", "13", - "0", "0.003", + "", "0.003", "-45", "-45", - "0", "-0.003" - // FIXME - // "", "0" + "", "-0.003", + "", "0" + ); + + assertEvalFormat("'\\0\\[#.#\\]\\0'", + "0[13.]0", "13", + "0[.]0", "0.003", + "0[.3]0", "0.3", + "-0[45.]0", "-45", + "0[.]0", "-0.003", + "-0[.3]0", "-0.3", + "0[.]0", "0" ); assertEvalFormat("\"#;n'g;'\"", -- cgit v1.2.3