Open Source Repository

Home /jodd/jodd-3.3.2 | Repository Home



jodd/format/PrintfFormat.java
// Copyright (c) 2003-2012, Jodd Team (jodd.org). All Rights Reserved.

package jodd.format;

import jodd.util.StringPool;

import java.math.BigInteger;

/**
 * Fast simple and yet useful formattings.
 */
public class PrintfFormat {
  
  protected int width;
  protected int precision;
  protected StringBuffer pre;
  protected StringBuffer post;
  protected boolean leadingZeroes;
  protected boolean showPlus;
  protected boolean alternate;
  protected boolean showSpace;
  protected boolean leftAlign;
  protected boolean groupDigits;
  protected char fmt;                  // one of cdeEfgGiosxXos
  protected boolean countSignInLen;
  private static final BigInteger bgInt = new BigInteger("9223372036854775808");  // 2^63

  /**
   * Formats a number in a printf format, like C.
   *
   @param s      the format string following printf format string
   *               The string has a prefix, a format code and a suffix. The prefix and suffix
   *               become part of the formatted output. The format code directs the
   *               formatting of the (single) parameter to be formatted. The code has the
   *               following structure
   *               <ul>
   *               <li> <b>%</b> (required)
   *
   *               <li> a modifier (optional)
   *               <dl>
   *               <dt> <dd> forces display of + for positive numbers
   *               <dt> <dd> do not count leading + or - in length
   *               <dt> <dd> show leading zeroes
   *               <dt> <dd> align left in the field
   *               <dt> space <dd> prepend a space in front of positive numbers
   *               <dt> <dd> use "alternate" format. Add 0 or 0x for octal or hexadecimal numbers.
   *               Don't suppress trailing zeroes in general floating point format.
   *               <dt> <dd> groups decimal values by thousands (for 'diuxXb' formats)
   *               </dl>
   *
   *               <li> an integer denoting field width (optional)
   *
   *               <li> a period (<b>.</b>) followed by an integer denoting precision (optional)
   *
   *               <li> a format descriptor (required)
   *               <dl>
   *               <dt><dd> floating point number in fixed format,
   *               <dt>e, E <dd> floating point number in exponential notation (scientific format).
   *               The E format results in an uppercase E for the exponent (1.14130E+003), the e
   *               format in a lowercase e,
   *               <dt>g, G <dd> floating point number in general format (fixed format for small
   *               numbers, exponential format for large numbers). Trailing zeroes are suppressed.
   *               The G format results in an uppercase E for the exponent (if any), the g format
   *               in a lowercase e,.
   *               <dt>d, i <dd> signed long and integer in decimal,
   *               <dt><dd> unsigned long or integer in decimal,
   *               <dt><dd> unsigned long or integer in hexadecimal,
   *               <dt><dd> unsigned long or integer in octal,
   *               <dt><dd> unsigned long or integer in binary,
   *               <dt><dd> string,
   *               <dt><dd> character,
   *               <dt>l, L <dd> boolean in lower or upper case (for booleans and int/longs).
   *               </dl>
   *               </ul>
   */
  public PrintfFormat(String s) {
    init(s, 0);
  }

  /**
   * For internal use with {@link #init(String, int)} and {@link #reinit(String)}.
   */
  protected PrintfFormat() {
  }

  protected PrintfFormat reinit(String s) {
    if (pre == null) {
      init(s, 0);
    else {
      init(s, pre.length());
    }
    return this;
  }

  protected void init(String s, int i) {
    width = 0;
    precision = -1;
    pre = (i == new StringBuffer() new StringBuffer(s.substring(0, i)));
    post = new StringBuffer();
    leadingZeroes = false;
    showPlus = false;
    alternate = false;
    showSpace = false;
    leftAlign = false;
    countSignInLen = true;
    fmt = ' ';

    int length = s.length();
    int parseState;                 // 0 = prefix, 1 = flags, 2 = width, 3 = precision, 4 = format, 5 = end

    // 0: parse string prefix upto first '%'.
    while (true) {
      if (i >= length) {
        throw new IllegalArgumentException("Format string requires '%'.");
      }
      char c = s.charAt(i);
      if (c != '%') {
        pre.append(c);
        i++;
        continue;
      }
      if (i >= length - 1) {
        throw new IllegalArgumentException("Format string can not end with '%'.");
      }
      if (s.charAt(i + 1== '%') {       // double '%%'
        pre.append('%');
        i += 2;
        continue;
      }
      parseState = 1;                 // single $ founded
      i++;
      break;
    }

    // 1: parse flags
    flagsloop:
    while (parseState == 1) {
      if (i >= length) {
        parseState = 5;
        break;
      }
      char c = s.charAt(i);
      switch (c) {
        case ' ': showSpace = truebreak;
        case '-': leftAlign = truebreak;
        case '+': showPlus = truebreak;
        case '0': leadingZeroes = truebreak;
        case '#': alternate = truebreak;
        case '~': countSignInLen = falsebreak;
        case ',': groupDigits = truebreak;
        default:
          parseState = 2;
          break flagsloop;
      }
      i++;
    }

    // 2: parse width
    while (parseState == 2) {
      if (i >= length) {
        parseState = 5;
        break;
      }
      char c = s.charAt(i);
      if ((c >= '0'&& (c <= '9')) {
        width = (width * 10+ s.charAt(i'0';
        i++;
        continue;
      }
      if (s.charAt(i== '.') {
        parseState = 3;
        precision = 0;
        i++;
      else {
        parseState = 4;
      }
      break;
    }

    // 3: parse precision
    while (parseState == 3) {
      if (i >= length) {
        parseState = 5;
        break;
      }
      char c = s.charAt(i);
      if ((c >= '0'&& (c <= '9')) {
        precision = (precision * 10+ s.charAt(i'0';
        i++;
        continue;
      }
      parseState = 4;
      break;
    }

    // 4: parse format
    if (parseState == 4) {
      if (i < length) {
        fmt = s.charAt(i);
        i++;        
//      } else {
//        parseState = 5;
      }
    }

    // append suffix
    if (i < length) {
      post.append(s.substring(i, length));
    }
  }

  /**
   * Formats a double with exp format.
   */
  protected String expFormat(double d) {
    StringBuilder f = new StringBuilder();
    int e = 0;
    double dd = d;
    double factor = 1;

    if (d != 0) {
      while (dd > 10) {
        e++;
        factor /= 10;
        dd /= 10;
      }
      while (dd < 1) {
        e--;
        factor *= 10;
        dd *= 10;
      }
    }
    if (((fmt == 'g'|| (fmt == 'G')) && (e >= -4&& (e < precision)) {
      return fixedFormat(d);
    }

    d *= factor;
    f.append(fixedFormat(d));

    if (fmt == 'e' || fmt == 'g') {
      f.append('e');
    else {
      f.append('E');
    }

    StringBuilder p = new StringBuilder("000");
    if (e >= 0) {
      f.append('+');
      p.append(e);
    else {
      f.append('-');
      p.append(-e);
    }

    char[] data = new char[3];
    p.getChars(p.length() 3, p.length(), data, 0);
    return f.append(data).toString();
  }

  /**
   * Formats a double with fixed format.
   */
  protected String fixedFormat(double d) {
    boolean removeTrailing = (fmt == 'G' || fmt == 'g'&& !alternate;

    // remove trailing zeroes and decimal point
    if (d > 0x7FFFFFFFFFFFFFFFL) {
      return expFormat(d);
    }
    if (precision == 0) {
      return (long) (/*+ 0.5*/(removeTrailing ? "" : StringPool.DOT);  // no rounding
    }

    long whole = (longd;
    double fr = d - whole; // fractional part

    if (fr >= || fr < 0) {
      return expFormat(d);
    }

    double factor = 1;
    StringBuilder leadingZeroesStr = new StringBuilder();

    for (int i = 1; i <= precision && factor <= 0x7FFFFFFFFFFFFFFFL; i++) {
      factor *= 10;
      leadingZeroesStr.append('0');
    }

    long l = (long) (factor * fr /*+ 0.5*/);    // no rounding
    if (l >= factor) {
      l = 0;
      whole++;
    }

    String z = leadingZeroesStr.toString() + l;
    z = '.' + z.substring(z.length() - precision, z.length());

    if (removeTrailing) {
      int t = z.length() 1;
      while (t >= && z.charAt(t== '0') {
        t--;
      }
      if (t >= && z.charAt(t== '.') {
        t--;
      }
      z = z.substring(0, t + 1);
    }
    return whole + z;
  }

  /**
   * Pads the value with spaces and adds prefix and suffix.
   */
  protected String pad(String value) {
    String spaces = repeat(' ', width - value.length());
    if (leftAlign) {
      return pre + value + spaces + post;
    else {
      return pre + spaces + value + post;
    }
  }

  /**
   * Returns new string created by repeating a single character.
   */
  protected static String repeat(char c, int n) {
    if (n <= 0) {
      return (StringPool.EMPTY);
    }
    char[] buffer = new char[n];
    for (int i = 0; i < n; i++) {
      buffer[i= c;
    }
    return new String(buffer);
  }

  protected String sign(int s, String r) {
    String p = StringPool.EMPTY;

    if (s < 0) {
      p = StringPool.DASH;
    else if (s > 0) {
      if (showPlus) {
        p = StringPool.PLUS;
      else if (showSpace) {
        p = StringPool.SPACE;
      }
    else {
      if (fmt == 'o' && alternate && r.length() && r.charAt(0!= '0') {
        p = "0";
      else if (fmt == 'x' && alternate) {
        p = "0x";
      else if (fmt == 'X' && alternate) {
        p = "0X";
      }
    }

    int w = 0;

    if (leadingZeroes) {
      w = width;
    else if ((fmt == 'u' || fmt == 'd' || fmt == 'i' || fmt == 'x' || fmt == 'X' || fmt == 'o'&& precision > 0) {
      w = precision;
    }

    if (countSignInLen) {
      return p + repeat('0', w - p.length() - r.length()) + r;
    else {
      return p + repeat('0', w - r.length()) + r;
    }
  }

  /**
   * Groups numbers by inserting 'separator' after every group of 'size' digits,
   * starting from the right.
   */
  protected String groupDigits(String value, int size, char separator) {
    if (groupDigits == false) {
      return value;
    }
    StringBuilder r = new StringBuilder(value.length() 10);
    int ndx = 0;
    int len = value.length() 1;
    int mod = len % size;
    while (ndx < len) {
      r.append(value.charAt(ndx));
      if (mod == 0) {
        r.append(separator);
        mod = size;
      }
      mod--;
      ndx++;
    }
    r.append(value.charAt(ndx));
    return r.toString();
  }



  // ---------------------------------------------------------------- public form methods

  /**
   * Formats a character into a string (like sprintf in C).
   */
  public String form(char value) {
    if (fmt != 'c') {
      throw new IllegalArgumentException("Invalid character format: '" + fmt + "' is not 'c'.");
    }
    return pad(String.valueOf(value));
  }

  /**
   * Formats a boolean into a string (like sprintf in C).
   */
  public String form(boolean value) {
    if (fmt == 'l') {
      return pad(value ? "true" "false");
    }
    if (fmt == 'L') {
      return pad(value ? "TRUE" "FALSE");
    }
    throw new IllegalArgumentException("Invalid boolean format: '" + fmt + "' is not one of 'bB'.");

  }

  /**
   * Formats a double into a string (like sprintf in C).
   */
  public String form(double x) {
    String r;

    if (precision < 0) {
      precision = 6;
    }

    int s = 1;
    if (x < 0) {
      x = -x;
      s = -1;
    }
    if (fmt == 'f') {
      r = fixedFormat(x);
    else if (fmt == 'e' || fmt == 'E' || fmt == 'g' || fmt == 'G') {
      r = expFormat(x);
    else {
      throw new IllegalArgumentException("Invalid floating format: '" + fmt + "' is not one of 'feEgG'.");
    }
    return pad(sign(s, r));
  }

  /**
   * Formats a long integer into a string (like sprintf in C).
   */
  public String form(long x) {
    String r;
    int s = 0;

    switch (fmt) {
      case 'd':
        if (x < 0) {
          r = Long.toString(x).substring(1);
          s = -1;
        else {
          r = Long.toString(x);
          s = 1;
        }
        r = groupDigits(r, 3',');
        break;
      case 'i':
        int xx = (intx;
        if (xx < 0) {
          r = Integer.toString(xx).substring(1);
          s = -1;
        else {
          r = Integer.toString(xx);
          s = 1;
        }
        r = groupDigits(r, 3',');
        break;
      case 'u':
        if (x < 0) {
          long xl = x & 0x7FFFFFFFFFFFFFFFL;
          r = Long.toString(xl);
          BigInteger bi = new BigInteger(r);
          r = bi.add(bgInt).toString();
        else {
          r = Long.toString(x);
        }
        r = groupDigits(r, 3',');
        s = 1;
        break;
      case 'o':
        r = Long.toOctalString(x);
        break;
      case 'x':
        r = Long.toHexString(x);
        r = groupDigits(r, 4' ');
        break;
      case 'X':
        r = Long.toHexString(x).toUpperCase();
        r = groupDigits(r, 4' ');
        break;
      case 'b':
        r = Long.toBinaryString(x);
        r = groupDigits(r, 8' ');
        break;
      case 'l':
        r = (x == "false" "true");
        break;
      case 'L':
        r = (x == "FALSE" "TRUE");
        break;
      default:
        throw new IllegalArgumentException("Invalid long format: '" + fmt + "' is not one of 'diuoxXblL'.");
    }

    return pad(sign(s, r));
  }

  /**
   * Formats an integer into a string (like sprintf in C).
   */
  public String form(int x) {
    String r;
    int s = 0;

    switch (fmt) {
      case 'd':
      case 'i':
        if (x < 0) {
          r = Integer.toString(x).substring(1);
          s = -1;
        else {
          r = Integer.toString(x);
          s = 1;
        }
        r = groupDigits(r, 3',');
        break;
      case 'u':
        long xl = x & 0x00000000FFFFFFFFL;
        r = Long.toString(xl);
        r = groupDigits(r, 3',');
        s = 1;
        break;
      case 'o':
        r = Integer.toOctalString(x);
        break;
      case 'x':
        r = Integer.toHexString(x);
        r = groupDigits(r, 4' ');
        break;
      case 'X':
        r = Integer.toHexString(x).toUpperCase();
        r = groupDigits(r, 4' ');
        break;
      case 'b':
        r = Integer.toBinaryString(x);
        r = groupDigits(r, 8' ');
        break;
      case 'l':
        r = (x == "false" "true");
        break;
      case 'L':
        r = (x == "FALSE" "TRUE");
        break;
      default:
        throw new IllegalArgumentException("Invalid int format: '" + fmt + "' is not one of 'diuoxXblL'.");
    }
    return pad(sign(s, r));
  }

  /**
   * Formats a string into a larger string (like sprintf in C).
   */
  public String form(String s) {
    if (fmt != 's') {
      throw new IllegalArgumentException("Invalid long format: '" + fmt + "' is not 's'.");
    }
    if (precision >= && precision < s.length()) {
      s = s.substring(0, precision);
    }

    return pad(s);
  }


}