git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1226 f203690c-595d-4dc9-a70b-905162fa7fd2tags/jackcess-2.2.1
@@ -18,6 +18,7 @@ package com.healthmarketscience.jackcess.expr; | |||
import java.text.DecimalFormatSymbols; | |||
import java.util.Locale; | |||
import com.healthmarketscience.jackcess.impl.expr.FormatUtil; | |||
/** | |||
* A NumericConfig encapsulates number formatting options for expression | |||
@@ -33,12 +34,21 @@ public class NumericConfig | |||
public static final NumericConfig US_NUMERIC_CONFIG = new NumericConfig( | |||
2, true, false, true, 3, Locale.US); | |||
public enum Type { | |||
CURRENCY, FIXED, STANDARD, PERCENT, SCIENTIFIC; | |||
} | |||
private final int _numDecDigits; | |||
private final boolean _incLeadingDigit; | |||
private final boolean _useNegParens; | |||
private final boolean _useNegCurrencyParens; | |||
private final int _numGroupDigits; | |||
private final DecimalFormatSymbols _symbols; | |||
private final String _currencyFormat; | |||
private final String _fixedFormat; | |||
private final String _standardFormat; | |||
private final String _percentFormat; | |||
private final String _scientificFormat; | |||
public NumericConfig(int numDecDigits, boolean incLeadingDigit, | |||
boolean useNegParens, boolean useNegCurrencyParens, | |||
@@ -49,6 +59,22 @@ public class NumericConfig | |||
_useNegCurrencyParens = useNegCurrencyParens; | |||
_numGroupDigits = numGroupDigits; | |||
_symbols = DecimalFormatSymbols.getInstance(locale); | |||
_currencyFormat = FormatUtil.createNumberFormatPattern( | |||
FormatUtil.NumPatternType.CURRENCY, _numDecDigits, _incLeadingDigit, | |||
_useNegCurrencyParens, _numGroupDigits); | |||
_fixedFormat = FormatUtil.createNumberFormatPattern( | |||
FormatUtil.NumPatternType.GENERAL, _numDecDigits, true, | |||
_useNegParens, 0); | |||
_standardFormat = FormatUtil.createNumberFormatPattern( | |||
FormatUtil.NumPatternType.GENERAL, _numDecDigits, _incLeadingDigit, | |||
_useNegParens, _numGroupDigits); | |||
_percentFormat = FormatUtil.createNumberFormatPattern( | |||
FormatUtil.NumPatternType.PERCENT, _numDecDigits, _incLeadingDigit, | |||
_useNegParens, 0); | |||
_scientificFormat = FormatUtil.createNumberFormatPattern( | |||
FormatUtil.NumPatternType.SCIENTIFIC, _numDecDigits, true, | |||
false, 0); | |||
} | |||
public int getNumDecimalDigits() { | |||
@@ -71,6 +97,23 @@ public class NumericConfig | |||
return _numGroupDigits; | |||
} | |||
public String getNumberFormat(Type type) { | |||
switch(type) { | |||
case CURRENCY: | |||
return _currencyFormat; | |||
case FIXED: | |||
return _fixedFormat; | |||
case STANDARD: | |||
return _standardFormat; | |||
case PERCENT: | |||
return _percentFormat; | |||
case SCIENTIFIC: | |||
return _scientificFormat; | |||
default: | |||
throw new IllegalArgumentException("unknown number type " + type); | |||
} | |||
} | |||
public DecimalFormatSymbols getDecimalFormatSymbols() { | |||
return _symbols; | |||
} |
@@ -35,6 +35,10 @@ public interface Value | |||
{ | |||
NULL, STRING, DATE, TIME, DATE_TIME, LONG, DOUBLE, BIG_DEC; | |||
public boolean isString() { | |||
return (this == STRING); | |||
} | |||
public boolean isNumeric() { | |||
return inRange(LONG, BIG_DEC); | |||
} |
@@ -29,6 +29,7 @@ import com.healthmarketscience.jackcess.expr.NumericConfig; | |||
import com.healthmarketscience.jackcess.expr.TemporalConfig; | |||
import com.healthmarketscience.jackcess.impl.expr.DefaultFunctions; | |||
import com.healthmarketscience.jackcess.impl.expr.Expressionator; | |||
import com.healthmarketscience.jackcess.impl.expr.NumberFormatter; | |||
import com.healthmarketscience.jackcess.impl.expr.RandomContext; | |||
/** | |||
@@ -112,11 +113,12 @@ public class DBEvalContext implements Expressionator.ParseContext, EvalConfig | |||
DecimalFormat df = _dfs.get(formatStr); | |||
if(df == null) { | |||
df = new DecimalFormat(formatStr, _numeric.getDecimalFormatSymbols()); | |||
df.setRoundingMode(NumberFormatter.ROUND_MODE); | |||
_dfs.put(formatStr, df); | |||
} | |||
return df; | |||
} | |||
public float getRandom(Integer seed) { | |||
return _rndCtx.getRandom(seed); | |||
} |
@@ -22,7 +22,6 @@ import java.util.Date; | |||
import com.healthmarketscience.jackcess.expr.Value; | |||
import com.healthmarketscience.jackcess.expr.EvalException; | |||
import com.healthmarketscience.jackcess.expr.LocaleContext; | |||
import com.healthmarketscience.jackcess.impl.NumberFormatter; | |||
/** | |||
* |
@@ -19,7 +19,6 @@ package com.healthmarketscience.jackcess.impl.expr; | |||
import java.math.BigDecimal; | |||
import com.healthmarketscience.jackcess.expr.LocaleContext; | |||
import com.healthmarketscience.jackcess.impl.NumberFormatter; | |||
/** | |||
* |
@@ -22,7 +22,6 @@ import java.util.regex.Pattern; | |||
import com.healthmarketscience.jackcess.expr.EvalException; | |||
import com.healthmarketscience.jackcess.expr.LocaleContext; | |||
import com.healthmarketscience.jackcess.expr.Value; | |||
import com.healthmarketscience.jackcess.impl.NumberFormatter; | |||
import static com.healthmarketscience.jackcess.impl.expr.ValueSupport.*; | |||
@@ -222,7 +221,7 @@ public class BuiltinOperators | |||
Value.Type mathType = getMathTypePrecedence(ctx, param1, param2, | |||
CoercionType.GENERAL); | |||
if(mathType == Value.Type.STRING) { | |||
if(mathType.isString()) { | |||
throw new EvalException("Unexpected type " + mathType); | |||
} | |||
return toValue(param1.getAsLongInt(ctx) / param2.getAsLongInt(ctx)); | |||
@@ -270,7 +269,7 @@ public class BuiltinOperators | |||
Value.Type mathType = getMathTypePrecedence(ctx, param1, param2, | |||
CoercionType.GENERAL); | |||
if(mathType == Value.Type.STRING) { | |||
if(mathType.isString()) { | |||
throw new EvalException("Unexpected type " + mathType); | |||
} | |||
return toValue(param1.getAsLongInt(ctx) % param2.getAsLongInt(ctx)); | |||
@@ -579,7 +578,7 @@ public class BuiltinOperators | |||
return t1; | |||
} | |||
if((t1 == Value.Type.STRING) || (t2 == Value.Type.STRING)) { | |||
if(t1.isString() || t2.isString()) { | |||
if(cType._allowCoerceStringToNum) { | |||
// see if this is mixed string/numeric and the string can be coerced |
@@ -34,7 +34,6 @@ import com.healthmarketscience.jackcess.expr.NumericConfig; | |||
import com.healthmarketscience.jackcess.expr.TemporalConfig; | |||
import com.healthmarketscience.jackcess.expr.Value; | |||
import com.healthmarketscience.jackcess.impl.DatabaseImpl; | |||
import com.healthmarketscience.jackcess.impl.NumberFormatter; | |||
import static com.healthmarketscience.jackcess.impl.expr.FunctionSupport.*; | |||
/** | |||
@@ -75,7 +74,7 @@ public class DefaultFunctions | |||
public static final Function HEX = registerStringFunc(new Func1NullIsNull("Hex") { | |||
@Override | |||
protected Value eval1(EvalContext ctx, Value param1) { | |||
if((param1.getType() == Value.Type.STRING) && | |||
if(param1.getType().isString() && | |||
(param1.getAsString(ctx).length() == 0)) { | |||
return ValueSupport.ZERO_VAL; | |||
} | |||
@@ -95,8 +94,7 @@ public class DefaultFunctions | |||
return params[1]; | |||
} | |||
Value.Type resultType = ctx.getResultType(); | |||
return (((resultType == null) || | |||
(resultType == Value.Type.STRING)) ? | |||
return (((resultType == null) || resultType.isString()) ? | |||
ValueSupport.EMPTY_STR_VAL : ValueSupport.ZERO_VAL); | |||
} | |||
}); | |||
@@ -131,7 +129,7 @@ public class DefaultFunctions | |||
public static final Function OCT = registerStringFunc(new Func1NullIsNull("Oct") { | |||
@Override | |||
protected Value eval1(EvalContext ctx, Value param1) { | |||
if((param1.getType() == Value.Type.STRING) && | |||
if(param1.getType().isString() && | |||
(param1.getAsString(ctx).length() == 0)) { | |||
return ValueSupport.ZERO_VAL; | |||
} | |||
@@ -256,7 +254,7 @@ public class DefaultFunctions | |||
// return true if it is explicitly a date/time, not if it is just a | |||
// number (even though casting a number string to a date/time works in | |||
// general) | |||
if((param1.getType() == Value.Type.STRING) && | |||
if(param1.getType().isString() && | |||
!stringIsNumeric(ctx, param1) && | |||
stringIsTemporal(ctx, param1)) { | |||
return ValueSupport.TRUE_VAL; | |||
@@ -275,8 +273,7 @@ public class DefaultFunctions | |||
// note, only a string can be considered numberic for this function, | |||
// even though a date/time can be cast to a number in general | |||
if((param1.getType() == Value.Type.STRING) && | |||
stringIsNumeric(ctx, param1)) { | |||
if(param1.getType().isString() && stringIsNumeric(ctx, param1)) { | |||
return ValueSupport.TRUE_VAL; | |||
} | |||
@@ -287,21 +284,21 @@ public class DefaultFunctions | |||
public static final Function FORMATNUMBER = registerFunc(new FuncVar("FormatNumber", 1, 6) { | |||
@Override | |||
protected Value evalVar(EvalContext ctx, Value[] params) { | |||
return formatNumber(ctx, params, false, false); | |||
return formatNumber(ctx, params, FormatUtil.NumPatternType.GENERAL); | |||
} | |||
}); | |||
public static final Function FORMATPERCENT = registerFunc(new FuncVar("FormatPercent", 1, 6) { | |||
@Override | |||
protected Value evalVar(EvalContext ctx, Value[] params) { | |||
return formatNumber(ctx, params, true, false); | |||
return formatNumber(ctx, params, FormatUtil.NumPatternType.PERCENT); | |||
} | |||
}); | |||
public static final Function FORMATCURRENCY = registerFunc(new FuncVar("FormatCurrency", 1, 6) { | |||
@Override | |||
protected Value evalVar(EvalContext ctx, Value[] params) { | |||
return formatNumber(ctx, params, false, true); | |||
return formatNumber(ctx, params, FormatUtil.NumPatternType.CURRENCY); | |||
} | |||
}); | |||
@@ -479,24 +476,30 @@ public class DefaultFunctions | |||
}); | |||
private static boolean stringIsNumeric(EvalContext ctx, Value param) { | |||
return (maybeGetAsBigDecimal(ctx, param) != null); | |||
} | |||
static BigDecimal maybeGetAsBigDecimal(EvalContext ctx, Value param) { | |||
try { | |||
param.getAsBigDecimal(ctx); | |||
return true; | |||
return param.getAsBigDecimal(ctx); | |||
} catch(EvalException ignored) { | |||
// fall through to false | |||
// not a number | |||
} | |||
return false; | |||
return null; | |||
} | |||
private static boolean stringIsTemporal(EvalContext ctx, Value param) { | |||
return (maybeGetAsDateTimeValue(ctx, param) != null); | |||
} | |||
static Value maybeGetAsDateTimeValue(EvalContext ctx, Value param) { | |||
try { | |||
// see if we can coerce to date/time | |||
param.getAsDateTimeValue(ctx); | |||
return true; | |||
return param.getAsDateTimeValue(ctx); | |||
} catch(EvalException ignored) { | |||
// not a date/time | |||
} | |||
return false; | |||
return null; | |||
} | |||
private static boolean getOptionalTriStateBoolean( | |||
@@ -525,7 +528,7 @@ public class DefaultFunctions | |||
} | |||
private static Value formatNumber( | |||
EvalContext ctx, Value[] params, boolean isPercent, boolean isCurrency) { | |||
EvalContext ctx, Value[] params, FormatUtil.NumPatternType numPatType) { | |||
Value param1 = params[0]; | |||
if(param1.isNull()) { | |||
@@ -537,44 +540,18 @@ public class DefaultFunctions | |||
ctx, params, 1, cfg.getNumDecimalDigits(), -1); | |||
boolean incLeadDigit = getOptionalTriStateBoolean( | |||
ctx, params, 2, cfg.includeLeadingDigit()); | |||
boolean defNegParens = (isCurrency ? cfg.useParensForCurrencyNegatives() : | |||
cfg.useParensForNegatives()); | |||
boolean defNegParens = numPatType.useParensForNegatives(cfg); | |||
boolean negParens = getOptionalTriStateBoolean( | |||
ctx, params, 3, defNegParens); | |||
int numGroupDigits = cfg.getNumGroupingDigits(); | |||
int defNumGroupDigits = cfg.getNumGroupingDigits(); | |||
boolean groupDigits = getOptionalTriStateBoolean( | |||
ctx, params, 4, (numGroupDigits > 0)); | |||
StringBuilder fmt = new StringBuilder(); | |||
if(isCurrency) { | |||
fmt.append("\u00A4"); | |||
} | |||
ctx, params, 4, (defNumGroupDigits > 0)); | |||
int numGroupDigits = (groupDigits ? defNumGroupDigits : 0); | |||
if(groupDigits) { | |||
fmt.append("#,"); | |||
DefaultTextFunctions.nchars(fmt, numGroupDigits - 1, '#'); | |||
} | |||
fmt.append(incLeadDigit ? "0" : "#"); | |||
if(numDecDigits > 0) { | |||
fmt.append("."); | |||
DefaultTextFunctions.nchars(fmt, numDecDigits, '0'); | |||
} | |||
if(isPercent) { | |||
fmt.append("%"); | |||
} | |||
if(negParens) { | |||
// the javadocs claim the second pattern does not need to be fully | |||
// defined, but it doesn't seem to work that way | |||
String mainPat = fmt.toString(); | |||
fmt.append(";(").append(mainPat).append(")"); | |||
} | |||
String fmtStr = FormatUtil.createNumberFormatPattern( | |||
numPatType, numDecDigits, incLeadDigit, negParens, numGroupDigits); | |||
// Note, DecimalFormat rounding mode uses HALF_EVEN by default | |||
DecimalFormat df = ctx.createDecimalFormat(fmt.toString()); | |||
DecimalFormat df = ctx.createDecimalFormat(fmtStr); | |||
return ValueSupport.toValue(df.format(param1.getAsBigDecimal(ctx))); | |||
} |
@@ -22,7 +22,6 @@ import com.healthmarketscience.jackcess.expr.EvalContext; | |||
import com.healthmarketscience.jackcess.expr.EvalException; | |||
import com.healthmarketscience.jackcess.expr.Function; | |||
import com.healthmarketscience.jackcess.expr.Value; | |||
import com.healthmarketscience.jackcess.impl.NumberFormatter; | |||
import static com.healthmarketscience.jackcess.impl.expr.DefaultFunctions.*; | |||
import static com.healthmarketscience.jackcess.impl.expr.FunctionSupport.*; | |||
@@ -19,7 +19,6 @@ package com.healthmarketscience.jackcess.impl.expr; | |||
import java.math.BigDecimal; | |||
import com.healthmarketscience.jackcess.expr.LocaleContext; | |||
import com.healthmarketscience.jackcess.impl.NumberFormatter; | |||
/** | |||
* |
@@ -16,11 +16,15 @@ limitations under the License. | |||
package com.healthmarketscience.jackcess.impl.expr; | |||
import java.math.BigDecimal; | |||
import java.text.DateFormat; | |||
import java.text.DecimalFormat; | |||
import java.text.NumberFormat; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import com.healthmarketscience.jackcess.expr.EvalContext; | |||
import com.healthmarketscience.jackcess.expr.NumericConfig; | |||
import com.healthmarketscience.jackcess.expr.TemporalConfig; | |||
import com.healthmarketscience.jackcess.expr.Value; | |||
@@ -30,11 +34,43 @@ import com.healthmarketscience.jackcess.expr.Value; | |||
*/ | |||
public class FormatUtil | |||
{ | |||
public enum NumPatternType { | |||
GENERAL, CURRENCY { | |||
@Override | |||
protected void appendPrefix(StringBuilder fmt) { | |||
fmt.append("\u00A4"); | |||
} | |||
@Override | |||
protected boolean useParensForNegatives(NumericConfig cfg) { | |||
return cfg.useParensForCurrencyNegatives(); | |||
} | |||
}, | |||
PERCENT { | |||
@Override | |||
protected void appendSuffix(StringBuilder fmt) { | |||
fmt.append("%"); | |||
} | |||
}, | |||
SCIENTIFIC { | |||
@Override | |||
protected void appendSuffix(StringBuilder fmt) { | |||
fmt.append("E0"); | |||
} | |||
}; | |||
protected void appendPrefix(StringBuilder fmt) {} | |||
protected void appendSuffix(StringBuilder fmt) {} | |||
protected boolean useParensForNegatives(NumericConfig cfg) { | |||
return cfg.useParensForNegatives(); | |||
} | |||
} | |||
private static final Map<String,Fmt> PREDEF_FMTS = new HashMap<String,Fmt>(); | |||
static { | |||
PREDEF_FMTS.put("General Date", | |||
new PredefDateFmt(TemporalConfig.Type.GENERAL_DATE)); | |||
PREDEF_FMTS.put("General Date", new GenPredefDateFmt()); | |||
PREDEF_FMTS.put("Long Date", | |||
new PredefDateFmt(TemporalConfig.Type.LONG_DATE)); | |||
PREDEF_FMTS.put("Medium Date", | |||
@@ -48,6 +84,20 @@ public class FormatUtil | |||
PREDEF_FMTS.put("Short Time", | |||
new PredefDateFmt(TemporalConfig.Type.SHORT_TIME)); | |||
PREDEF_FMTS.put("General Number", new GenPredefNumberFmt()); | |||
PREDEF_FMTS.put("Currency", | |||
new PredefNumberFmt(NumericConfig.Type.CURRENCY)); | |||
// FIXME ? | |||
// PREDEF_FMTS.put("Euro", | |||
// new PredefNumberFmt(???)); | |||
PREDEF_FMTS.put("Fixed", | |||
new PredefNumberFmt(NumericConfig.Type.FIXED)); | |||
PREDEF_FMTS.put("Standard", | |||
new PredefNumberFmt(NumericConfig.Type.STANDARD)); | |||
PREDEF_FMTS.put("Percent", | |||
new PredefNumberFmt(NumericConfig.Type.PERCENT)); | |||
PREDEF_FMTS.put("Scientific", new ScientificPredefNumberFmt()); | |||
PREDEF_FMTS.put("True/False", new PredefBoolFmt("True", "False")); | |||
PREDEF_FMTS.put("Yes/No", new PredefBoolFmt("Yes", "No")); | |||
PREDEF_FMTS.put("On/Off", new PredefBoolFmt("On", "Off")); | |||
@@ -59,14 +109,55 @@ public class FormatUtil | |||
public static Value format(EvalContext ctx, Value expr, String fmtStr, | |||
int firstDay, int firstWeekType) { | |||
Fmt predefFmt = PREDEF_FMTS.get(fmtStr); | |||
if(predefFmt != null) { | |||
if(expr.isNull()) { | |||
// predefined formats return null for null | |||
return ValueSupport.NULL_VAL; | |||
} | |||
return predefFmt.format(ctx, expr, null, firstDay, firstWeekType); | |||
} | |||
// FIXME, | |||
throw new UnsupportedOperationException(); | |||
} | |||
public static String createNumberFormatPattern( | |||
NumPatternType numPatType, int numDecDigits, boolean incLeadDigit, | |||
boolean negParens, int numGroupDigits) { | |||
StringBuilder fmt = new StringBuilder(); | |||
numPatType.appendPrefix(fmt); | |||
if(numGroupDigits > 0) { | |||
fmt.append("#,"); | |||
DefaultTextFunctions.nchars(fmt, numGroupDigits - 1, '#'); | |||
} | |||
fmt.append(incLeadDigit ? "0" : "#"); | |||
if(numDecDigits > 0) { | |||
fmt.append("."); | |||
DefaultTextFunctions.nchars(fmt, numDecDigits, '0'); | |||
} | |||
numPatType.appendSuffix(fmt); | |||
if(negParens) { | |||
// the javadocs claim the second pattern does not need to be fully | |||
// defined, but it doesn't seem to work that way | |||
String mainPat = fmt.toString(); | |||
fmt.append(";(").append(mainPat).append(")"); | |||
} | |||
return fmt.toString(); | |||
} | |||
private static abstract class Fmt | |||
{ | |||
// FIXME, no null | |||
// FIXME, need fmtStr? | |||
public abstract Value format(EvalContext ctx, Value expr, String fmtStr, | |||
int firstDay, int firstWeekType); | |||
} | |||
@@ -88,6 +179,22 @@ public class FormatUtil | |||
} | |||
} | |||
private static class GenPredefDateFmt extends Fmt | |||
{ | |||
@Override | |||
public Value format(EvalContext ctx, Value expr, String fmtStr, | |||
int firstDay, int firstWeekType) { | |||
Value tempExpr = expr; | |||
if(!expr.getType().isTemporal()) { | |||
Value maybe = DefaultFunctions.maybeGetAsDateTimeValue(ctx, expr); | |||
if(maybe != null) { | |||
tempExpr = maybe; | |||
} | |||
} | |||
return ValueSupport.toValue(tempExpr.getAsString(ctx)); | |||
} | |||
} | |||
private static class PredefBoolFmt extends Fmt | |||
{ | |||
private final Value _trueVal; | |||
@@ -101,10 +208,64 @@ public class FormatUtil | |||
@Override | |||
public Value format(EvalContext ctx, Value expr, String fmtStr, | |||
int firstDay, int firstWeekType) { | |||
// FIXME, handle null? | |||
return(expr.getAsBoolean(ctx) ? _trueVal : _falseVal); | |||
} | |||
} | |||
private static class PredefNumberFmt extends Fmt | |||
{ | |||
private final NumericConfig.Type _type; | |||
private PredefNumberFmt(NumericConfig.Type type) { | |||
_type = type; | |||
} | |||
@Override | |||
public Value format(EvalContext ctx, Value expr, String fmtStr, | |||
int firstDay, int firstWeekType) { | |||
DecimalFormat df = ctx.createDecimalFormat( | |||
ctx.getNumericConfig().getNumberFormat(_type)); | |||
return ValueSupport.toValue(df.format(expr.getAsBigDecimal(ctx))); | |||
} | |||
} | |||
private static class GenPredefNumberFmt extends Fmt | |||
{ | |||
@Override | |||
public Value format(EvalContext ctx, Value expr, String fmtStr, | |||
int firstDay, int firstWeekType) { | |||
Value numExpr = expr; | |||
if(!expr.getType().isNumeric()) { | |||
if(expr.getType().isString()) { | |||
BigDecimal bd = DefaultFunctions.maybeGetAsBigDecimal(ctx, expr); | |||
if(bd != null) { | |||
numExpr = ValueSupport.toValue(bd); | |||
} else { | |||
// convert to date to number | |||
Value maybe = DefaultFunctions.maybeGetAsDateTimeValue(ctx, expr); | |||
if(maybe != null) { | |||
numExpr = ValueSupport.toValue(maybe.getAsDouble(ctx)); | |||
} | |||
} | |||
} else { | |||
// convert date to number | |||
numExpr = ValueSupport.toValue(expr.getAsDouble(ctx)); | |||
} | |||
} | |||
return ValueSupport.toValue(numExpr.getAsString(ctx)); | |||
} | |||
} | |||
private static class ScientificPredefNumberFmt extends Fmt | |||
{ | |||
@Override | |||
public Value format(EvalContext ctx, Value expr, String fmtStr, | |||
int firstDay, int firstWeekType) { | |||
NumberFormat df = ctx.createDecimalFormat( | |||
ctx.getNumericConfig().getNumberFormat( | |||
NumericConfig.Type.SCIENTIFIC)); | |||
df = new NumberFormatter.ScientificFormat(df); | |||
return ValueSupport.toValue(df.format(expr.getAsBigDecimal(ctx))); | |||
} | |||
} | |||
} |
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and | |||
limitations under the License. | |||
*/ | |||
package com.healthmarketscience.jackcess.impl; | |||
package com.healthmarketscience.jackcess.impl.expr; | |||
import java.math.BigDecimal; | |||
import java.math.MathContext; | |||
@@ -32,6 +32,41 @@ public class NumberFormatter | |||
{ | |||
public static final RoundingMode ROUND_MODE = RoundingMode.HALF_EVEN; | |||
/** designates the format of exponent notation used by ScientificFormat */ | |||
public enum NotationType { | |||
/** Scientific notation "E", "E-" (default java behavior) */ | |||
EXP_E_MINUS { | |||
@Override | |||
protected void format(StringBuffer sb, int eIdx) { | |||
// nothing to do | |||
} | |||
}, | |||
/** Scientific notation "E+", "E-" */ | |||
EXP_E_PLUS { | |||
@Override | |||
protected void format(StringBuffer sb, int eIdx) { | |||
maybeInsertExpPlus(sb, eIdx); | |||
} | |||
}, | |||
/** Scientific notation "e", "e-" */ | |||
EXP_e_MINUS { | |||
@Override | |||
protected void format(StringBuffer sb, int eIdx) { | |||
sb.setCharAt(eIdx, 'e'); | |||
} | |||
}, | |||
/** Scientific notation "e+", "e-" */ | |||
EXP_e_PLUS { | |||
@Override | |||
protected void format(StringBuffer sb, int eIdx) { | |||
sb.setCharAt(eIdx, 'e'); | |||
maybeInsertExpPlus(sb, eIdx); | |||
} | |||
}; | |||
protected abstract void format(StringBuffer sb, int idx); | |||
} | |||
private static final int FLT_SIG_DIGITS = 7; | |||
private static final int DBL_SIG_DIGITS = 15; | |||
private static final int DEC_SIG_DIGITS = 28; | |||
@@ -102,10 +137,18 @@ public class NumberFormatter | |||
return _decFmt.format(bd.round(DEC_MATH_CONTEXT)); | |||
} | |||
private static ScientificFormat createScientificFormat(int prec) { | |||
DecimalFormat df = new DecimalFormat("0.#E00"); | |||
df.setMaximumIntegerDigits(1); | |||
df.setMaximumFractionDigits(prec); | |||
df.setRoundingMode(ROUND_MODE); | |||
return new ScientificFormat(df); | |||
} | |||
private static final class TypeFormatter | |||
{ | |||
private final DecimalFormat _df = new DecimalFormat("0.#"); | |||
private final BetterDecimalFormat _dfS; | |||
private final ScientificFormat _dfS; | |||
private final int _prec; | |||
private TypeFormatter(int prec) { | |||
@@ -113,7 +156,7 @@ public class NumberFormatter | |||
_df.setMaximumIntegerDigits(prec); | |||
_df.setMaximumFractionDigits(prec); | |||
_df.setRoundingMode(ROUND_MODE); | |||
_dfS = new BetterDecimalFormat("0.#E00", prec); | |||
_dfS = createScientificFormat(prec); | |||
} | |||
public String format(BigDecimal bd) { | |||
@@ -132,18 +175,26 @@ public class NumberFormatter | |||
} | |||
} | |||
private static final class BetterDecimalFormat extends NumberFormat | |||
private static void maybeInsertExpPlus(StringBuffer sb, int eIdx) { | |||
if(sb.charAt(eIdx + 1) != '-') { | |||
sb.insert(eIdx + 1, '+'); | |||
} | |||
} | |||
public static class ScientificFormat extends NumberFormat | |||
{ | |||
private static final long serialVersionUID = 0L; | |||
private final DecimalFormat _df; | |||
private final NumberFormat _df; | |||
private final NotationType _type; | |||
private BetterDecimalFormat(String pat, int prec) { | |||
super(); | |||
_df = new DecimalFormat(pat); | |||
_df.setMaximumIntegerDigits(1); | |||
_df.setMaximumFractionDigits(prec); | |||
_df.setRoundingMode(ROUND_MODE); | |||
public ScientificFormat(NumberFormat df) { | |||
this(df, NotationType.EXP_E_PLUS); | |||
} | |||
public ScientificFormat(NumberFormat df, NotationType type) { | |||
_df = df; | |||
_type = type; | |||
} | |||
@Override | |||
@@ -151,10 +202,7 @@ public class NumberFormatter | |||
FieldPosition pos) | |||
{ | |||
StringBuffer sb = _df.format(number, toAppendTo, pos); | |||
int idx = sb.lastIndexOf("E"); | |||
if(sb.charAt(idx + 1) != '-') { | |||
sb.insert(idx + 1, '+'); | |||
} | |||
_type.format(sb, sb.lastIndexOf("E")); | |||
return sb; | |||
} | |||
@@ -263,6 +263,65 @@ public class DefaultFunctionsTest extends TestCase | |||
assertEval("13:37", "=FormatDateTime(#1/1/1973 1:37:25 PM#,4)"); | |||
} | |||
public void testFormat() throws Exception | |||
{ | |||
assertEval("12345.6789", "=Format(12345.6789, 'General Number')"); | |||
assertEval("0.12345", "=Format(0.12345, 'General Number')"); | |||
assertEval("-12345.6789", "=Format(-12345.6789, 'General Number')"); | |||
assertEval("-0.12345", "=Format(-0.12345, 'General Number')"); | |||
assertEval("12345.6789", "=Format('12345.6789', 'General Number')"); | |||
assertEval("1678.9", "=Format('1.6789E+3', 'General Number')"); | |||
assertEval("37623.2916666667", "=Format(#01/02/2003 7:00:00 AM#, 'General Number')"); | |||
assertEval("foo", "=Format('foo', 'General Number')"); | |||
assertEval("12,345.68", "=Format(12345.6789, 'Standard')"); | |||
assertEval("0.12", "=Format(0.12345, 'Standard')"); | |||
assertEval("-12,345.68", "=Format(-12345.6789, 'Standard')"); | |||
assertEval("-0.12", "=Format(-0.12345, 'Standard')"); | |||
assertEval("12345.68", "=Format(12345.6789, 'Fixed')"); | |||
assertEval("0.12", "=Format(0.12345, 'Fixed')"); | |||
assertEval("-12345.68", "=Format(-12345.6789, 'Fixed')"); | |||
assertEval("-0.12", "=Format(-0.12345, 'Fixed')"); | |||
assertEval("$12,345.68", "=Format(12345.6789, 'Currency')"); | |||
assertEval("$0.12", "=Format(0.12345, 'Currency')"); | |||
assertEval("($12,345.68)", "=Format(-12345.6789, 'Currency')"); | |||
assertEval("($0.12)", "=Format(-0.12345, 'Currency')"); | |||
assertEval("1234567.89%", "=Format(12345.6789, 'Percent')"); | |||
assertEval("12.34%", "=Format(0.12345, 'Percent')"); | |||
assertEval("-1234567.89%", "=Format(-12345.6789, 'Percent')"); | |||
assertEval("-12.34%", "=Format(-0.12345, 'Percent')"); | |||
assertEval("1.23E+4", "=Format(12345.6789, 'Scientific')"); | |||
assertEval("1.23E-1", "=Format(0.12345, 'Scientific')"); | |||
assertEval("-1.23E+4", "=Format(-12345.6789, 'Scientific')"); | |||
assertEval("-1.23E-1", "=Format(-0.12345, 'Scientific')"); | |||
assertEval("Yes", "=Format(True, 'Yes/No')"); | |||
assertEval("No", "=Format(False, 'Yes/No')"); | |||
assertEval("True", "=Format(True, 'True/False')"); | |||
assertEval("False", "=Format(False, 'True/False')"); | |||
assertEval("On", "=Format(True, 'On/Off')"); | |||
assertEval("Off", "=Format(False, 'On/Off')"); | |||
assertEval("1/2/2003 7:00:00 AM", "=Format(#01/02/2003 7:00:00 AM#, 'General Date')"); | |||
assertEval("1/2/2003", "=Format(#01/02/2003#, 'General Date')"); | |||
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("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')"); | |||
assertEval("1/2/2003", "=Format(#01/02/2003 7:00:00 AM#, 'Short Date')"); | |||
assertEval("7:00:00 AM", "=Format(#01/02/2003 7:00:00 AM#, 'Long Time')"); | |||
assertEval("07:00 AM", "=Format(#01/02/2003 7:00:00 AM#, 'Medium Time')"); | |||
assertEval("07:00", "=Format(#01/02/2003 7:00:00 AM#, 'Short Time')"); | |||
assertEval("19:00", "=Format(#01/02/2003 7:00:00 PM#, 'Short Time')"); | |||
} | |||
public void testNumberFuncs() throws Exception | |||
{ | |||
assertEval(1, "=Abs(1)"); |
@@ -38,7 +38,7 @@ import com.healthmarketscience.jackcess.expr.ParseException; | |||
import com.healthmarketscience.jackcess.expr.TemporalConfig; | |||
import com.healthmarketscience.jackcess.expr.Value; | |||
import com.healthmarketscience.jackcess.impl.BaseEvalContext; | |||
import com.healthmarketscience.jackcess.impl.NumberFormatter; | |||
import com.healthmarketscience.jackcess.impl.expr.NumberFormatter; | |||
import junit.framework.TestCase; | |||
/** | |||
@@ -609,7 +609,7 @@ public class ExpressionatorTest extends TestCase | |||
return new DecimalFormat( | |||
formatStr, NumericConfig.US_NUMERIC_CONFIG.getDecimalFormatSymbols()); | |||
} | |||
public FunctionLookup getFunctionLookup() { | |||
return DefaultFunctions.LOOKUP; | |||
} |
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and | |||
limitations under the License. | |||
*/ | |||
package com.healthmarketscience.jackcess.impl; | |||
package com.healthmarketscience.jackcess.impl.expr; | |||
import java.math.BigDecimal; |