]> source.dussan.org Git - jackcess.git/commitdiff
implement Format with predefined formats
authorJames Ahlborn <jtahlborn@yahoo.com>
Tue, 20 Nov 2018 02:01:46 +0000 (02:01 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Tue, 20 Nov 2018 02:01:46 +0000 (02:01 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1226 f203690c-595d-4dc9-a70b-905162fa7fd2

16 files changed:
src/main/java/com/healthmarketscience/jackcess/expr/NumericConfig.java
src/main/java/com/healthmarketscience/jackcess/expr/Value.java
src/main/java/com/healthmarketscience/jackcess/impl/DBEvalContext.java
src/main/java/com/healthmarketscience/jackcess/impl/NumberFormatter.java [deleted file]
src/main/java/com/healthmarketscience/jackcess/impl/expr/BaseValue.java
src/main/java/com/healthmarketscience/jackcess/impl/expr/BigDecimalValue.java
src/main/java/com/healthmarketscience/jackcess/impl/expr/BuiltinOperators.java
src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctions.java
src/main/java/com/healthmarketscience/jackcess/impl/expr/DefaultNumberFunctions.java
src/main/java/com/healthmarketscience/jackcess/impl/expr/DoubleValue.java
src/main/java/com/healthmarketscience/jackcess/impl/expr/FormatUtil.java
src/main/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatter.java [new file with mode: 0644]
src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java [deleted file]
src/test/java/com/healthmarketscience/jackcess/impl/expr/DefaultFunctionsTest.java
src/test/java/com/healthmarketscience/jackcess/impl/expr/ExpressionatorTest.java
src/test/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatterTest.java [new file with mode: 0644]

index cf9bacb6f70294ea638c9ec6746a7877dedd2065..74dd06eac7c47ac6781866953584a6e771d12663 100644 (file)
@@ -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;
   }
index e9508fae63e971008f19ac0f8faf12a907470193..118215ef41b3bab6c1490d6f9cd75dc0f117cd5d 100644 (file)
@@ -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);
     }
index b1e9995a9c141348e9208a89f916fa1a2ab7b58d..398d9fa7baf27abc9e2769d57388a6696221683b 100644 (file)
@@ -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);
   }
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/NumberFormatter.java b/src/main/java/com/healthmarketscience/jackcess/impl/NumberFormatter.java
deleted file mode 100644 (file)
index 358a0a6..0000000
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
-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 java.math.MathContext;
-import java.math.RoundingMode;
-import java.text.DecimalFormat;
-import java.text.FieldPosition;
-import java.text.NumberFormat;
-import java.text.ParsePosition;
-
-/**
- *
- * @author James Ahlborn
- */
-public class NumberFormatter
-{
-  public static final RoundingMode ROUND_MODE = RoundingMode.HALF_EVEN;
-
-  private static final int FLT_SIG_DIGITS = 7;
-  private static final int DBL_SIG_DIGITS = 15;
-  private static final int DEC_SIG_DIGITS = 28;
-
-  public static final MathContext FLT_MATH_CONTEXT =
-    new MathContext(FLT_SIG_DIGITS, ROUND_MODE);
-  public static final MathContext DBL_MATH_CONTEXT =
-    new MathContext(DBL_SIG_DIGITS, ROUND_MODE);
-  public static final MathContext DEC_MATH_CONTEXT =
-    new MathContext(DEC_SIG_DIGITS, ROUND_MODE);
-
-  // note, java doesn't distinguish between pos/neg NaN
-  private static final String NAN_STR = "1.#QNAN";
-  private static final String POS_INF_STR = "1.#INF";
-  private static final String NEG_INf_STR = "-1.#INF";
-
-  private static final ThreadLocal<NumberFormatter> INSTANCE =
-    new ThreadLocal<NumberFormatter>() {
-    @Override
-    protected NumberFormatter initialValue() {
-      return new NumberFormatter();
-    }
-  };
-
-  private final TypeFormatter _fltFmt = new TypeFormatter(FLT_SIG_DIGITS);
-  private final TypeFormatter _dblFmt = new TypeFormatter(DBL_SIG_DIGITS);
-  private final TypeFormatter _decFmt = new TypeFormatter(DEC_SIG_DIGITS);
-
-  private NumberFormatter() {}
-
-  public static String format(float f) {
-    return INSTANCE.get().formatImpl(f);
-  }
-
-  public static String format(double d) {
-    return INSTANCE.get().formatImpl(d);
-  }
-
-  public static String format(BigDecimal bd) {
-    return INSTANCE.get().formatImpl(bd);
-  }
-
-  private String formatImpl(float f) {
-
-    if(Float.isNaN(f)) {
-      return NAN_STR;
-    }
-    if(Float.isInfinite(f)) {
-      return ((f < 0f) ? NEG_INf_STR : POS_INF_STR);
-    }
-
-    return _fltFmt.format(new BigDecimal(f, FLT_MATH_CONTEXT));
-  }
-
-  private String formatImpl(double d) {
-
-    if(Double.isNaN(d)) {
-      return NAN_STR;
-    }
-    if(Double.isInfinite(d)) {
-      return ((d < 0d) ? NEG_INf_STR : POS_INF_STR);
-    }
-
-    return _dblFmt.format(new BigDecimal(d, DBL_MATH_CONTEXT));
-  }
-
-  private String formatImpl(BigDecimal bd) {
-    return _decFmt.format(bd.round(DEC_MATH_CONTEXT));
-  }
-
-  private static final class TypeFormatter
-  {
-    private final DecimalFormat _df = new DecimalFormat("0.#");
-    private final BetterDecimalFormat _dfS;
-    private final int _prec;
-
-    private TypeFormatter(int prec) {
-      _prec = prec;
-      _df.setMaximumIntegerDigits(prec);
-      _df.setMaximumFractionDigits(prec);
-      _df.setRoundingMode(ROUND_MODE);
-      _dfS = new BetterDecimalFormat("0.#E00", prec);
-    }
-
-    public String format(BigDecimal bd) {
-      bd = bd.stripTrailingZeros();
-      int prec = bd.precision();
-      int scale = bd.scale();
-
-      int sigDigits = prec;
-      if(scale < 0) {
-        sigDigits -= scale;
-      } else if(scale > prec) {
-        sigDigits += (scale - prec);
-      }
-
-      return ((sigDigits > _prec) ? _dfS.format(bd) : _df.format(bd));
-    }
-  }
-
-  private static final class BetterDecimalFormat extends NumberFormat
-  {
-    private static final long serialVersionUID = 0L;
-
-    private final DecimalFormat _df;
-
-    private BetterDecimalFormat(String pat, int prec) {
-      super();
-      _df = new DecimalFormat(pat);
-      _df.setMaximumIntegerDigits(1);
-      _df.setMaximumFractionDigits(prec);
-      _df.setRoundingMode(ROUND_MODE);
-    }
-
-    @Override
-    public StringBuffer format(Object number, StringBuffer toAppendTo,
-                               FieldPosition pos)
-    {
-      StringBuffer sb = _df.format(number, toAppendTo, pos);
-      int idx = sb.lastIndexOf("E");
-      if(sb.charAt(idx + 1) != '-') {
-        sb.insert(idx + 1, '+');
-      }
-      return sb;
-    }
-
-    @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();
-    }
-  }
-}
index b4442183261531438587bbb22883b566fa235ac7..2b172d398691acea35853bdbf3db0912a02274c0 100644 (file)
@@ -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;
 
 /**
  *
index b847b592b1af92dd870ed10e3e8d6fb6000c887e..0e78f90a9481c30453935f62539ff872f3da9ced 100644 (file)
@@ -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;
 
 /**
  *
index be893c3b24564d6384f58d50f74afcab072ee189..5131a93ff5775891845c3fe0b4f31f7e72e145e6 100644 (file)
@@ -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
index d66330978dfc4cbf2054a6aa68fcc8a58d3dd312..29c0f71eea58aa2200329c0152b2dfca5a992c9f 100644 (file)
@@ -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)));
   }
index 43bf5f4b05755b3805830cad92fef02372e530c3..1ec08db689bbf923dcef4fed8159f01abd7f63cf 100644 (file)
@@ -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.*;
 
index fbbdd96cac7ffc0971970902af22eb6d0722a3f2..d37c90b68a38ce79e55cfb45f4ce92576fd98f99 100644 (file)
@@ -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;
 
 /**
  *
index 1d9e21ef0acea83ab92edf4bd4da9ca99cabdf26..2c475eba56201e247e64560c7942b71850d17ef3 100644 (file)
@@ -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)));
+    }
+  }
 }
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatter.java b/src/main/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatter.java
new file mode 100644 (file)
index 0000000..ce251c2
--- /dev/null
@@ -0,0 +1,226 @@
+/*
+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.expr;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+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;
+
+  public static final MathContext FLT_MATH_CONTEXT =
+    new MathContext(FLT_SIG_DIGITS, ROUND_MODE);
+  public static final MathContext DBL_MATH_CONTEXT =
+    new MathContext(DBL_SIG_DIGITS, ROUND_MODE);
+  public static final MathContext DEC_MATH_CONTEXT =
+    new MathContext(DEC_SIG_DIGITS, ROUND_MODE);
+
+  // note, java doesn't distinguish between pos/neg NaN
+  private static final String NAN_STR = "1.#QNAN";
+  private static final String POS_INF_STR = "1.#INF";
+  private static final String NEG_INf_STR = "-1.#INF";
+
+  private static final ThreadLocal<NumberFormatter> INSTANCE =
+    new ThreadLocal<NumberFormatter>() {
+    @Override
+    protected NumberFormatter initialValue() {
+      return new NumberFormatter();
+    }
+  };
+
+  private final TypeFormatter _fltFmt = new TypeFormatter(FLT_SIG_DIGITS);
+  private final TypeFormatter _dblFmt = new TypeFormatter(DBL_SIG_DIGITS);
+  private final TypeFormatter _decFmt = new TypeFormatter(DEC_SIG_DIGITS);
+
+  private NumberFormatter() {}
+
+  public static String format(float f) {
+    return INSTANCE.get().formatImpl(f);
+  }
+
+  public static String format(double d) {
+    return INSTANCE.get().formatImpl(d);
+  }
+
+  public static String format(BigDecimal bd) {
+    return INSTANCE.get().formatImpl(bd);
+  }
+
+  private String formatImpl(float f) {
+
+    if(Float.isNaN(f)) {
+      return NAN_STR;
+    }
+    if(Float.isInfinite(f)) {
+      return ((f < 0f) ? NEG_INf_STR : POS_INF_STR);
+    }
+
+    return _fltFmt.format(new BigDecimal(f, FLT_MATH_CONTEXT));
+  }
+
+  private String formatImpl(double d) {
+
+    if(Double.isNaN(d)) {
+      return NAN_STR;
+    }
+    if(Double.isInfinite(d)) {
+      return ((d < 0d) ? NEG_INf_STR : POS_INF_STR);
+    }
+
+    return _dblFmt.format(new BigDecimal(d, DBL_MATH_CONTEXT));
+  }
+
+  private String formatImpl(BigDecimal bd) {
+    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 ScientificFormat _dfS;
+    private final int _prec;
+
+    private TypeFormatter(int prec) {
+      _prec = prec;
+      _df.setMaximumIntegerDigits(prec);
+      _df.setMaximumFractionDigits(prec);
+      _df.setRoundingMode(ROUND_MODE);
+      _dfS = createScientificFormat(prec);
+    }
+
+    public String format(BigDecimal bd) {
+      bd = bd.stripTrailingZeros();
+      int prec = bd.precision();
+      int scale = bd.scale();
+
+      int sigDigits = prec;
+      if(scale < 0) {
+        sigDigits -= scale;
+      } else if(scale > prec) {
+        sigDigits += (scale - prec);
+      }
+
+      return ((sigDigits > _prec) ? _dfS.format(bd) : _df.format(bd));
+    }
+  }
+
+  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 NumberFormat _df;
+    private final NotationType _type;
+
+    public ScientificFormat(NumberFormat df) {
+      this(df, NotationType.EXP_E_PLUS);
+    }
+
+    public ScientificFormat(NumberFormat df, NotationType type) {
+      _df = df;
+      _type = type;
+    }
+
+    @Override
+    public StringBuffer format(Object number, StringBuffer toAppendTo,
+                               FieldPosition pos)
+    {
+      StringBuffer sb = _df.format(number, toAppendTo, pos);
+      _type.format(sb, sb.lastIndexOf("E"));
+      return sb;
+    }
+
+    @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();
+    }
+  }
+}
diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/NumberFormatterTest.java
deleted file mode 100644 (file)
index f69dca1..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
-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")));
-  }
-}
index 8c015d8d6039c2ec3d5880abfb1c5bf8df78bc3e..132b788861193fc29f80b728af2ea7deadb31496 100644 (file)
@@ -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)");
index 17ad3d7db1acb0490a074be7bb6e1b823272726e..c67dfe7905f6b3e3f909dcceed4aa78da35cd5d5 100644 (file)
@@ -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;
     }
diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatterTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/expr/NumberFormatterTest.java
new file mode 100644 (file)
index 0000000..4d26c1b
--- /dev/null
@@ -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.expr;
+
+
+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")));
+  }
+}