aboutsummaryrefslogtreecommitdiffstats
path: root/src/test
diff options
context:
space:
mode:
Diffstat (limited to 'src/test')
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java121
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java283
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/TestUtil.java80
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java106
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/impl/TopoSorterTest.java166
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java221
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java468
7 files changed, 1405 insertions, 40 deletions
diff --git a/src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java b/src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java
index 7ca0521..056ca68 100644
--- a/src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java
+++ b/src/test/java/com/healthmarketscience/jackcess/PropertiesTest.java
@@ -21,9 +21,11 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
import static com.healthmarketscience.jackcess.Database.*;
+import com.healthmarketscience.jackcess.InvalidValueException;
import com.healthmarketscience.jackcess.impl.DatabaseImpl;
import static com.healthmarketscience.jackcess.impl.JetFormatTest.*;
import com.healthmarketscience.jackcess.impl.PropertyMapImpl;
@@ -44,7 +46,7 @@ public class PropertiesTest extends TestCase
public void testPropertyMaps() throws Exception
{
- PropertyMaps maps = new PropertyMaps(10, null, null);
+ PropertyMaps maps = new PropertyMaps(10, null, null, null);
assertTrue(maps.isEmpty());
assertEquals(0, maps.getSize());
assertFalse(maps.iterator().hasNext());
@@ -103,7 +105,7 @@ public class PropertiesTest extends TestCase
public void testInferTypes() throws Exception
{
- PropertyMaps maps = new PropertyMaps(10, null, null);
+ PropertyMaps maps = new PropertyMaps(10, null, null, null);
PropertyMap defMap = maps.getDefault();
assertEquals(DataType.TEXT,
@@ -210,7 +212,8 @@ public class PropertiesTest extends TestCase
for(Row row : ((DatabaseImpl)db).getSystemCatalog()) {
int id = row.getInt("Id");
byte[] propBytes = row.getBytes("LvProp");
- PropertyMaps propMaps = ((DatabaseImpl)db).getPropertiesForObject(id);
+ PropertyMaps propMaps = ((DatabaseImpl)db).getPropertiesForObject(
+ id, null);
int byteLen = ((propBytes != null) ? propBytes.length : 0);
if(byteLen == 0) {
assertTrue(propMaps.isEmpty());
@@ -403,9 +406,119 @@ public class PropertiesTest extends TestCase
}
}
+ public void testEnforceProperties() throws Exception
+ {
+ for(final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
+ Database db = create(fileFormat);
+
+ Table t = new TableBuilder("testReq")
+ .addColumn(new ColumnBuilder("id", DataType.LONG)
+ .setAutoNumber(true)
+ .putProperty(PropertyMap.REQUIRED_PROP, true))
+ .addColumn(new ColumnBuilder("value", DataType.TEXT)
+ .putProperty(PropertyMap.REQUIRED_PROP, true))
+ .toTable(db);
+
+ t.addRow(Column.AUTO_NUMBER, "v1");
+
+ try {
+ t.addRow(Column.AUTO_NUMBER, null);
+ fail("InvalidValueException should have been thrown");
+ } catch(InvalidValueException expected) {
+ // success
+ }
+
+ t.addRow(Column.AUTO_NUMBER, "");
+
+ List<? extends Map<String, Object>> expectedRows =
+ createExpectedTable(
+ createExpectedRow(
+ "id", 1,
+ "value", "v1"),
+ createExpectedRow(
+ "id", 2,
+ "value", ""));
+ assertTable(expectedRows, t);
+
+
+ t = new TableBuilder("testNz")
+ .addColumn(new ColumnBuilder("id", DataType.LONG)
+ .setAutoNumber(true)
+ .putProperty(PropertyMap.REQUIRED_PROP, true))
+ .addColumn(new ColumnBuilder("value", DataType.TEXT)
+ .putProperty(PropertyMap.ALLOW_ZERO_LEN_PROP, false))
+ .toTable(db);
+
+ t.addRow(Column.AUTO_NUMBER, "v1");
+
+ try {
+ t.addRow(Column.AUTO_NUMBER, "");
+ fail("InvalidValueException should have been thrown");
+ } catch(InvalidValueException expected) {
+ // success
+ }
+
+ t.addRow(Column.AUTO_NUMBER, null);
+
+ expectedRows =
+ createExpectedTable(
+ createExpectedRow(
+ "id", 1,
+ "value", "v1"),
+ createExpectedRow(
+ "id", 2,
+ "value", null));
+ assertTable(expectedRows, t);
+
+
+ t = new TableBuilder("testReqNz")
+ .addColumn(new ColumnBuilder("id", DataType.LONG)
+ .setAutoNumber(true)
+ .putProperty(PropertyMap.REQUIRED_PROP, true))
+ .addColumn(new ColumnBuilder("value", DataType.TEXT))
+ .toTable(db);
+
+ Column col = t.getColumn("value");
+ PropertyMap props = col.getProperties();
+ props.put(PropertyMap.REQUIRED_PROP, true);
+ props.put(PropertyMap.ALLOW_ZERO_LEN_PROP, false);
+ props.save();
+
+ t.addRow(Column.AUTO_NUMBER, "v1");
+
+ try {
+ t.addRow(Column.AUTO_NUMBER, "");
+ fail("InvalidValueException should have been thrown");
+ } catch(InvalidValueException expected) {
+ // success
+ }
+
+ try {
+ t.addRow(Column.AUTO_NUMBER, null);
+ fail("InvalidValueException should have been thrown");
+ } catch(InvalidValueException expected) {
+ // success
+ }
+
+ t.addRow(Column.AUTO_NUMBER, "v2");
+
+ expectedRows =
+ createExpectedTable(
+ createExpectedRow(
+ "id", 1,
+ "value", "v1"),
+ createExpectedRow(
+ "id", 2,
+ "value", "v2"));
+ assertTable(expectedRows, t);
+
+ db.close();
+ }
+ }
+
public void testEnumValues() throws Exception
{
- PropertyMaps maps = new PropertyMaps(10, null, null);
+ PropertyMaps maps = new PropertyMaps(10, null, null, null);
PropertyMapImpl colMap = maps.get("testcol");
diff --git a/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java b/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java
new file mode 100644
index 0000000..5d3fb44
--- /dev/null
+++ b/src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java
@@ -0,0 +1,283 @@
+/*
+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.util.List;
+
+import junit.framework.TestCase;
+
+import static com.healthmarketscience.jackcess.Database.*;
+import static com.healthmarketscience.jackcess.TestUtil.*;
+import static com.healthmarketscience.jackcess.impl.JetFormatTest.*;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class PropertyExpressionTest extends TestCase
+{
+
+ public PropertyExpressionTest(String name) {
+ super(name);
+ }
+
+ public void testDefaultValue() throws Exception
+ {
+ for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
+ Database db = create(fileFormat);
+ db.setEvaluateExpressions(true);
+
+ Table t = new TableBuilder("test")
+ .addColumn(new ColumnBuilder("id", DataType.LONG).setAutoNumber(true))
+ .addColumn(new ColumnBuilder("data1", DataType.TEXT)
+ .putProperty(PropertyMap.DEFAULT_VALUE_PROP,
+ "=\"FOO \" & \"BAR\""))
+ .addColumn(new ColumnBuilder("data2", DataType.LONG)
+ .putProperty(PropertyMap.DEFAULT_VALUE_PROP,
+ "37"))
+ .toTable(db);
+
+ t.addRow(Column.AUTO_NUMBER, null, 13);
+ t.addRow(Column.AUTO_NUMBER, "blah", null);
+
+ setProp(t, "data1", PropertyMap.DEFAULT_VALUE_PROP, null);
+ setProp(t, "data2", PropertyMap.DEFAULT_VALUE_PROP, "42");
+
+ t.addRow(Column.AUTO_NUMBER, null, null);
+
+ List<Row> expectedRows =
+ createExpectedTable(
+ createExpectedRow(
+ "id", 1,
+ "data1", "FOO BAR",
+ "data2", 13),
+ createExpectedRow(
+ "id", 2,
+ "data1", "blah",
+ "data2", 37),
+ createExpectedRow(
+ "id", 3,
+ "data1", null,
+ "data2", 42));
+
+ assertTable(expectedRows, t);
+
+ db.close();
+ }
+ }
+
+ public void testCalculatedValue() throws Exception
+ {
+ Database db = create(FileFormat.V2016);
+ db.setEvaluateExpressions(true);
+
+ Table t = new TableBuilder("test")
+ .addColumn(new ColumnBuilder("id", DataType.LONG).setAutoNumber(true))
+ .addColumn(new ColumnBuilder("c1", DataType.LONG)
+ .setCalculatedInfo("[c2]+[c3]"))
+ .addColumn(new ColumnBuilder("c2", DataType.LONG)
+ .setCalculatedInfo("[c3]*5"))
+ .addColumn(new ColumnBuilder("c3", DataType.LONG)
+ .setCalculatedInfo("[c4]-6"))
+ .addColumn(new ColumnBuilder("c4", DataType.LONG))
+ .toTable(db);
+
+ t.addRow(Column.AUTO_NUMBER, null, null, null, 16);
+
+ setProp(t, "c1", PropertyMap.EXPRESSION_PROP, "[c4]+2");
+ setProp(t, "c2", PropertyMap.EXPRESSION_PROP, "[c1]+[c3]");
+ setProp(t, "c3", PropertyMap.EXPRESSION_PROP, "[c1]*7");
+
+ t.addRow(Column.AUTO_NUMBER, null, null, null, 7);
+
+ List<Row> expectedRows =
+ createExpectedTable(
+ createExpectedRow(
+ "id", 1,
+ "c1", 60,
+ "c2", 50,
+ "c3", 10,
+ "c4", 16),
+ createExpectedRow(
+ "id", 2,
+ "c1", 9,
+ "c2", 72,
+ "c3", 63,
+ "c4", 7));
+
+ assertTable(expectedRows, t);
+
+ db.close();
+ }
+
+ public void testColumnValidator() throws Exception
+ {
+ for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
+ Database db = create(fileFormat);
+ db.setEvaluateExpressions(true);
+
+ Table t = new TableBuilder("test")
+ .addColumn(new ColumnBuilder("id", DataType.LONG).setAutoNumber(true))
+ .addColumn(new ColumnBuilder("data1", DataType.LONG)
+ .putProperty(PropertyMap.VALIDATION_RULE_PROP,
+ ">37"))
+ .addColumn(new ColumnBuilder("data2", DataType.LONG)
+ .putProperty(PropertyMap.VALIDATION_RULE_PROP,
+ "between 7 and 10")
+ .putProperty(PropertyMap.VALIDATION_TEXT_PROP,
+ "You failed"))
+ .toTable(db);
+
+ t.addRow(Column.AUTO_NUMBER, 42, 8);
+
+ try {
+ t.addRow(Column.AUTO_NUMBER, 42, 20);
+ fail("InvalidValueException should have been thrown");
+ } catch(InvalidValueException ive) {
+ // success
+ assertTrue(ive.getMessage().contains("You failed"));
+ }
+
+ try {
+ t.addRow(Column.AUTO_NUMBER, 3, 8);
+ fail("InvalidValueException should have been thrown");
+ } catch(InvalidValueException ive) {
+ // success
+ assertFalse(ive.getMessage().contains("You failed"));
+ }
+
+ t.addRow(Column.AUTO_NUMBER, 54, 9);
+
+ setProp(t, "data1", PropertyMap.VALIDATION_RULE_PROP, null);
+ setProp(t, "data2", PropertyMap.VALIDATION_RULE_PROP, "<100");
+ setProp(t, "data2", PropertyMap.VALIDATION_TEXT_PROP, "Too big");
+
+ try {
+ t.addRow(Column.AUTO_NUMBER, 42, 200);
+ fail("InvalidValueException should have been thrown");
+ } catch(InvalidValueException ive) {
+ // success
+ assertTrue(ive.getMessage().contains("Too big"));
+ }
+
+ t.addRow(Column.AUTO_NUMBER, 1, 9);
+
+ List<Row> expectedRows =
+ createExpectedTable(
+ createExpectedRow(
+ "id", 1,
+ "data1", 42,
+ "data2", 8),
+ createExpectedRow(
+ "id", 2,
+ "data1", 54,
+ "data2", 9),
+ createExpectedRow(
+ "id", 3,
+ "data1", 1,
+ "data2", 9));
+
+ assertTable(expectedRows, t);
+
+ db.close();
+ }
+ }
+
+ public void testRowValidator() throws Exception
+ {
+ for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
+ Database db = create(fileFormat);
+ db.setEvaluateExpressions(true);
+
+ Table t = new TableBuilder("test")
+ .addColumn(new ColumnBuilder("id", DataType.LONG).setAutoNumber(true))
+ .addColumn(new ColumnBuilder("data1", DataType.LONG))
+ .addColumn(new ColumnBuilder("data2", DataType.LONG))
+ .putProperty(PropertyMap.VALIDATION_RULE_PROP,
+ "([data1] > 10) and ([data2] < 100)")
+ .putProperty(PropertyMap.VALIDATION_TEXT_PROP,
+ "You failed")
+ .toTable(db);
+
+ t.addRow(Column.AUTO_NUMBER, 42, 8);
+
+ try {
+ t.addRow(Column.AUTO_NUMBER, 1, 20);
+ fail("InvalidValueException should have been thrown");
+ } catch(InvalidValueException ive) {
+ // success
+ assertTrue(ive.getMessage().contains("You failed"));
+ }
+
+ t.addRow(Column.AUTO_NUMBER, 54, 9);
+
+ setTableProp(t, PropertyMap.VALIDATION_RULE_PROP, "[data2]<100");
+ setTableProp(t, PropertyMap.VALIDATION_TEXT_PROP, "Too big");
+
+ try {
+ t.addRow(Column.AUTO_NUMBER, 42, 200);
+ fail("InvalidValueException should have been thrown");
+ } catch(InvalidValueException ive) {
+ // success
+ assertTrue(ive.getMessage().contains("Too big"));
+ }
+
+ t.addRow(Column.AUTO_NUMBER, 1, 9);
+
+ List<Row> expectedRows =
+ createExpectedTable(
+ createExpectedRow(
+ "id", 1,
+ "data1", 42,
+ "data2", 8),
+ createExpectedRow(
+ "id", 2,
+ "data1", 54,
+ "data2", 9),
+ createExpectedRow(
+ "id", 3,
+ "data1", 1,
+ "data2", 9));
+
+ assertTable(expectedRows, t);
+
+ db.close();
+ }
+ }
+
+ private static void setProp(Table t, String colName, String propName,
+ String propVal) throws Exception {
+ PropertyMap props = t.getColumn(colName).getProperties();
+ if(propVal != null) {
+ props.put(propName, propVal);
+ } else {
+ props.remove(propName);
+ }
+ props.save();
+ }
+
+ private static void setTableProp(Table t, String propName,
+ String propVal) throws Exception {
+ PropertyMap props = t.getProperties();
+ if(propVal != null) {
+ props.put(propName, propVal);
+ } else {
+ props.remove(propName);
+ }
+ props.save();
+ }
+}
diff --git a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java
index 3317c7f..7680fb3 100644
--- a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java
+++ b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java
@@ -53,12 +53,12 @@ import org.junit.Assert;
*
* @author James Ahlborn
*/
-public class TestUtil
+public class TestUtil
{
public static final TimeZone TEST_TZ =
TimeZone.getTimeZone("America/New_York");
-
- private static final ThreadLocal<Boolean> _autoSync =
+
+ private static final ThreadLocal<Boolean> _autoSync =
new ThreadLocal<Boolean>();
private TestUtil() {}
@@ -76,22 +76,22 @@ public class TestUtil
return ((autoSync != null) ? autoSync : Database.DEFAULT_AUTO_SYNC);
}
- public static Database open(FileFormat fileFormat, File file)
- throws Exception
+ public static Database open(FileFormat fileFormat, File file)
+ throws Exception
{
return open(fileFormat, file, false);
}
- public static Database open(FileFormat fileFormat, File file, boolean inMem)
- throws Exception
+ public static Database open(FileFormat fileFormat, File file, boolean inMem)
+ throws Exception
{
FileChannel channel = (inMem ? MemFileChannel.newChannel(
- file, DatabaseImpl.RW_CHANNEL_MODE)
+ file, DatabaseImpl.RW_CHANNEL_MODE)
: null);
final Database db = new DatabaseBuilder(file).setReadOnly(true)
.setAutoSync(getTestAutoSync()).setChannel(channel).open();
- Assert.assertEquals("Wrong JetFormat.",
- DatabaseImpl.getFileFormatDetails(fileFormat).getFormat(),
+ Assert.assertEquals("Wrong JetFormat.",
+ DatabaseImpl.getFileFormatDetails(fileFormat).getFormat(),
((DatabaseImpl)db).getFormat());
Assert.assertEquals("Wrong FileFormat.", fileFormat, db.getFileFormat());
return db;
@@ -109,8 +109,8 @@ public class TestUtil
return create(fileFormat, false);
}
- public static Database create(FileFormat fileFormat, boolean keep)
- throws Exception
+ public static Database create(FileFormat fileFormat, boolean keep)
+ throws Exception
{
return create(fileFormat, keep, false);
}
@@ -119,9 +119,9 @@ public class TestUtil
return create(fileFormat, false, true);
}
- private static Database create(FileFormat fileFormat, boolean keep,
- boolean inMem)
- throws Exception
+ private static Database create(FileFormat fileFormat, boolean keep,
+ boolean inMem)
+ throws Exception
{
FileChannel channel = (inMem ? MemFileChannel.newChannel() : null);
@@ -147,7 +147,7 @@ public class TestUtil
ByteUtil.closeQuietly(outStream);
}
}
-
+
return new DatabaseBuilder(createTempFile(keep)).setFileFormat(fileFormat)
.setAutoSync(getTestAutoSync()).setChannel(channel).create();
}
@@ -176,7 +176,7 @@ public class TestUtil
File tmp = createTempFile(keep);
copyFile(file, tmp);
Database db = new DatabaseBuilder(tmp).setAutoSync(getTestAutoSync()).open();
- Assert.assertEquals("Wrong JetFormat.",
+ Assert.assertEquals("Wrong JetFormat.",
DatabaseImpl.getFileFormatDetails(fileFormat).getFormat(),
((DatabaseImpl)db).getFormat());
Assert.assertEquals("Wrong FileFormat.", fileFormat, db.getFileFormat());
@@ -192,7 +192,7 @@ public class TestUtil
public static Object[] createTestRow() {
return createTestRow("Tim");
}
-
+
static Map<String,Object> createTestRowMap(String col1Val) {
return createExpectedRow("A", col1Val, "B", "R", "C", "McCune",
"D", 1234, "E", (byte) 0xad, "F", 555.66d,
@@ -220,7 +220,7 @@ public class TestUtil
static String createNonAsciiString(int len) {
return createString(len, '\u0CC0');
}
-
+
private static String createString(int len, char firstChar) {
StringBuilder builder = new StringBuilder(len);
for(int i = 0; i < len; ++i) {
@@ -235,7 +235,7 @@ public class TestUtil
Assert.assertEquals(expectedRowCount, countRows(table));
Assert.assertEquals(expectedRowCount, table.getRowCount());
}
-
+
public static int countRows(Table table) throws Exception {
int rtn = 0;
for(Map<String, Object> row : CursorBuilder.createCursor(table)) {
@@ -245,15 +245,15 @@ public class TestUtil
}
public static void assertTable(
- List<? extends Map<String, Object>> expectedTable,
+ List<? extends Map<String, Object>> expectedTable,
Table table)
throws IOException
{
assertCursor(expectedTable, CursorBuilder.createCursor(table));
}
-
+
public static void assertCursor(
- List<? extends Map<String, Object>> expectedTable,
+ List<? extends Map<String, Object>> expectedTable,
Cursor cursor)
{
List<Map<String, Object>> foundTable =
@@ -264,9 +264,9 @@ public class TestUtil
Assert.assertEquals(expectedTable.size(), foundTable.size());
for(int i = 0; i < expectedTable.size(); ++i) {
Assert.assertEquals(expectedTable.get(i), foundTable.get(i));
- }
+ }
}
-
+
public static RowImpl createExpectedRow(Object... rowElements) {
RowImpl row = new RowImpl((RowIdImpl)null);
for(int i = 0; i < rowElements.length; i += 2) {
@@ -274,12 +274,12 @@ public class TestUtil
rowElements[i + 1]);
}
return row;
- }
+ }
public static List<Row> createExpectedTable(Row... rows) {
return Arrays.<Row>asList(rows);
- }
-
+ }
+
public static void dumpDatabase(Database mdb) throws Exception {
dumpDatabase(mdb, false);
}
@@ -313,7 +313,7 @@ public class TestUtil
for(Index index : table.getIndexes()) {
((IndexImpl)index).initialize();
}
-
+
writer.println("TABLE: " + table.getName());
List<String> colNames = new ArrayList<String>();
for(Column col : table.getColumns()) {
@@ -377,25 +377,33 @@ public class TestUtil
"), found " + foundTime + " (" + found + ")");
}
}
-
+
static void copyFile(File srcFile, File dstFile)
throws IOException
{
// FIXME should really be using commons io FileUtils here, but don't want
// to add dep for one simple test method
- byte[] buf = new byte[1024];
OutputStream ostream = new FileOutputStream(dstFile);
InputStream istream = new FileInputStream(srcFile);
try {
- int numBytes = 0;
- while((numBytes = istream.read(buf)) >= 0) {
- ostream.write(buf, 0, numBytes);
- }
+ copyStream(istream, ostream);
} finally {
ostream.close();
}
}
+ static void copyStream(InputStream istream, OutputStream ostream)
+ throws IOException
+ {
+ // FIXME should really be using commons io FileUtils here, but don't want
+ // to add dep for one simple test method
+ byte[] buf = new byte[1024];
+ int numBytes = 0;
+ while((numBytes = istream.read(buf)) >= 0) {
+ ostream.write(buf, 0, numBytes);
+ }
+ }
+
static File createTempFile(boolean keep) throws Exception {
File tmp = File.createTempFile("databaseTest", ".mdb");
if(keep) {
@@ -416,7 +424,7 @@ public class TestUtil
val = f.get(val);
((Map<?,?>)val).clear();
}
-
+
public static byte[] toByteArray(File file)
throws IOException
{
diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java
new file mode 100644
index 0000000..f69dca1
--- /dev/null
+++ b/src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java
@@ -0,0 +1,106 @@
+/*
+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.math.BigDecimal;
+
+import junit.framework.TestCase;
+
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class NumberFormatterTest extends TestCase
+{
+
+ public NumberFormatterTest(String name) {
+ super(name);
+ }
+
+ public void testDoubleFormat() throws Exception
+ {
+ assertEquals("894984737284944", NumberFormatter.format(894984737284944d));
+ assertEquals("-894984737284944", NumberFormatter.format(-894984737284944d));
+ assertEquals("8949.84737284944", NumberFormatter.format(8949.84737284944d));
+ assertEquals("8949847372844", NumberFormatter.format(8949847372844d));
+ assertEquals("8949.847384944", NumberFormatter.format(8949.847384944d));
+ assertEquals("8.94985647372849E+16", NumberFormatter.format(89498564737284944d));
+ assertEquals("-8.94985647372849E+16", NumberFormatter.format(-89498564737284944d));
+ assertEquals("895649.847372849", NumberFormatter.format(895649.84737284944d));
+ assertEquals("300", NumberFormatter.format(300d));
+ assertEquals("-300", NumberFormatter.format(-300d));
+ assertEquals("0.3", NumberFormatter.format(0.3d));
+ assertEquals("0.1", NumberFormatter.format(0.1d));
+ assertEquals("2.3423421E-12", NumberFormatter.format(0.0000000000023423421d));
+ assertEquals("2.3423421E-11", NumberFormatter.format(0.000000000023423421d));
+ assertEquals("2.3423421E-10", NumberFormatter.format(0.00000000023423421d));
+ assertEquals("-2.3423421E-10", NumberFormatter.format(-0.00000000023423421d));
+ assertEquals("2.34234214E-12", NumberFormatter.format(0.00000000000234234214d));
+ assertEquals("2.342342156E-12", NumberFormatter.format(0.000000000002342342156d));
+ assertEquals("0.000000023423421", NumberFormatter.format(0.000000023423421d));
+ assertEquals("2.342342133E-07", NumberFormatter.format(0.0000002342342133d));
+ assertEquals("1.#INF", NumberFormatter.format(Double.POSITIVE_INFINITY));
+ assertEquals("-1.#INF", NumberFormatter.format(Double.NEGATIVE_INFINITY));
+ assertEquals("1.#QNAN", NumberFormatter.format(Double.NaN));
+ }
+
+ public void testFloatFormat() throws Exception
+ {
+ assertEquals("8949847", NumberFormatter.format(8949847f));
+ assertEquals("-8949847", NumberFormatter.format(-8949847f));
+ assertEquals("8949.847", NumberFormatter.format(8949.847f));
+ assertEquals("894984", NumberFormatter.format(894984f));
+ assertEquals("8949.84", NumberFormatter.format(8949.84f));
+ assertEquals("8.949856E+16", NumberFormatter.format(89498564737284944f));
+ assertEquals("-8.949856E+16", NumberFormatter.format(-89498564737284944f));
+ assertEquals("895649.9", NumberFormatter.format(895649.84737284944f));
+ assertEquals("300", NumberFormatter.format(300f));
+ assertEquals("-300", NumberFormatter.format(-300f));
+ assertEquals("0.3", NumberFormatter.format(0.3f));
+ assertEquals("0.1", NumberFormatter.format(0.1f));
+ assertEquals("2.342342E-12", NumberFormatter.format(0.0000000000023423421f));
+ assertEquals("2.342342E-11", NumberFormatter.format(0.000000000023423421f));
+ assertEquals("2.342342E-10", NumberFormatter.format(0.00000000023423421f));
+ assertEquals("-2.342342E-10", NumberFormatter.format(-0.00000000023423421f));
+ assertEquals("2.342342E-12", NumberFormatter.format(0.00000000000234234214f));
+ assertEquals("2.342342E-12", NumberFormatter.format(0.000000000002342342156f));
+ assertEquals("0.0000234", NumberFormatter.format(0.0000234f));
+ assertEquals("2.342E-05", NumberFormatter.format(0.00002342f));
+ assertEquals("1.#INF", NumberFormatter.format(Float.POSITIVE_INFINITY));
+ assertEquals("-1.#INF", NumberFormatter.format(Float.NEGATIVE_INFINITY));
+ assertEquals("1.#QNAN", NumberFormatter.format(Float.NaN));
+ }
+
+ public void testDecimalFormat() throws Exception
+ {
+ assertEquals("9874539485972.2342342234234", NumberFormatter.format(new BigDecimal("9874539485972.2342342234234")));
+ assertEquals("9874539485972.234234223423468", NumberFormatter.format(new BigDecimal("9874539485972.2342342234234678")));
+ assertEquals("-9874539485972.234234223423468", NumberFormatter.format(new BigDecimal("-9874539485972.2342342234234678")));
+ assertEquals("9.874539485972234234223423468E+31", NumberFormatter.format(new BigDecimal("98745394859722342342234234678000")));
+ assertEquals("9.874539485972234234223423468E+31", NumberFormatter.format(new BigDecimal("98745394859722342342234234678000")));
+ assertEquals("-9.874539485972234234223423468E+31", NumberFormatter.format(new BigDecimal("-98745394859722342342234234678000")));
+ assertEquals("300", NumberFormatter.format(new BigDecimal("300.0")));
+ assertEquals("-300", NumberFormatter.format(new BigDecimal("-300.000")));
+ assertEquals("0.3", NumberFormatter.format(new BigDecimal("0.3")));
+ assertEquals("0.1", NumberFormatter.format(new BigDecimal("0.1000")));
+ assertEquals("0.0000000000023423428930458", NumberFormatter.format(new BigDecimal("0.0000000000023423428930458")));
+ assertEquals("2.3423428930458389038451E-12", NumberFormatter.format(new BigDecimal("0.0000000000023423428930458389038451")));
+ assertEquals("2.342342893045838903845134766E-12", NumberFormatter.format(new BigDecimal("0.0000000000023423428930458389038451347656")));
+ }
+}
diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/TopoSorterTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/TopoSorterTest.java
new file mode 100644
index 0000000..61acacb
--- /dev/null
+++ b/src/test/java/com/healthmarketscience/jackcess/impl/TopoSorterTest.java
@@ -0,0 +1,166 @@
+/*
+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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class TopoSorterTest extends TestCase
+{
+
+ public TopoSorterTest(String name) {
+ super(name);
+ }
+
+ public void testTopoSort() throws Exception
+ {
+ doTopoTest(Arrays.asList("A", "B", "C"),
+ Arrays.asList("A", "B", "C"));
+
+ doTopoTest(Arrays.asList("B", "A", "C"),
+ Arrays.asList("A", "B", "C"),
+ "B", "C",
+ "A", "B");
+
+ try {
+ doTopoTest(Arrays.asList("B", "A", "C"),
+ Arrays.asList("C", "B", "A"),
+ "B", "C",
+ "A", "B",
+ "C", "A");
+ fail("IllegalStateException should have been thrown");
+ } catch(IllegalStateException expected) {
+ // success
+ assertTrue(expected.getMessage().startsWith("Cycle"));
+ }
+
+ try {
+ doTopoTest(Arrays.asList("B", "A", "C"),
+ Arrays.asList("C", "B", "A"),
+ "B", "D");
+ fail("IllegalStateException should have been thrown");
+ } catch(IllegalStateException expected) {
+ // success
+ assertTrue(expected.getMessage().startsWith("Unknown descendent"));
+ }
+
+ doTopoTest(Arrays.asList("B", "D", "A", "C"),
+ Arrays.asList("D", "A", "B", "C"),
+ "B", "C",
+ "A", "B");
+
+ doTopoTest(Arrays.asList("B", "D", "A", "C"),
+ Arrays.asList("A", "D", "B", "C"),
+ "B", "C",
+ "A", "B",
+ "A", "D");
+
+ doTopoTest(Arrays.asList("B", "D", "A", "C"),
+ Arrays.asList("D", "A", "C", "B"),
+ "D", "A",
+ "C", "B");
+
+ doTopoTest(Arrays.asList("B", "D", "A", "C"),
+ Arrays.asList("D", "C", "A", "B"),
+ "D", "A",
+ "C", "B",
+ "C", "A");
+
+ doTopoTest(Arrays.asList("B", "D", "A", "C"),
+ Arrays.asList("C", "D", "A", "B"),
+ "D", "A",
+ "C", "B",
+ "C", "D");
+
+ doTopoTest(Arrays.asList("B", "D", "A", "C"),
+ Arrays.asList("D", "A", "C", "B"),
+ "D", "A",
+ "C", "B",
+ "D", "B");
+ }
+
+ private static void doTopoTest(List<String> original,
+ List<String> expected,
+ String... descs) {
+
+ List<String> values = new ArrayList<String>();
+ values.addAll(original);
+
+ TestTopoSorter tsorter = new TestTopoSorter(values, false);
+ for(int i = 0; i < descs.length; i+=2) {
+ tsorter.addDescendents(descs[i], descs[i+1]);
+ }
+
+ tsorter.sort();
+
+ assertEquals(expected, values);
+
+
+ values = new ArrayList<String>();
+ values.addAll(original);
+
+ tsorter = new TestTopoSorter(values, true);
+ for(int i = 0; i < descs.length; i+=2) {
+ tsorter.addDescendents(descs[i], descs[i+1]);
+ }
+
+ tsorter.sort();
+
+ List<String> expectedReverse = new ArrayList<String>(expected);
+ Collections.reverse(expectedReverse);
+
+ assertEquals(expectedReverse, values);
+ }
+
+ private static class TestTopoSorter extends TopoSorter<String>
+ {
+ private final Map<String,List<String>> _descMap =
+ new HashMap<String,List<String>>();
+
+ protected TestTopoSorter(List<String> values, boolean reverse) {
+ super(values, reverse);
+ }
+
+ public void addDescendents(String from, String... tos) {
+ List<String> descs = _descMap.get(from);
+ if(descs == null) {
+ descs = new ArrayList<String>();
+ _descMap.put(from, descs);
+ }
+
+ descs.addAll(Arrays.asList(tos));
+ }
+
+ @Override
+ protected void getDescendents(String from, List<String> descendents) {
+ List<String> descs = _descMap.get(from);
+ if(descs != null) {
+ descendents.addAll(descs);
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java
new file mode 100644
index 0000000..0b02888
--- /dev/null
+++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java
@@ -0,0 +1,221 @@
+/*
+Copyright (c) 2016 James Ahlborn
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package com.healthmarketscience.jackcess.impl.expr;
+
+import java.math.BigDecimal;
+
+import com.healthmarketscience.jackcess.expr.EvalException;
+import junit.framework.TestCase;
+import static com.healthmarketscience.jackcess.impl.expr.ExpressionatorTest.eval;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class DefaultFunctionsTest extends TestCase
+{
+
+ public DefaultFunctionsTest(String name) {
+ super(name);
+ }
+
+ public void testFuncs() throws Exception
+ {
+ assertEquals("foo", eval("=IIf(10 > 1, \"foo\", \"bar\")"));
+ assertEquals("bar", eval("=IIf(10 < 1, \"foo\", \"bar\")"));
+ assertEquals(102, eval("=Asc(\"foo\")"));
+ assertEquals(9786, eval("=AscW(\"\u263A\")"));
+ assertEquals("f", eval("=Chr(102)"));
+ assertEquals("\u263A", eval("=ChrW(9786)"));
+ assertEquals("263A", eval("=Hex(9786)"));
+
+ assertEquals("blah", eval("=Nz(\"blah\")"));
+ assertEquals("", eval("=Nz(Null)"));
+ assertEquals("blah", eval("=Nz(\"blah\",\"FOO\")"));
+ assertEquals("FOO", eval("=Nz(Null,\"FOO\")"));
+
+ assertEquals("23072", eval("=Oct(9786)"));
+ assertEquals(" 9786", eval("=Str(9786)"));
+ assertEquals("-42", eval("=Str(-42)"));
+ assertEquals("-42", eval("=Str$(-42)"));
+ assertNull(eval("=Str(Null)"));
+
+ try {
+ eval("=Str$(Null)");
+ fail("EvalException should have been thrown");
+ } catch(EvalException expected) {
+ // success
+ }
+
+ assertEquals(-1, eval("=CBool(\"1\")"));
+ assertEquals(13, eval("=CByte(\"13\")"));
+ assertEquals(14, eval("=CByte(\"13.7\")"));
+ assertEquals(new BigDecimal("57.1235"), eval("=CCur(\"57.12346\")"));
+ assertEquals(new Double("57.12345"), eval("=CDbl(\"57.12345\")"));
+ assertEquals(new BigDecimal("57.123456789"), eval("=CDec(\"57.123456789\")"));
+ assertEquals(513, eval("=CInt(\"513\")"));
+ assertEquals(514, eval("=CInt(\"513.7\")"));
+ assertEquals(345513, eval("=CLng(\"345513\")"));
+ assertEquals(345514, eval("=CLng(\"345513.7\")"));
+ assertEquals(new Float("57.12345").doubleValue(),
+ eval("=CSng(\"57.12345\")"));
+ assertEquals("9786", eval("=CStr(9786)"));
+ assertEquals("-42", eval("=CStr(-42)"));
+
+ assertEquals(2, eval("=InStr('AFOOBAR', 'FOO')"));
+ assertEquals(2, eval("=InStr('AFOOBAR', 'foo')"));
+ assertEquals(2, eval("=InStr(1, 'AFOOBAR', 'foo')"));
+ assertEquals(0, eval("=InStr(1, 'AFOOBAR', 'foo', 0)"));
+ assertEquals(2, eval("=InStr(1, 'AFOOBAR', 'foo', 1)"));
+ assertEquals(2, eval("=InStr(1, 'AFOOBAR', 'FOO', 0)"));
+ assertEquals(2, eval("=InStr(2, 'AFOOBAR', 'FOO')"));
+ assertEquals(0, eval("=InStr(3, 'AFOOBAR', 'FOO')"));
+ assertEquals(0, eval("=InStr(17, 'AFOOBAR', 'FOO')"));
+ assertEquals(2, eval("=InStr(1, 'AFOOBARFOOBAR', 'FOO')"));
+ assertEquals(8, eval("=InStr(3, 'AFOOBARFOOBAR', 'FOO')"));
+ assertNull(eval("=InStr(3, Null, 'FOO')"));
+
+ assertEquals(2, eval("=InStrRev('AFOOBAR', 'FOO')"));
+ assertEquals(2, eval("=InStrRev('AFOOBAR', 'foo')"));
+ assertEquals(2, eval("=InStrRev('AFOOBAR', 'foo', -1)"));
+ assertEquals(0, eval("=InStrRev('AFOOBAR', 'foo', -1, 0)"));
+ assertEquals(2, eval("=InStrRev('AFOOBAR', 'foo', -1, 1)"));
+ assertEquals(2, eval("=InStrRev('AFOOBAR', 'FOO', -1, 0)"));
+ assertEquals(2, eval("=InStrRev('AFOOBAR', 'FOO', 4)"));
+ assertEquals(0, eval("=InStrRev('AFOOBAR', 'FOO', 3)"));
+ assertEquals(2, eval("=InStrRev('AFOOBAR', 'FOO', 17)"));
+ assertEquals(2, eval("=InStrRev('AFOOBARFOOBAR', 'FOO', 9)"));
+ assertEquals(8, eval("=InStrRev('AFOOBARFOOBAR', 'FOO', 10)"));
+ assertNull(eval("=InStrRev(Null, 'FOO', 3)"));
+
+ assertEquals("FOOO", eval("=UCase(\"fOoO\")"));
+ assertEquals("fooo", eval("=LCase(\"fOoO\")"));
+
+ assertEquals("bl", eval("=Left(\"blah\", 2)"));
+ assertEquals("", eval("=Left(\"blah\", 0)"));
+ assertEquals("blah", eval("=Left(\"blah\", 17)"));
+
+ assertEquals("ah", eval("=Right(\"blah\", 2)"));
+ assertEquals("", eval("=Right(\"blah\", 0)"));
+ assertEquals("blah", eval("=Right(\"blah\", 17)"));
+
+ }
+
+
+ public void testFinancialFuncs() throws Exception
+ {
+ assertEquals("-9.57859403981317",
+ eval("=CStr(NPer(0.12/12,-100,-1000))"));
+ assertEquals("-9.48809500550583",
+ eval("=CStr(NPer(0.12/12,-100,-1000,0,1))"));
+ assertEquals("60.0821228537617",
+ eval("=CStr(NPer(0.12/12,-100,-1000,10000))"));
+ assertEquals("59.6738656742946",
+ eval("=CStr(NPer(0.12/12,-100,-1000,10000,1))"));
+ assertEquals("69.6607168935748",
+ eval("=CStr(NPer(0.12/12,-100,0,10000))"));
+ assertEquals("69.1619606798004",
+ eval("=CStr(NPer(0.12/12,-100,0,10000,1))"));
+
+ assertEquals("8166.96698564091",
+ eval("=CStr(FV(0.12/12,60,-100))"));
+ assertEquals("8248.63665549732",
+ eval("=CStr(FV(0.12/12,60,-100,0,1))"));
+ assertEquals("6350.27028707682",
+ eval("=CStr(FV(0.12/12,60,-100,1000))"));
+ assertEquals("6431.93995693323",
+ eval("=CStr(FV(0.12/12,60,-100,1000,1))"));
+
+ assertEquals("4495.5038406224",
+ eval("=CStr(PV(0.12/12,60,-100))"));
+ assertEquals("4540.45887902863",
+ eval("=CStr(PV(0.12/12,60,-100,0,1))"));
+ assertEquals("-1008.99231875519",
+ eval("=CStr(PV(0.12/12,60,-100,10000))"));
+ assertEquals("-964.037280348968",
+ eval("=CStr(PV(0.12/12,60,-100,10000,1))"));
+
+ assertEquals("22.2444476849018",
+ eval("=CStr(Pmt(0.12/12,60,-1000))"));
+ assertEquals("22.0242056286156",
+ eval("=CStr(Pmt(0.12/12,60,-1000,0,1))"));
+ assertEquals("-100.200029164116",
+ eval("=CStr(Pmt(0.12/12,60,-1000,10000))"));
+ assertEquals("-99.2079496674414",
+ eval("=CStr(Pmt(0.12/12,60,-1000,10000,1))"));
+ assertEquals("-122.444476849018",
+ eval("=CStr(Pmt(0.12/12,60,0,10000))"));
+ assertEquals("-121.232155296057",
+ eval("=CStr(Pmt(0.12/12,60,0,10000,1))"));
+
+ // FIXME not working for all param combos
+ // assertEquals("10.0",
+ // eval("=CStr(IPmt(0.12/12,1,60,-1000))"));
+ // assertEquals("5.904184782975672",
+ // eval("=CStr(IPmt(0.12/12,30,60,-1000))"));
+ // 0
+ // assertEquals("",
+ // eval("=CStr(IPmt(0.12/12,1,60,-1000,0,1))"));
+ // 5.84572750...
+ // assertEquals("5.845727507896704",
+ // eval("=CStr(IPmt(0.12/12,30,60,-1000,0,1))"));
+ // 0
+ // assertEquals("",
+ // eval("=CStr(IPmt(0.12/12,1,60,0,10000))"));
+ // 40.9581521702433
+ // assertEquals("40.95815217024329",
+ // eval("=CStr(IPmt(0.12/12,30,60,0,10000))"));
+ // 0
+ // assertEquals("",
+ // eval("=CStr(IPmt(0.12/12,1,60,0,10000,1))"));
+ // 40.552625911132
+ // assertEquals("40.55262591113197",
+ // eval("=CStr(IPmt(0.12/12,30,60,0,10000,1))"));
+ // assertEquals("10.0",
+ // eval("=CStr(IPmt(0.12/12,1,60,-1000,10000))"));
+ // assertEquals("46.862336953218964",
+ // eval("=CStr(IPmt(0.12/12,30,60,-1000,10000))"));
+ // 0
+ // assertEquals("",
+ // eval("=CStr(IPmt(0.12/12,1,60,-1000,10000,1))"));
+ // 46.3983534190287
+ // assertEquals("46.39835341902867",
+ // eval("=CStr(IPmt(0.12/12,30,60,-1000,10000,1))"));
+
+ // FIXME, doesn't work for partial days
+ // assertEquals("1.3150684931506849",
+ // eval("=CStr(DDB(2400,300,10*365,1))"));
+ // assertEquals("40.0",
+ // eval("=CStr(DDB(2400,300,10*12,1))"));
+ // assertEquals("480.0",
+ // eval("=CStr(DDB(2400,300,10,1))"));
+ // assertEquals("22.122547200000042",
+ // eval("=CStr(DDB(2400,300,10,10))"));
+ // assertEquals("245.76",
+ // eval("=CStr(DDB(2400,300,10,4))"));
+ // assertEquals("307.20000000000005",
+ // eval("=CStr(DDB(2400,300,10,3))"));
+ // assertEquals("480.0",
+ // eval("=CStr(DDB(2400,300,10,0.1))"));
+ // 274.768033075174
+ // assertEquals("",
+ // eval("=CStr(DDB(2400,300,10,3.5))"));
+
+
+ }
+
+}
diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java
new file mode 100644
index 0000000..2f6e738
--- /dev/null
+++ b/src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java
@@ -0,0 +1,468 @@
+/*
+Copyright (c) 2016 James Ahlborn
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package com.healthmarketscience.jackcess.impl.expr;
+
+import java.math.BigDecimal;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import javax.script.Bindings;
+import javax.script.SimpleBindings;
+
+import com.healthmarketscience.jackcess.DatabaseBuilder;
+import com.healthmarketscience.jackcess.TestUtil;
+import com.healthmarketscience.jackcess.expr.EvalContext;
+import com.healthmarketscience.jackcess.expr.Expression;
+import com.healthmarketscience.jackcess.expr.Function;
+import com.healthmarketscience.jackcess.expr.Identifier;
+import com.healthmarketscience.jackcess.expr.TemporalConfig;
+import com.healthmarketscience.jackcess.expr.Value;
+import com.healthmarketscience.jackcess.impl.NumberFormatter;
+import junit.framework.TestCase;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class ExpressionatorTest extends TestCase
+{
+ private static final double[] DBLS = {
+ -10.3d,-9.0d,-8.234d,-7.11111d,-6.99999d,-5.5d,-4.0d,-3.4159265d,-2.84d,
+ -1.0000002d,-1.0d,-0.0002013d,0.0d, 0.9234d,1.0d,1.954d,2.200032d,3.001d,
+ 4.9321d,5.0d,6.66666d,7.396d,8.1d,9.20456200d,10.325d};
+
+ public ExpressionatorTest(String name) {
+ super(name);
+ }
+
+
+ public void testParseSimpleExprs() throws Exception
+ {
+ validateExpr("\"A\"", "<ELiteralValue>{\"A\"}");
+
+ validateExpr("13", "<ELiteralValue>{13}");
+
+ validateExpr("-42", "<EUnaryOp>{- <ELiteralValue>{42}}");
+
+ validateExpr("(+37)", "<EParen>{(<EUnaryOp>{+ <ELiteralValue>{37}})}");
+
+ doTestSimpleBinOp("EBinaryOp", "+", "-", "*", "/", "\\", "^", "&", "Mod");
+ doTestSimpleBinOp("ECompOp", "<", "<=", ">", ">=", "=", "<>");
+ doTestSimpleBinOp("ELogicalOp", "And", "Or", "Eqv", "Xor", "Imp");
+
+ for(String constStr : new String[]{"True", "False", "Null"}) {
+ validateExpr(constStr, "<EConstValue>{" + constStr + "}");
+ }
+
+ validateExpr("[Field1]", "<EObjValue>{[Field1]}");
+
+ validateExpr("[Table2].[Field3]", "<EObjValue>{[Table2].[Field3]}");
+
+ validateExpr("Not \"A\"", "<EUnaryOp>{Not <ELiteralValue>{\"A\"}}");
+
+ validateExpr("-[Field1]", "<EUnaryOp>{- <EObjValue>{[Field1]}}");
+
+ validateExpr("\"A\" Is Null", "<ENullOp>{<ELiteralValue>{\"A\"} Is Null}");
+
+ validateExpr("\"A\" In (1,2,3)", "<EInOp>{<ELiteralValue>{\"A\"} In (<ELiteralValue>{1},<ELiteralValue>{2},<ELiteralValue>{3})}");
+
+ validateExpr("\"A\" Not Between 3 And 7", "<EBetweenOp>{<ELiteralValue>{\"A\"} Not Between <ELiteralValue>{3} And <ELiteralValue>{7}}");
+
+ validateExpr("(\"A\" Or \"B\")", "<EParen>{(<ELogicalOp>{<ELiteralValue>{\"A\"} Or <ELiteralValue>{\"B\"}})}");
+
+ validateExpr("IIf(\"A\",42,False)", "<EFunc>{IIf(<ELiteralValue>{\"A\"},<ELiteralValue>{42},<EConstValue>{False})}");
+
+ validateExpr("\"A\" Like \"a*b\"", "<ELikeOp>{<ELiteralValue>{\"A\"} Like \"a*b\"(a.*b)}");
+
+ validateExpr("' \"A\" '", "<ELiteralValue>{\" \"\"A\"\" \"}",
+ "\" \"\"A\"\" \"");
+ }
+
+ private static void doTestSimpleBinOp(String opName, String... ops) throws Exception
+ {
+ for(String op : ops) {
+ validateExpr("\"A\" " + op + " \"B\"",
+ "<" + opName + ">{<ELiteralValue>{\"A\"} " + op +
+ " <ELiteralValue>{\"B\"}}");
+ }
+ }
+
+ public void testOrderOfOperations() throws Exception
+ {
+ validateExpr("\"A\" Eqv \"B\"",
+ "<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELiteralValue>{\"B\"}}");
+
+ validateExpr("\"A\" Eqv \"B\" Xor \"C\"",
+ "<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELogicalOp>{<ELiteralValue>{\"B\"} Xor <ELiteralValue>{\"C\"}}}");
+
+ validateExpr("\"A\" Eqv \"B\" Xor \"C\" Or \"D\"",
+ "<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELogicalOp>{<ELiteralValue>{\"B\"} Xor <ELogicalOp>{<ELiteralValue>{\"C\"} Or <ELiteralValue>{\"D\"}}}}");
+
+ validateExpr("\"A\" Eqv \"B\" Xor \"C\" Or \"D\" And \"E\"",
+ "<ELogicalOp>{<ELiteralValue>{\"A\"} Eqv <ELogicalOp>{<ELiteralValue>{\"B\"} Xor <ELogicalOp>{<ELiteralValue>{\"C\"} Or <ELogicalOp>{<ELiteralValue>{\"D\"} And <ELiteralValue>{\"E\"}}}}}");
+
+ validateExpr("\"A\" Or \"B\" Or \"C\"",
+ "<ELogicalOp>{<ELogicalOp>{<ELiteralValue>{\"A\"} Or <ELiteralValue>{\"B\"}} Or <ELiteralValue>{\"C\"}}");
+
+ validateExpr("\"A\" & \"B\" Is Null",
+ "<ENullOp>{<EBinaryOp>{<ELiteralValue>{\"A\"} & <ELiteralValue>{\"B\"}} Is Null}");
+
+ validateExpr("\"A\" Or \"B\" Is Null",
+ "<ELogicalOp>{<ELiteralValue>{\"A\"} Or <ENullOp>{<ELiteralValue>{\"B\"} Is Null}}");
+
+ validateExpr("Not \"A\" & \"B\"",
+ "<EUnaryOp>{Not <EBinaryOp>{<ELiteralValue>{\"A\"} & <ELiteralValue>{\"B\"}}}");
+
+ validateExpr("Not \"A\" Or \"B\"",
+ "<ELogicalOp>{<EUnaryOp>{Not <ELiteralValue>{\"A\"}} Or <ELiteralValue>{\"B\"}}");
+
+ validateExpr("\"A\" + \"B\" Not Between 37 - 15 And 52 / 4",
+ "<EBetweenOp>{<EBinaryOp>{<ELiteralValue>{\"A\"} + <ELiteralValue>{\"B\"}} Not Between <EBinaryOp>{<ELiteralValue>{37} - <ELiteralValue>{15}} And <EBinaryOp>{<ELiteralValue>{52} / <ELiteralValue>{4}}}");
+
+ validateExpr("\"A\" + (\"B\" Not Between 37 - 15 And 52) / 4",
+ "<EBinaryOp>{<ELiteralValue>{\"A\"} + <EBinaryOp>{<EParen>{(<EBetweenOp>{<ELiteralValue>{\"B\"} Not Between <EBinaryOp>{<ELiteralValue>{37} - <ELiteralValue>{15}} And <ELiteralValue>{52}})} / <ELiteralValue>{4}}}");
+
+
+ }
+
+ public void testSimpleMathExpressions() throws Exception
+ {
+ for(int i = -10; i <= 10; ++i) {
+ assertEquals(-i, eval("=-(" + i + ")"));
+ }
+
+ for(int i = -10; i <= 10; ++i) {
+ assertEquals(i, eval("=+(" + i + ")"));
+ }
+
+ for(double i : DBLS) {
+ assertEquals(toBD(-i), eval("=-(" + i + ")"));
+ }
+
+ for(double i : DBLS) {
+ assertEquals(toBD(i), eval("=+(" + i + ")"));
+ }
+
+ for(int i = -10; i <= 10; ++i) {
+ for(int j = -10; j <= 10; ++j) {
+ assertEquals((i + j), eval("=" + i + " + " + j));
+ }
+ }
+
+ for(double i : DBLS) {
+ for(double j : DBLS) {
+ assertEquals(toBD(toBD(i).add(toBD(j))), eval("=" + i + " + " + j));
+ }
+ }
+
+ for(int i = -10; i <= 10; ++i) {
+ for(int j = -10; j <= 10; ++j) {
+ assertEquals((i - j), eval("=" + i + " - " + j));
+ }
+ }
+
+ for(double i : DBLS) {
+ for(double j : DBLS) {
+ assertEquals(toBD(toBD(i).subtract(toBD(j))), eval("=" + i + " - " + j));
+ }
+ }
+
+ for(int i = -10; i <= 10; ++i) {
+ for(int j = -10; j <= 10; ++j) {
+ assertEquals((i * j), eval("=" + i + " * " + j));
+ }
+ }
+
+ for(double i : DBLS) {
+ for(double j : DBLS) {
+ assertEquals(toBD(toBD(i).multiply(toBD(j))), eval("=" + i + " * " + j));
+ }
+ }
+
+ for(int i = -10; i <= 10; ++i) {
+ for(int j = -10; j <= 10; ++j) {
+ if(j == 0L) {
+ evalFail("=" + i + " \\ " + j, ArithmeticException.class);
+ } else {
+ assertEquals((i / j), eval("=" + i + " \\ " + j));
+ }
+ }
+ }
+
+ for(double i : DBLS) {
+ for(double j : DBLS) {
+ if(roundToLongInt(j) == 0) {
+ evalFail("=" + i + " \\ " + j, ArithmeticException.class);
+ } else {
+ assertEquals((roundToLongInt(i) / roundToLongInt(j)),
+ eval("=" + i + " \\ " + j));
+ }
+ }
+ }
+
+ for(int i = -10; i <= 10; ++i) {
+ for(int j = -10; j <= 10; ++j) {
+ if(j == 0) {
+ evalFail("=" + i + " Mod " + j, ArithmeticException.class);
+ } else {
+ assertEquals((i % j), eval("=" + i + " Mod " + j));
+ }
+ }
+ }
+
+ for(double i : DBLS) {
+ for(double j : DBLS) {
+ if(roundToLongInt(j) == 0) {
+ evalFail("=" + i + " Mod " + j, ArithmeticException.class);
+ } else {
+ assertEquals((roundToLongInt(i) % roundToLongInt(j)),
+ eval("=" + i + " Mod " + j));
+ }
+ }
+ }
+
+ for(int i = -10; i <= 10; ++i) {
+ for(int j = -10; j <= 10; ++j) {
+ if(j == 0) {
+ evalFail("=" + i + " / " + j, ArithmeticException.class);
+ } else {
+ double result = (double)i / (double)j;
+ if((int)result == result) {
+ assertEquals((int)result, eval("=" + i + " / " + j));
+ } else {
+ assertEquals(result, eval("=" + i + " / " + j));
+ }
+ }
+ }
+ }
+
+ for(double i : DBLS) {
+ for(double j : DBLS) {
+ if(j == 0.0d) {
+ evalFail("=" + i + " / " + j, ArithmeticException.class);
+ } else {
+ assertEquals(toBD(BuiltinOperators.divide(toBD(i), toBD(j))),
+ eval("=" + i + " / " + j));
+ }
+ }
+ }
+
+ for(int i = -10; i <= 10; ++i) {
+ for(int j = -10; j <= 10; ++j) {
+ double result = Math.pow(i, j);
+ if((int)result == result) {
+ assertEquals((int)result, eval("=" + i + " ^ " + j));
+ } else {
+ assertEquals(result, eval("=" + i + " ^ " + j));
+ }
+ }
+ }
+ }
+
+ public void testTrickyMathExpressions() throws Exception
+ {
+ assertEquals(37, eval("=30+7"));
+ assertEquals(23, eval("=30+-7"));
+ assertEquals(23, eval("=30-+7"));
+ assertEquals(37, eval("=30--7"));
+ assertEquals(23, eval("=30-7"));
+
+ assertEquals(100, eval("=-10^2"));
+ assertEquals(-100, eval("=-(10)^2"));
+ assertEquals(-100d, eval("=-\"10\"^2"));
+ assertEquals(toBD(-98.9d), eval("=1.1+(-\"10\"^2)"));
+
+ assertEquals(toBD(99d), eval("=-10E-1+10e+1"));
+ assertEquals(toBD(-101d), eval("=-10E-1-10e+1"));
+ }
+
+ public void testTypeCoercion() throws Exception
+ {
+ assertEquals("foobar", eval("=\"foo\" + \"bar\""));
+
+ assertEquals("12foo", eval("=12 + \"foo\""));
+ assertEquals("foo12", eval("=\"foo\" + 12"));
+
+ assertEquals(37d, eval("=\"25\" + 12"));
+ assertEquals(37d, eval("=12 + \"25\""));
+
+ evalFail(("=12 - \"foo\""), RuntimeException.class);
+ evalFail(("=\"foo\" - 12"), RuntimeException.class);
+
+ assertEquals("foo1225", eval("=\"foo\" + 12 + 25"));
+ assertEquals("37foo", eval("=12 + 25 + \"foo\""));
+ assertEquals("foo37", eval("=\"foo\" + (12 + 25)"));
+ assertEquals("25foo12", eval("=\"25foo\" + 12"));
+
+ assertEquals(new Date(1485579600000L), eval("=#1/1/2017# + 27"));
+ assertEquals(128208, eval("=#1/1/2017# * 3"));
+ }
+
+ public void testLikeExpression() throws Exception
+ {
+ validateExpr("Like \"[abc]*\"", "<ELikeOp>{<EThisValue>{<THIS_COL>} Like \"[abc]*\"([abc].*)}",
+ "<THIS_COL> Like \"[abc]*\"");
+ assertTrue(evalCondition("Like \"[abc]*\"", "afcd"));
+ assertFalse(evalCondition("Like \"[abc]*\"", "fcd"));
+
+ validateExpr("Like \"[abc*\"", "<ELikeOp>{<EThisValue>{<THIS_COL>} Like \"[abc*\"((?!))}",
+ "<THIS_COL> Like \"[abc*\"");
+ assertFalse(evalCondition("Like \"[abc*\"", "afcd"));
+ assertFalse(evalCondition("Like \"[abc*\"", "fcd"));
+ assertFalse(evalCondition("Like \"[abc*\"", ""));
+ }
+
+ public void testLiteralDefaultValue() throws Exception
+ {
+ assertEquals("-28 blah ", eval("=CDbl(9)-37 & \" blah \"",
+ Value.Type.STRING));
+ assertEquals("CDbl(9)-37 & \" blah \"",
+ eval("CDbl(9)-37 & \" blah \"", Value.Type.STRING));
+
+ assertEquals(-28d, eval("=CDbl(9)-37", Value.Type.DOUBLE));
+ assertEquals(-28d, eval("CDbl(9)-37", Value.Type.DOUBLE));
+ }
+
+ private static void validateExpr(String exprStr, String debugStr) {
+ validateExpr(exprStr, debugStr, exprStr);
+ }
+
+ private static void validateExpr(String exprStr, String debugStr,
+ String cleanStr) {
+ Expression expr = Expressionator.parse(
+ Expressionator.Type.FIELD_VALIDATOR, exprStr, null, null);
+ String foundDebugStr = expr.toDebugString();
+ if(foundDebugStr.startsWith("<EImplicitCompOp>")) {
+ assertEquals("<EImplicitCompOp>{<EThisValue>{<THIS_COL>} = " +
+ debugStr + "}", foundDebugStr);
+ } else {
+ assertEquals(debugStr, foundDebugStr);
+ }
+ assertEquals(cleanStr, expr.toString());
+ }
+
+ static Object eval(String exprStr) {
+ return eval(exprStr, null);
+ }
+
+ static Object eval(String exprStr, Value.Type resultType) {
+ Expression expr = Expressionator.parse(
+ Expressionator.Type.DEFAULT_VALUE, exprStr, resultType,
+ new TestParseContext());
+ return expr.eval(new TestEvalContext(null));
+ }
+
+ private static void evalFail(
+ String exprStr, Class<? extends Exception> failure)
+ {
+ Expression expr = Expressionator.parse(
+ Expressionator.Type.DEFAULT_VALUE, exprStr, null,
+ new TestParseContext());
+ try {
+ expr.eval(new TestEvalContext(null));
+ fail(failure + " should have been thrown");
+ } catch(Exception e) {
+ assertTrue(failure.isInstance(e));
+ }
+ }
+
+ private static Boolean evalCondition(String exprStr, String thisVal) {
+ Expression expr = Expressionator.parse(
+ Expressionator.Type.FIELD_VALIDATOR, exprStr, null, new TestParseContext());
+ return (Boolean)expr.eval(new TestEvalContext(BuiltinOperators.toValue(thisVal)));
+ }
+
+ static int roundToLongInt(double d) {
+ return new BigDecimal(d).setScale(0, NumberFormatter.ROUND_MODE)
+ .intValueExact();
+ }
+
+ static BigDecimal toBD(double d) {
+ return toBD(BigDecimal.valueOf(d));
+ }
+
+ static BigDecimal toBD(BigDecimal bd) {
+ return BuiltinOperators.normalize(bd);
+ }
+
+ private static final class TestParseContext implements Expressionator.ParseContext
+ {
+ public TemporalConfig getTemporalConfig() {
+ return TemporalConfig.US_TEMPORAL_CONFIG;
+ }
+ public SimpleDateFormat createDateFormat(String formatStr) {
+ SimpleDateFormat sdf = DatabaseBuilder.createDateFormat(formatStr);
+ sdf.setTimeZone(TestUtil.TEST_TZ);
+ return sdf;
+ }
+
+ public Function getExpressionFunction(String name) {
+ return DefaultFunctions.getFunction(name);
+ }
+ }
+
+ private static final class TestEvalContext implements EvalContext
+ {
+ private final Value _thisVal;
+ private final RandomContext _rndCtx = new RandomContext();
+ private final Bindings _bindings = new SimpleBindings();
+
+ private TestEvalContext(Value thisVal) {
+ _thisVal = thisVal;
+ }
+
+ public Value.Type getResultType() {
+ return null;
+ }
+
+ public TemporalConfig getTemporalConfig() {
+ return TemporalConfig.US_TEMPORAL_CONFIG;
+ }
+
+ public SimpleDateFormat createDateFormat(String formatStr) {
+ SimpleDateFormat sdf = DatabaseBuilder.createDateFormat(formatStr);
+ sdf.setTimeZone(TestUtil.TEST_TZ);
+ return sdf;
+ }
+
+ public Value getThisColumnValue() {
+ if(_thisVal == null) {
+ throw new UnsupportedOperationException();
+ }
+ return _thisVal;
+ }
+
+ public Value getIdentifierValue(Identifier identifier) {
+ throw new UnsupportedOperationException();
+ }
+
+ public float getRandom(Integer seed) {
+ return _rndCtx.getRandom(seed);
+ }
+
+ public Bindings getBindings() {
+ return _bindings;
+ }
+
+ public Object get(String key) {
+ return _bindings.get(key);
+ }
+
+ public void put(String key, Object value) {
+ _bindings.put(key, value);
+ }
+ }
+}