Browse Source

add unit tests, fix bugs in custom formats

git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/jdk8@1269 f203690c-595d-4dc9-a70b-905162fa7fd2
tags/jackcess-3.0.0
James Ahlborn 5 years ago
parent
commit
f3c8bb34a4

+ 144
- 49
src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java View File

@@ -17,7 +17,10 @@ limitations under the License.
package com.healthmarketscience.jackcess.impl.expr;

import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
@@ -263,6 +266,7 @@ public class FormatUtil

private static final NumberFormatter.NotationType[] NO_EXP_TYPES =
new NumberFormatter.NotationType[NUM_NF_FMTS];
private static final boolean[] NO_FMT_TYPES = new boolean[NUM_NF_FMTS];


private static final class Args
@@ -552,7 +556,7 @@ public class FormatUtil
private static void addCustomGeneralFormat(String[] fmtStrs, int fmtIdx,
StringBuilder sb)
{
addCustomNumberFormat(fmtStrs, NO_EXP_TYPES, fmtIdx, sb);
addCustomNumberFormat(fmtStrs, NO_EXP_TYPES, NO_FMT_TYPES, fmtIdx, sb);
}

private static Fmt parseCustomDateFormat(ExprBuf buf, Args args) {
@@ -732,6 +736,7 @@ public class FormatUtil
StringBuilder pendingLiteral = new StringBuilder();
NumberFormatter.NotationType[] expTypes =
new NumberFormatter.NotationType[NUM_NF_FMTS];
boolean[] hasFmts = new boolean[NUM_NF_FMTS];

BUF_LOOP:
while(buf.hasNext()) {
@@ -770,13 +775,14 @@ public class FormatUtil
break BUF_LOOP;
}
flushPendingNumberLiteral(pendingLiteral, sb);
addCustomNumberFormat(fmtStrs, expTypes, fmtIdx++, sb);
addCustomNumberFormat(fmtStrs, expTypes, hasFmts, fmtIdx++, sb);
break;
default:
pendingLiteral.append(c);
}
break;
case FCT_NUMBER:
hasFmts[fmtIdx] = true;
switch(c) {
case EXP_E_CHAR:
int signChar = buf.peekNext();
@@ -818,19 +824,19 @@ public class FormatUtil
// fill in remaining formats
while(fmtIdx < NUM_NF_FMTS) {
flushPendingNumberLiteral(pendingLiteral, sb);
addCustomNumberFormat(fmtStrs, expTypes, fmtIdx++, sb);
addCustomNumberFormat(fmtStrs, expTypes, hasFmts, fmtIdx++, sb);
}

return new CustomNumberFmt(
createCustomNumberFormat(fmtStrs, expTypes, NF_POS_IDX, args),
createCustomNumberFormat(fmtStrs, expTypes, NF_NEG_IDX, args),
createCustomNumberFormat(fmtStrs, expTypes, NF_ZERO_IDX, args),
createCustomNumberFormat(fmtStrs, expTypes, NF_NULL_IDX, args));
createCustomNumberFormat(fmtStrs, expTypes, hasFmts, NF_POS_IDX, args),
createCustomNumberFormat(fmtStrs, expTypes, hasFmts, NF_NEG_IDX, args),
createCustomNumberFormat(fmtStrs, expTypes, hasFmts, NF_ZERO_IDX, args),
createCustomNumberFormat(fmtStrs, expTypes, hasFmts, NF_NULL_IDX, args));
}

private static void addCustomNumberFormat(
String[] fmtStrs, NumberFormatter.NotationType[] expTypes,
int fmtIdx, StringBuilder sb)
boolean[] hasFmts, int fmtIdx, StringBuilder sb)
{
if(sb.length() == 0) {
// do special empty format handling on a per-format-type basis
@@ -839,11 +845,13 @@ public class FormatUtil
// re-use "pos" format
sb.append('-').append(fmtStrs[NF_POS_IDX]);
expTypes[NF_NEG_IDX] = expTypes[NF_POS_IDX];
hasFmts[NF_NEG_IDX] = hasFmts[NF_POS_IDX];
break;
case NF_ZERO_IDX:
// re-use "pos" format
sb.append(fmtStrs[NF_POS_IDX]);
expTypes[NF_ZERO_IDX] = expTypes[NF_POS_IDX];
hasFmts[NF_ZERO_IDX] = hasFmts[NF_POS_IDX];
break;
default:
// use empty string result
@@ -886,22 +894,43 @@ public class FormatUtil

private static NumberFormat createCustomNumberFormat(
String[] fmtStrs, NumberFormatter.NotationType[] expTypes,
int fmtIdx, Args args) {
boolean[] hasFmts, int fmtIdx, Args args) {

String fmtStr = fmtStrs[fmtIdx];
if(!hasFmts[fmtIdx]) {
// convert the literal string to a dummy number format
if(fmtStr.length() > 0) {
// strip quoting
StringBuilder sb = new StringBuilder(fmtStr)
.deleteCharAt(fmtStr.length() - 1)
.deleteCharAt(0);
if(sb.length() > 0) {
for(int i = 0; i < sb.length(); ++i) {
if(sb.charAt(i) == SINGLE_QUOTE_CHAR) {
// delete next single quote char
sb.deleteCharAt(++i);
}
}
}
fmtStr = sb.toString();
}
return new LiteralNumberFormat(fmtStr);
}

NumberFormatter.NotationType expType = expTypes[fmtIdx];
NumberFormat nf = args._ctx.createDecimalFormat(fmtStr);

if(fmtIdx == NF_NEG_IDX) {
// force explicit negative format handling
fmtStr = fmtStr + ";" + fmtStr;
DecimalFormat df = (DecimalFormat)nf;
if(df.getMaximumFractionDigits() > 0) {
// if the decimal is included in the format, access always shows it
df.setDecimalSeparatorAlwaysShown(true);
}

NumberFormat df = args._ctx.createDecimalFormat(fmtStr);
if(expType != null) {
df = new NumberFormatter.ScientificFormat(df, expType);
nf = new NumberFormatter.ScientificFormat(nf, expType);
}

return df;
return nf;
}

private static Fmt parseCustomTextFormat(ExprBuf buf, Args args) {
@@ -1088,7 +1117,7 @@ public class FormatUtil

private static String parseColorString(ExprBuf buf) {
return ExpressionTokenizer.parseStringUntil(
buf, END_COLOR_CHAR, START_COLOR_CHAR, false);
buf, START_COLOR_CHAR, END_COLOR_CHAR, false);
}

private static void fillInPartialPrefixes() {
@@ -1369,7 +1398,29 @@ public class FormatUtil
}
}

private static final class CustomNumberFmt implements Fmt
private static abstract class BaseCustomNumberFmt implements Fmt
{
@Override
public Value format(Args args) {
if(args._expr.isNull()) {
return formatNull(args);
}

BigDecimal bd = args.getAsBigDecimal();
int cmp = BigDecimal.ZERO.compareTo(bd);

return ((cmp < 0) ? formatPos(bd, args) :
((cmp > 0) ? formatNeg(bd, args) :
formatZero(bd, args)));
}

protected abstract Value formatNull(Args args);
protected abstract Value formatPos(BigDecimal bd, Args args);
protected abstract Value formatNeg(BigDecimal bd, Args args);
protected abstract Value formatZero(BigDecimal bd, Args args);
}

private static final class CustomNumberFmt extends BaseCustomNumberFmt
{
private final NumberFormat _posFmt;
private final NumberFormat _negFmt;
@@ -1384,36 +1435,41 @@ public class FormatUtil
_nullFmt = nullFmt;
}

@Override
public Value format(Args args) {
if(args._expr.isNull()) {
return ValueSupport.toValue(_nullFmt.format(BigDecimal.ZERO));
private Value formatMaybeZero(BigDecimal bd, NumberFormat fmt) {
// in theory we want to use the given format. however, if, due to
// rounding, we end up with a number equivalent to zero, then we fall
// back to the zero format
int maxDecDigits = fmt.getMaximumFractionDigits();
if(maxDecDigits < bd.scale()) {
bd = bd.setScale(maxDecDigits, NumberFormatter.ROUND_MODE);
}
BigDecimal bd = args.getAsBigDecimal();
int cmp = BigDecimal.ZERO.compareTo(bd);

NumberFormat fmt = null;
if(cmp > 0) {
// in theory we want to use the negative format. however, if, due to
// rounding, we end up with a number equivalent to zero, then we fall
// back to the zero format
fmt = _negFmt;
int maxDecDigits = fmt.getMaximumFractionDigits();
bd = bd.negate().setScale(maxDecDigits, NumberFormatter.ROUND_MODE);
if(BigDecimal.ZERO.equals(bd)) {
// fall back to zero format
fmt = _zeroFmt;
}
} else {
// positive or zero number
fmt = ((cmp < 0) ? _posFmt : _zeroFmt);
if(BigDecimal.ZERO.compareTo(bd) == 0) {
// fall back to zero format
fmt = _zeroFmt;
}

return ValueSupport.toValue(fmt.format(bd));
}

@Override
protected Value formatNull(Args args) {
return ValueSupport.toValue(_nullFmt.format(BigDecimal.ZERO));
}
@Override
protected Value formatPos(BigDecimal bd, Args args) {
return formatMaybeZero(bd, _posFmt);
}
@Override
protected Value formatNeg(BigDecimal bd, Args args) {
return formatMaybeZero(bd.negate(), _negFmt);
}
@Override
protected Value formatZero(BigDecimal bd, Args args) {
return ValueSupport.toValue(_zeroFmt.format(bd));
}
}

private static final class CustomGeneralFmt implements Fmt
private static final class CustomGeneralFmt extends BaseCustomNumberFmt
{
private final Value _posVal;
private final Value _negVal;
@@ -1429,17 +1485,56 @@ public class FormatUtil
}

@Override
public Value format(Args args) {
if(args._expr.isNull()) {
return _nullVal;
}
BigDecimal bd = args.getAsBigDecimal();
int cmp = BigDecimal.ZERO.compareTo(bd);

return ((cmp < 0) ? _posVal :
((cmp > 0) ? _negVal : _zeroVal));
protected Value formatNull(Args args) {
return _nullVal;
}
@Override
protected Value formatPos(BigDecimal bd, Args args) {
return _posVal;
}
@Override
protected Value formatNeg(BigDecimal bd, Args args) {
return _negVal;
}
@Override
protected Value formatZero(BigDecimal bd, Args args) {
return _zeroVal;
}
}

private static final class LiteralNumberFormat extends NumberFormat
{
private static final long serialVersionUID = 0L;

private final String _str;

private LiteralNumberFormat(String str) {
_str = str;
}

@Override
public StringBuffer format(Object number, StringBuffer toAppendTo,
FieldPosition pos)
{
return toAppendTo.append(_str);
}

@Override
public StringBuffer format(double number, StringBuffer toAppendTo,
FieldPosition pos) {
throw new UnsupportedOperationException();
}

@Override
public Number parse(String source, ParsePosition parsePosition) {
throw new UnsupportedOperationException();
}

@Override
public StringBuffer format(long number, StringBuffer toAppendTo,
FieldPosition pos) {
throw new UnsupportedOperationException();
}
}

}

+ 182
- 12
src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java View File

@@ -21,6 +21,7 @@ import java.time.LocalDateTime;
import java.util.Calendar;

import com.healthmarketscience.jackcess.expr.EvalException;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import static com.healthmarketscience.jackcess.impl.expr.ExpressionatorTest.eval;
import static com.healthmarketscience.jackcess.impl.expr.ExpressionatorTest.toBD;
@@ -251,6 +252,7 @@ public class DefaultFunctionsTest extends TestCase
assertEval("-.123%", "=FormatPercent(-0.0012345,3,False)");

assertEval("$12,345.00", "=FormatCurrency(12345)");
assertEval("($12,345.00)", "=FormatCurrency(-12345)");
assertEval("-$12.34", "=FormatCurrency(-12.345,-1,True,False)");
assertEval("$12", "=FormatCurrency(12.345,0,True,True)");
assertEval("($.123)", "=FormatCurrency(-0.12345,3,False)");
@@ -317,6 +319,7 @@ public class DefaultFunctionsTest extends TestCase
assertEval("7:00:00 AM", "=Format(#7:00:00 AM#, 'General Date')");
assertEval("1/2/2003 7:00:00 AM", "=Format('37623.2916666667', 'General Date')");
assertEval("foo", "=Format('foo', 'General Date')");
assertEval("", "=Format('', 'General Date')");

assertEval("Thursday, January 02, 2003", "=Format(#01/02/2003 7:00:00 AM#, 'Long Date')");
assertEval("02-Jan-03", "=Format(#01/02/2003 7:00:00 AM#, 'Medium Date')");
@@ -340,22 +343,189 @@ public class DefaultFunctionsTest extends TestCase
assertEval("4 7:13:00 AM ttt this 'is' \"text\"",
"=Format(#7:13:00 AM#, \"q c ttt \"\"this 'is' \"\"\"\"text\"\"\"\"\"\"\")");
assertEval("12/29/1899", "=Format('true', 'c')");
assertEval("Tuesday, 00 Jan 2, 21:36:00",
"=Format('3.9', 'dddd, yy mmm d, hh:nn:ss')");
assertEval("Tuesday, 00 Jan 01 2, 09:36:00 PM",
"=Format('3.9', 'dddd, yy mmm mm d, hh:nn:ss AMPM')");
assertEval("Tuesday, 00 Jan 2, 21:36:00 Y",
"=Format('3.9', '*~dddd, yy mmm d, hh:nn:ss \\Y[Yellow]')");
assertEval("Tuesday, 00 Jan 01/2, 09:36:00 PM",
"=Format('3.9', 'dddd, yy mmm mm/d, hh:nn:ss AMPM')");
assertEval("foo",
"=Format('foo', 'dddd, yy mmm mm d, hh:nn:ss AMPM')");

assertEval("p13.00blah",
"=Format('13', '\"p\"#.00#\"blah\"')");
assertEval("-p13.00blah",
"=Format('-13', '\"p\"#.00#\"blah\";(\"p\"#.00#\"blah\")')");
// assertEval("-p13.00blah",
// "=Format('-13', '\"p\"#.00#\"blah\"')");


assertEvalFormat("';\\y;\\n'",
"foo", "'foo'",
"", "''",
"y", "True",
"n", "'0'",
"", "Null");

assertEvalFormat("';\"y\";!\\n;*~\\z[Blue];'",
"foo", "'foo'",
"", "''",
"y", "True",
"n", "'0'",
"z", "Null");

assertEvalFormat("'\"p\"#.00#\"blah\"'",
"p13.00blah", "13",
"-p13.00blah", "-13",
"p.00blah", "0",
"", "''",
"", "Null");

assertEvalFormat("'\"p\"#.00#\"blah\";(\"p\"#.00#\"blah\")'",
"p13.00blah", "13",
"(p13.00blah)", "-13",
"p.00blah", "0",
"(p1.00blah)", "True",
"p.00blah", "'false'",
"p37623.292blah", "#01/02/2003 7:00:00 AM#",
"p37623.292blah", "'01/02/2003 7:00:00 AM'",
"NotANumber", "'NotANumber'",
"", "''",
"", "Null");

assertEvalFormat("'\"p\"#.00#\"blah\";!(\"p\"#.00#\"blah\")[Red];\"zero\"'",
"p13.00blah", "13",
"(p13.00blah)", "-13",
"zero", "0",
"", "''",
"", "Null");

assertEvalFormat("'\"p\"#.00#\"blah\";(\"p\"#.00#\"blah\");\"zero\";\"yuck\"'",
"p13.00blah", "13",
"(p13.00blah)", "-13",
"zero", "0",
"", "''",
"yuck", "Null");

assertEvalFormat("'0.##;(0.###);\"zero\";\"yuck\"'",
"0.03", "0.03",
"zero", "0.003",
"(0.003)", "-0.003",
"zero", "-0.0003");

// FIXME, need to handle rounding w/ negatives
// FIXME, need to handle dangling decimal
// assertEvalFormat("'0.##;(0.###E+0)'",
// "0.03", "0.03",
// "(0.003)", "-0.0003",

assertEvalFormat("'0.'",
"13.", "13",
"0.", "0.003",
"-45.", "-45",
"0.", "-0.003",
"0.", "0"
);

assertEvalFormat("'0.#'",
"13.", "13",
"0.3", "0.3",
"0.", "0.003",
"-45.", "-45",
"0.", "-0.003",
"0.", "0"
);

assertEvalFormat("'0'",
"13", "13",
"0", "0.003",
"-45", "-45",
"0", "-0.003",
"0", "0"
);

assertEvalFormat("'#'",
"13", "13",
"0", "0.003",
"-45", "-45",
"0", "-0.003"
// FIXME
// "", "0"
);

assertEvalFormat("'$0.0#'",
"$213.0", "213");

assertEvalFormat("'@'",
"foo", "'foo'",
"-13", "-13",
"0", "0",
"", "''",
"", "Null");

assertEvalFormat("'>@'",
"FOO", "'foo'",
"-13", "-13",
"0", "0",
"", "''",
"", "Null");

assertEvalFormat("'<@'",
"foo", "'FOO'",
"-13", "-13",
"0", "0",
"", "''",
"", "Null");

assertEvalFormat("'!>@'",
"O", "'foo'",
"3", "-13",
"0", "0",
"", "''",
"", "Null");

assertEvalFormat("'!>@[Red];\"empty\"'",
"O", "'foo'",
"3", "-13",
"0", "0",
"empty", "''",
"empty", "Null");

assertEvalFormat("'><@'",
"fOo", "'fOo'");

assertEvalFormat("'\\x@@@&&&\\y'",
"x fy", "'f'",
"x fooy", "'foo'",
"x foobay", "'fooba'",
"xfoobarybaz", "'foobarbaz'"
);

assertEvalFormat("'!\\x@@@&&&\\y'",
"xf y", "'f'",
"xfooy", "'foo'",
"xfoobay", "'fooba'",
"xbarbazy", "'foobarbaz'"
);

assertEvalFormat("'\\x&&&@@@\\y'",
"x fy", "'f'",
"xfooy", "'foo'",
"xfoobay", "'fooba'",
"xfoobarybaz", "'foobarbaz'"
);

assertEvalFormat("'!\\x&&&@@@\\y'",
"xf y", "'f'",
"xfoo y", "'foo'",
"xfooba y", "'fooba'",
"xbarbazy", "'foobarbaz'"
);
}

private static void assertEvalFormat(String fmtStr, String... testStrs) {
for(int i = 0; i < testStrs.length; i+=2) {
String expected = testStrs[i];
String val = testStrs[i + 1];

try {
assertEval(expected,
"=Format(" + val + ", " + fmtStr + ")");
} catch(AssertionFailedError afe) {
throw new AssertionFailedError("Input " + val + ": " +
afe.getMessage());
}
}
}

public void testNumberFuncs() throws Exception

Loading…
Cancel
Save