Browse Source

add support for monthname function; implement better string to date/time conversions

git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1202 f203690c-595d-4dc9-a70b-905162fa7fd2
tags/jackcess-2.2.1
James Ahlborn 5 years ago
parent
commit
a314d6501d

+ 1
- 14
src/main/java/com/healthmarketscience/jackcess/expr/EvalContext.java View File

@@ -16,7 +16,6 @@ limitations under the License.

package com.healthmarketscience.jackcess.expr;

import java.text.SimpleDateFormat;
import javax.script.Bindings;

/**
@@ -26,20 +25,8 @@ 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

+ 41
- 0
src/main/java/com/healthmarketscience/jackcess/expr/LocaleContext.java View File

@@ -0,0 +1,41 @@
/*
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);

}

+ 70
- 2
src/main/java/com/healthmarketscience/jackcess/expr/TemporalConfig.java View File

@@ -16,6 +16,9 @@ limitations under the License.

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
@@ -33,7 +36,45 @@ public class TemporalConfig

/** 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;
@@ -42,6 +83,7 @@ public class TemporalConfig
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
@@ -63,7 +105,7 @@ public class TemporalConfig
*/
public TemporalConfig(String dateFormat, String timeFormat12,
String timeFormat24, char dateSeparator,
char timeSeparator)
char timeSeparator, Locale locale)
{
_dateFormat = dateFormat;
_timeFormat12 = timeFormat12;
@@ -72,6 +114,7 @@ public class TemporalConfig
_timeSeparator = timeSeparator;
_dateTimeFormat12 = _dateFormat + " " + _timeFormat12;
_dateTimeFormat24 = _dateFormat + " " + _timeFormat24;
_symbols = DateFormatSymbols.getInstance(locale);
}

public String getDateFormat() {
@@ -113,4 +156,29 @@ public class TemporalConfig
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;
}
}

+ 2
- 2
src/main/java/com/healthmarketscience/jackcess/expr/package-info.java View File

@@ -138,7 +138,7 @@ limitations under the License.
* <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>
@@ -146,7 +146,7 @@ limitations under the License.
* <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>
*

+ 50
- 7
src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultDateFunctions.java View File

@@ -26,6 +26,7 @@ import java.util.Date;
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.*;
@@ -204,6 +205,27 @@ public class DefaultDateFunctions
}
});

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) {
@@ -236,10 +258,7 @@ public class DefaultDateFunctions
}
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);

@@ -280,14 +299,31 @@ public class DefaultDateFunctions
}

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;
}
}
@@ -358,4 +394,11 @@ public class DefaultDateFunctions
}
return firstDay;
}

private static boolean getOptionalBooleanParam(Value[] params, int idx) {
if(params.length > idx) {
return params[idx].getAsBoolean();
}
return false;
}
}

+ 114
- 88
src/main/java/com/healthmarketscience/jackcess/impl/expr/ExpressionTokenizer.java View File

@@ -25,6 +25,7 @@ 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;
@@ -32,9 +33,10 @@ import java.util.Set;
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;


/**
@@ -290,9 +292,33 @@ class ExpressionTokenizer

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;
@@ -306,29 +332,65 @@ class ExpressionTokenizer
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) {
@@ -426,17 +488,29 @@ class ExpressionTokenizer
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) {
@@ -484,69 +558,17 @@ class ExpressionTokenizer
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
@@ -618,6 +640,10 @@ class ExpressionTokenizer
}
}

/**
* 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;

+ 2
- 1
src/main/java/com/healthmarketscience/jackcess/impl/expr/Expressionator.java View File

@@ -41,6 +41,7 @@ import com.healthmarketscience.jackcess.expr.Expression;
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;
@@ -65,7 +66,7 @@ public class Expressionator
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();

+ 1
- 0
src/main/java/com/healthmarketscience/jackcess/impl/expr/StringValue.java View File

@@ -90,6 +90,7 @@ public class StringValue extends BaseValue

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;
}

+ 4
- 4
src/main/java/com/healthmarketscience/jackcess/impl/expr/ValueSupport.java View File

@@ -20,7 +20,7 @@ import java.math.BigDecimal;
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;
@@ -90,7 +90,7 @@ public class ValueSupport
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));
}

@@ -107,7 +107,7 @@ public class ValueSupport
}
}

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;
@@ -124,7 +124,7 @@ public class ValueSupport
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:

+ 25
- 2
src/test/java/com/healthmarketscience/jackcess/PropertyExpressionTest.java View File

@@ -16,7 +16,9 @@ limitations under the License.

package com.healthmarketscience.jackcess;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.script.Bindings;
import javax.script.SimpleBindings;

@@ -87,6 +89,26 @@ public class PropertyExpressionTest extends TestCase

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();
}
}
@@ -274,7 +296,8 @@ public class PropertyExpressionTest extends TestCase
{
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) {
@@ -327,7 +350,7 @@ public class PropertyExpressionTest extends TestCase
}

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);

+ 9
- 0
src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java View File

@@ -232,10 +232,19 @@ public class DefaultFunctionsTest extends TestCase
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#)"));

Loading…
Cancel
Save