package com.healthmarketscience.jackcess.expr;
-import java.text.SimpleDateFormat;
import javax.script.Bindings;
/**
*
* @author James Ahlborn
*/
-public interface EvalContext
+public interface EvalContext extends LocaleContext
{
- /**
- * @return the currently configured TemporalConfig (from the
- * {@link EvalConfig})
- */
- public TemporalConfig getTemporalConfig();
-
- /**
- * @return an appropriately configured (i.e. TimeZone and other date/time
- * flags) SimpleDateFormat for the given format.
- */
- public SimpleDateFormat createDateFormat(String formatStr);
-
/**
* @param seed the seed for the random value, following the rules for the
* "Rnd" function
--- /dev/null
+/*
+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.expr;
+
+import java.text.SimpleDateFormat;
+
+/**
+ * LocaleContext encapsulates all shared localization state for expression
+ * parsing and evaluation.
+ *
+ * @author James Ahlborn
+ */
+public interface LocaleContext
+{
+ /**
+ * @return the currently configured TemporalConfig (from the
+ * {@link EvalConfig})
+ */
+ public TemporalConfig getTemporalConfig();
+
+ /**
+ * @return an appropriately configured (i.e. TimeZone and other date/time
+ * flags) SimpleDateFormat for the given format.
+ */
+ public SimpleDateFormat createDateFormat(String formatStr);
+
+}
package com.healthmarketscience.jackcess.expr;
+import java.text.DateFormatSymbols;
+import java.util.Locale;
+
/**
* A TemporalConfig encapsulates date/time formatting options for expression
* evaluation. The default {@link #US_TEMPORAL_CONFIG} instance provides US
/** default implementation which is configured for the US locale */
public static final TemporalConfig US_TEMPORAL_CONFIG = new TemporalConfig(
- US_DATE_FORMAT, US_TIME_FORMAT_12, US_TIME_FORMAT_24, '/', ':');
+ US_DATE_FORMAT, US_TIME_FORMAT_12, US_TIME_FORMAT_24, '/', ':', Locale.US);
+
+ public enum Type {
+ DATE, TIME, DATE_TIME, TIME_12, TIME_24, DATE_TIME_12, DATE_TIME_24;
+
+ public Type getDefaultType() {
+ switch(this) {
+ case DATE:
+ return DATE;
+ case TIME:
+ case TIME_12:
+ case TIME_24:
+ return TIME;
+ case DATE_TIME:
+ case DATE_TIME_12:
+ case DATE_TIME_24:
+ return DATE_TIME;
+ default:
+ throw new RuntimeException("invalid type " + this);
+ }
+ }
+
+ public Value.Type getValueType() {
+ switch(this) {
+ case DATE:
+ return Value.Type.DATE;
+ case TIME:
+ case TIME_12:
+ case TIME_24:
+ return Value.Type.TIME;
+ case DATE_TIME:
+ case DATE_TIME_12:
+ case DATE_TIME_24:
+ return Value.Type.DATE_TIME;
+ default:
+ throw new RuntimeException("invalid type " + this);
+ }
+ }
+ }
private final String _dateFormat;
private final String _timeFormat12;
private final char _timeSeparator;
private final String _dateTimeFormat12;
private final String _dateTimeFormat24;
+ private final DateFormatSymbols _symbols;
/**
* Instantiates a new TemporalConfig with the given configuration. Note
*/
public TemporalConfig(String dateFormat, String timeFormat12,
String timeFormat24, char dateSeparator,
- char timeSeparator)
+ char timeSeparator, Locale locale)
{
_dateFormat = dateFormat;
_timeFormat12 = timeFormat12;
_timeSeparator = timeSeparator;
_dateTimeFormat12 = _dateFormat + " " + _timeFormat12;
_dateTimeFormat24 = _dateFormat + " " + _timeFormat24;
+ _symbols = DateFormatSymbols.getInstance(locale);
}
public String getDateFormat() {
public char getTimeSeparator() {
return _timeSeparator;
}
+
+ public String getDateTimeFormat(Type type) {
+ switch(type) {
+ case DATE:
+ return getDefaultDateFormat();
+ case TIME:
+ return getDefaultTimeFormat();
+ case DATE_TIME:
+ return getDefaultDateTimeFormat();
+ case TIME_12:
+ return getTimeFormat12();
+ case TIME_24:
+ return getTimeFormat24();
+ case DATE_TIME_12:
+ return getDateTimeFormat12();
+ case DATE_TIME_24:
+ return getDateTimeFormat24();
+ default:
+ throw new IllegalArgumentException("unknown date/time type " + type);
+ }
+ }
+
+ public DateFormatSymbols getDateFormatSymbols() {
+ return _symbols;
+ }
}
* <tr class="TableRowColor"><td>Hour</td><td>Y</td></tr>
* <tr class="TableRowColor"><td>Minute</td><td>Y</td></tr>
* <tr class="TableRowColor"><td>Month</td><td>Y</td></tr>
- * <tr class="TableRowColor"><td>MonthName</td><td></td></tr>
+ * <tr class="TableRowColor"><td>MonthName</td><td>Y</td></tr>
* <tr class="TableRowColor"><td>Now</td><td>Y</td></tr>
* <tr class="TableRowColor"><td>Second</td><td>Y</td></tr>
* <tr class="TableRowColor"><td>Time</td><td>Y</td></tr>
* <tr class="TableRowColor"><td>TimeSerial</td><td>Y</td></tr>
* <tr class="TableRowColor"><td>TimeValue</td><td>Y</td></tr>
* <tr class="TableRowColor"><td>Weekday</td><td>Y</td></tr>
- * <tr class="TableRowColor"><td>WeekdayName</td><td></td></tr>
+ * <tr class="TableRowColor"><td>WeekdayName</td><td>Y</td></tr>
* <tr class="TableRowColor"><td>Year</td><td>Y</td></tr>
* </table>
*
import com.healthmarketscience.jackcess.expr.EvalContext;
import com.healthmarketscience.jackcess.expr.EvalException;
import com.healthmarketscience.jackcess.expr.Function;
+import com.healthmarketscience.jackcess.expr.TemporalConfig;
import com.healthmarketscience.jackcess.expr.Value;
import com.healthmarketscience.jackcess.impl.ColumnImpl;
import static com.healthmarketscience.jackcess.impl.expr.DefaultFunctions.*;
}
});
+ public static final Function MONTHNAME = registerFunc(new FuncVar("MonthName", 1, 2) {
+ @Override
+ protected Value evalVar(EvalContext ctx, Value[] params) {
+ Value param1 = params[0];
+ if(param1 == null) {
+ return null;
+ }
+ // convert from 1 based to 0 based value
+ int month = param1.getAsLongInt() - 1;
+
+ boolean abbreviate = getOptionalBooleanParam(params, 1);
+
+ DateFormatSymbols syms = ctx.createDateFormat(
+ ctx.getTemporalConfig().getDateFormat()).getDateFormatSymbols();
+ String[] monthNames = (abbreviate ?
+ syms.getShortMonths() : syms.getMonths());
+ // note, the array is 1 based
+ return ValueSupport.toValue(monthNames[month]);
+ }
+ });
+
public static final Function DAY = registerFunc(new Func1NullIsNull("Day") {
@Override
protected Value eval1(EvalContext ctx, Value param1) {
}
int weekday = param1.getAsLongInt();
- boolean abbreviate = false;
- if(params.length > 1) {
- abbreviate = params[1].getAsBoolean();
- }
+ boolean abbreviate = getOptionalBooleanParam(params, 1);
int firstDay = getFirstDayParam(params, 2);
}
if(type == Value.Type.STRING) {
- // see if we can coerce to date/time
- // FIXME use ExpressionatorTokenizer to detect explicit date/time format
+ // see if we can coerce to date/time or double
+ String valStr = param.getAsString();
+ TemporalConfig.Type valTempType = ExpressionTokenizer.determineDateType(
+ valStr, ctx);
+
+ if(valTempType != null) {
+
+ try {
+ DateFormat parseDf = ExpressionTokenizer.createParseDateFormat(
+ valTempType, ctx);
+ Date dateVal = ExpressionTokenizer.parseComplete(parseDf, valStr);
+ return ValueSupport.toValue(ctx, valTempType.getValueType(),
+ dateVal);
+ } catch(java.text.ParseException pe) {
+ // not a valid date string, not a date/time
+ return null;
+ }
+ }
+ // see if string can be coerced to number
try {
return numberToDateValue(ctx, param.getAsDouble());
} catch(NumberFormatException ignored) {
- // not a number
+ // not a number, not a date/time
return null;
}
}
}
return firstDay;
}
+
+ private static boolean getOptionalBooleanParam(Value[] params, int idx) {
+ if(params.length > idx) {
+ return params[idx].getAsBoolean();
+ }
+ return false;
+ }
}
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.TimeZone;
import static com.healthmarketscience.jackcess.impl.expr.Expressionator.*;
-import com.healthmarketscience.jackcess.expr.Value;
-import com.healthmarketscience.jackcess.expr.TemporalConfig;
+import com.healthmarketscience.jackcess.expr.LocaleContext;
import com.healthmarketscience.jackcess.expr.ParseException;
+import com.healthmarketscience.jackcess.expr.TemporalConfig;
+import com.healthmarketscience.jackcess.expr.Value;
/**
private static Token parseDateLiteral(ExprBuf buf)
{
- TemporalConfig cfg = buf.getTemporalConfig();
String dateStr = parseDateLiteralString(buf);
+ TemporalConfig.Type type = determineDateType(
+ dateStr, buf.getContext());
+ if(type == null) {
+ throw new ParseException("Invalid date/time literal " + dateStr +
+ " " + buf);
+ }
+
+ // note that although we may parse in the time "24" format, we will
+ // display as the default time format
+ DateFormat parseDf = buf.getDateTimeFormat(type);
+ DateFormat df = buf.getDateTimeFormat(type.getDefaultType());
+
+ try {
+ return new Token(TokenType.LITERAL, parseComplete(parseDf, dateStr),
+ dateStr, type.getValueType(), df);
+ } catch(java.text.ParseException pe) {
+ throw new ParseException(
+ "Invalid date/time literal " + dateStr + " " + buf, pe);
+ }
+ }
+
+ static TemporalConfig.Type determineDateType(
+ String dateStr, LocaleContext ctx)
+ {
+ TemporalConfig cfg = ctx.getTemporalConfig();
boolean hasDate = (dateStr.indexOf(cfg.getDateSeparator()) >= 0);
boolean hasTime = (dateStr.indexOf(cfg.getTimeSeparator()) >= 0);
boolean hasAmPm = false;
PM_SUFFIX, 0, AMPM_SUFFIX_LEN)));
}
- DateFormat sdf = null;
- Value.Type valType = null;
- if(hasDate && hasTime) {
- sdf = (hasAmPm ? buf.getDateTimeFormat12() : buf.getDateTimeFormat24());
- valType = Value.Type.DATE_TIME;
- } else if(hasDate) {
- sdf = buf.getDateFormat();
- valType = Value.Type.DATE;
+ if(hasDate) {
+ if(hasTime) {
+ return (hasAmPm ? TemporalConfig.Type.DATE_TIME_12 :
+ TemporalConfig.Type.DATE_TIME_24);
+ }
+ return TemporalConfig.Type.DATE;
} else if(hasTime) {
- sdf = (hasAmPm ? buf.getTimeFormat12() : buf.getTimeFormat24());
- valType = Value.Type.TIME;
- } else {
- throw new ParseException("Invalid date time literal " + dateStr +
- " " + buf);
+ return (hasAmPm ? TemporalConfig.Type.TIME_12 :
+ TemporalConfig.Type.TIME_24);
}
+ return null;
+ }
- try {
- return new Token(TokenType.LITERAL, sdf.parse(dateStr), dateStr, valType,
- sdf);
- } catch(java.text.ParseException pe) {
- throw new ParseException(
- "Invalid date time literal " + dateStr + " " + buf, pe);
+ static DateFormat createParseDateFormat(TemporalConfig.Type type,
+ LocaleContext ctx)
+ {
+ TemporalConfig cfg = ctx.getTemporalConfig();
+ DateFormat df = ctx.createDateFormat(cfg.getDateTimeFormat(type));
+
+ TemporalConfig.Type parseType = null;
+ switch(type) {
+ case TIME:
+ parseType = TemporalConfig.Type.DATE_TIME;
+ break;
+ case TIME_12:
+ parseType = TemporalConfig.Type.DATE_TIME_12;
+ break;
+ case TIME_24:
+ parseType = TemporalConfig.Type.DATE_TIME_24;
+ break;
+ default:
+ }
+
+ if(parseType != null) {
+ // we need to use a special DateFormat impl which handles parsing
+ // separately from formatting
+ String baseDate = getBaseDatePrefix(ctx);
+ DateFormat parseDf = ctx.createDateFormat(
+ cfg.getDateTimeFormat(parseType));
+ df = new TimeFormat(parseDf, df, baseDate);
+ }
+
+ return df;
+ }
+
+ private static String getBaseDatePrefix(LocaleContext ctx) {
+ String dateFmt = ctx.getTemporalConfig().getDateFormat();
+ String baseDate = BASE_DATE;
+ if(!BASE_DATE_FMT.equals(dateFmt)) {
+ try {
+ // need to reformat the base date to the relevant date format
+ DateFormat parseDf = ctx.createDateFormat(BASE_DATE_FMT);
+ DateFormat df = ctx.createDateFormat(dateFmt);
+ baseDate = df.format(parseComplete(parseDf, baseDate));
+ } catch(Exception e) {
+ throw new ParseException("Could not parse base date", e);
+ }
}
+ return baseDate + " ";
}
private static Token maybeParseNumberLiteral(char firstChar, ExprBuf buf) {
return new AbstractMap.SimpleImmutableEntry<K,V>(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 DateFormat _dateFmt;
- private DateFormat _timeFmt12;
- private DateFormat _dateTimeFmt12;
- private DateFormat _timeFmt24;
- private DateFormat _dateTimeFmt24;
- private String _baseDate;
+ private final Map<TemporalConfig.Type,DateFormat> _dateTimeFmts =
+ new EnumMap<TemporalConfig.Type,DateFormat>(TemporalConfig.Type.class);
private final StringBuilder _scratch = new StringBuilder();
private ExprBuf(String str, ParseContext ctx) {
return _scratch;
}
- public TemporalConfig getTemporalConfig() {
- return _ctx.getTemporalConfig();
+ public ParseContext getContext() {
+ return _ctx;
}
- public DateFormat getDateFormat() {
- if(_dateFmt == null) {
- _dateFmt = _ctx.createDateFormat(getTemporalConfig().getDateFormat());
+ public DateFormat getDateTimeFormat(TemporalConfig.Type type) {
+ DateFormat df = _dateTimeFmts.get(type);
+ if(df == null) {
+ df = createParseDateFormat(type, _ctx);
+ _dateTimeFmts.put(type, df);
}
- return _dateFmt;
- }
-
- public DateFormat getTimeFormat12() {
- if(_timeFmt12 == null) {
- _timeFmt12 = new TimeFormat(
- getDateTimeFormat12(), _ctx.createDateFormat(
- getTemporalConfig().getTimeFormat12()),
- getBaseDate());
- }
- return _timeFmt12;
- }
-
- public DateFormat getDateTimeFormat12() {
- if(_dateTimeFmt12 == null) {
- _dateTimeFmt12 = _ctx.createDateFormat(
- getTemporalConfig().getDateTimeFormat12());
- }
- return _dateTimeFmt12;
- }
-
- public DateFormat getTimeFormat24() {
- if(_timeFmt24 == null) {
- _timeFmt24 = new TimeFormat(
- getDateTimeFormat24(), _ctx.createDateFormat(
- getTemporalConfig().getTimeFormat24()),
- getBaseDate());
- }
- return _timeFmt24;
- }
-
- public DateFormat getDateTimeFormat24() {
- if(_dateTimeFmt24 == null) {
- _dateTimeFmt24 = _ctx.createDateFormat(
- getTemporalConfig().getDateTimeFormat24());
- }
- return _dateTimeFmt24;
- }
-
- private String getBaseDate() {
- if(_baseDate == null) {
- String dateFmt = getTemporalConfig().getDateFormat();
- String baseDate = BASE_DATE;
- if(!BASE_DATE_FMT.equals(dateFmt)) {
- try {
- // need to reformat the base date to the relevant date format
- DateFormat df = _ctx.createDateFormat(BASE_DATE_FMT);
- baseDate = getDateFormat().format(df.parse(baseDate));
- } catch(Exception e) {
- throw new ParseException("Could not parse base date", e);
- }
- }
- _baseDate = baseDate + " ";
- }
- return _baseDate;
+ return df;
}
@Override
}
}
+ /**
+ * Special date/time format which will parse time-only strings "correctly"
+ * according to how access handles time-only values.
+ */
private static final class TimeFormat extends DateFormat
{
private static final long serialVersionUID = 0L;
import com.healthmarketscience.jackcess.expr.Function;
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;
DEFAULT_VALUE, EXPRESSION, FIELD_VALIDATOR, RECORD_VALIDATOR;
}
- public interface ParseContext {
+ public interface ParseContext extends LocaleContext {
public TemporalConfig getTemporalConfig();
public SimpleDateFormat createDateFormat(String formatStr);
public FunctionLookup getFunctionLookup();
if(tmpVal.charAt(0) != NUMBER_BASE_PREFIX) {
// parse using standard numeric support
+ // FIXME, this should handle grouping separator, but needs ctx
_num = ValueSupport.normalize(new BigDecimal(tmpVal));
return (BigDecimal)_num;
}
import java.text.DateFormat;
import java.util.Date;
-import com.healthmarketscience.jackcess.expr.EvalContext;
+import com.healthmarketscience.jackcess.expr.LocaleContext;
import com.healthmarketscience.jackcess.expr.EvalException;
import com.healthmarketscience.jackcess.expr.Value;
import com.healthmarketscience.jackcess.impl.ColumnImpl;
dd, fmt.getCalendar())), fmt);
}
- public static Value toValue(EvalContext ctx, Value.Type type, Date d) {
+ public static Value toValue(LocaleContext ctx, Value.Type type, Date d) {
return toValue(type, d, getDateFormatForType(ctx, type));
}
}
}
- static Value toDateValue(EvalContext ctx, Value.Type type, double v,
+ static Value toDateValue(LocaleContext ctx, Value.Type type, double v,
Value param1, Value param2)
{
DateFormat fmt = null;
return toValue(type, d, fmt);
}
- static DateFormat getDateFormatForType(EvalContext ctx, Value.Type type) {
+ static DateFormat getDateFormatForType(LocaleContext ctx, Value.Type type) {
String fmtStr = null;
switch(type) {
case DATE:
package com.healthmarketscience.jackcess;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import javax.script.Bindings;
import javax.script.SimpleBindings;
assertTable(expectedRows, t);
+ setProp(t, "data2", PropertyMap.REQUIRED_PROP, true);
+
+ t.addRow(Column.AUTO_NUMBER, "blah", 13);
+ t.addRow(Column.AUTO_NUMBER, "blah", null);
+
+ expectedRows = new ArrayList<Row>(expectedRows);
+ expectedRows.add(
+ createExpectedRow(
+ "id", 4,
+ "data1", "blah",
+ "data2", 13));
+ expectedRows.add(
+ createExpectedRow(
+ "id", 5,
+ "data1", "blah",
+ "data2", 42));
+
+ assertTable(expectedRows, t);
+
+
db.close();
}
}
{
TemporalConfig tempConf = new TemporalConfig("yyyy/M/d",
"hh.mm.ss a",
- "HH.mm.ss", '/', '.');
+ "HH.mm.ss", '/', '.',
+ Locale.US);
FunctionLookup lookup = new FunctionLookup() {
public Function getFunction(String name) {
}
private static void setProp(Table t, String colName, String propName,
- String propVal) throws Exception {
+ Object propVal) throws Exception {
PropertyMap props = t.getColumn(colName).getProperties();
if(propVal != null) {
props.put(propName, propVal);
assertEquals("1/2/2003", eval("=CStr(DateValue(#01/02/2003 7:00:00 AM#))"));
assertEquals("7:00:00 AM", eval("=CStr(TimeValue(#01/02/2003 7:00:00 AM#))"));
+ assertEquals("1:10:00 PM", eval("=CStr(#13:10:00#)"));
+
assertEquals(2003, eval("=Year(#01/02/2003 7:00:00 AM#)"));
assertEquals(1, eval("=Month(#01/02/2003 7:00:00 AM#)"));
assertEquals(2, eval("=Day(#01/02/2003 7:00:00 AM#)"));
+ assertEquals(2003, eval("=Year('01/02/2003 7:00:00 AM')"));
+ assertEquals(1899, eval("=Year(#7:00:00 AM#)"));
+
+ assertEquals("January", eval("=MonthName(1)"));
+ assertEquals("Feb", eval("=MonthName(2,True)"));
+ assertEquals("March", eval("=MonthName(3,False)"));
+
assertEquals(7, eval("=Hour(#01/02/2003 7:10:27 AM#)"));
assertEquals(19, eval("=Hour(#01/02/2003 7:10:27 PM#)"));
assertEquals(10, eval("=Minute(#01/02/2003 7:10:27 AM#)"));