package cafeaulait.io;


/** 
 *
 * This class provides C-like formatting functions that allow
 * programmers to convert an integer or floating point number
 * into a string with a specified, width, precision and format. 
 * For instance this might be used to format monetary data to 
 * two decimal places.<P>
 *
 * Because Java does not have variable length argument lists like C,
 * a different strategy must be employed. Each number is passed
 * to a format() method along with formatting instructions. The format()
 * method returns a formatted string which may then be passed to
 * System.out.println() or other methods. In short this is more
 * similar to C's sprintf() than to printf(). <P>
 *
 * There are a number of possible things one can do when a number
 * will not fit inside the specified format. You can throw an exception,
 * truncate the number, return an error string, or expand the width.
 * Here I've chosen to expand the width. <P>
 *
 * The rounding of these precisions still needs work. Currently 
 * excess digits are merely truncated.<P>
 *
 * @Version: 0.1 of August 11, 1997
 * @Author: Elliotte Rusty Harold (elharo@sunsite.unc.edu)
 */
 
public class Formatter {

  /* Since this class only contains static utility methods there's
  no reason to allow instances of it to be created */
  
  private Formatter() {
  }
  
  // longs and ints have different default widths,
  // otherwise they're treated the same
  // I used the maximum necessary widths for the type. An
  // alternative soultion would be to use only as many characters 
  // as necessary

  /** 
   *
   * This method formats an int in 10 characters
   * with no leading zeroes,
   * right aligned, without +
   * signs on positive numbers, filled out to the specified width
   * with spaces
   *
   * @param d The number to be formatted
   * @return A String containing a formatted representation of the number
   */
  public static String format(int i) {
    return format(i, 11, -1, false, false, ' ', 10);
  }

  /** 
   *
   * This method formats a long in 20 characters
   * with no leading zeroes,
   * right aligned, without +
   * signs on positive numbers, filled out to the specified width
   * with spaces
   *
   * @param d The number to be formatted
   * @return A String containing a formatted representation of the number
   */
  public static String format(long l) {
    return format(l, 20, -1, false, false, ' ', 10);
  }

  // Rather than providing all possible permutations of arguments
  // (64 in this case) it's customary to provide the most common
  // options in the most common order

  /** 
   *
   * This method formats an integer to a specified width,
   * no leading zeroes,
   * right aligned, without +
   * signs on positive numbers, filled out to the specified width
   * with spaces
   *
   * @param d The number to be formatted
   * @param width The minimum number of characters in the returned string
   * @return A String containing a formatted representation of the number
   */
  public static String format(long l, int width) {
    return format(l, width, -1, false, false, ' ', 10);
  }

  /** 
   *
   * This method formats an integer to a specified width,
   * number of leading zeroes,
   * right aligned, without +
   * signs on positive numbers, filled out to the specified width
   * with spaces
   *
   * @param d The number to be formatted
   * @param width The minimum number of characters in the returned string
   * @param precision The number of leading zeroes
   * @return A String containing a formatted representation of the number
   */
  public static String format(long l, int width, int precision) {
    return format(l, width, precision, false, false, ' ', 10);
  }

  /** 
   *
   * This method formats an integer to a specified width,
   * number of leading zeroes,
   * aligned left or right, without +
   * signs on positive numbers, filled out to the specified width
   * with spaces
   *
   * @param d The number to be formatted
   * @param width The minimum number of characters in the returned string
   * @param precision The number of leading zeroes
   * @param alignLeft True for left alignment in the width, 
   * false for right alignment
   * @return A String containing a formatted representation of the number
   */
  public static String format(long l, int width, int precision, 
   boolean alignLeft) {
    return format(l, width, precision, alignLeft, false, ' ', 10);
  }

  /** 
   *
   * This method formats an integer to a specified width,
   * number of leading zeroes,
   * aligned left or right, with or without +
   * signs on positive numbers, filled out to the specified width
   * with spaces
   *
   * @param d The number to be formatted
   * @param width The minimum number of characters in the returned string
   * @param precision The number of leading zeroes
   * @param alignLeft True for left alignment in the width, 
   * false for right alignment
   * @param usePlus True if positive numbers are prefixed with + signs
   * @return A String containing a formatted representation of the number
   */
  public static String format(long l, int width, int precision, 
   boolean alignLeft, boolean usePlus) {
    return format(l, width, precision, alignLeft, usePlus, ' ', 10);
  }

  /** 
   *
   * This method formats an integer to a specified width,
   * number of leading zeroes,
   * aligned left or right, with or without +
   * signs on positive numbers, with a user-specified padding 
   * character used to fill out the number to the specified width.
   *
   * @param d The number to be formatted
   * @param width The minimum number of characters in the returned string
   * @param precision The number of leading zeroes
   * @param alignLeft True for left alignment in the width, 
   * false for right alignment
   * @param usePlus True if positive numbers are prefixed with + signs
   * @param pad The character used to fill out the number 
   * to the specified width
   * @return A String containing a formatted representation of the number
   */
  public static String format(long l, int width, 
   int precision, boolean alignLeft, 
   boolean usePlus, char pad) {
    return format(l, width, precision, alignLeft, usePlus, pad, 10);
  }

  /** 
   *
   * This method formats an integer to a specified width,
   * number of leading zeroes,
   * aligned left or right, with or without +
   * signs on positive numbers, with a user-specified padding 
   * character used to fill out the number to the specified width,
   * in a specified base
   *
   * @param d The number to be formatted
   * @param width The minimum number of characters in the returned string
   * @param precision The number of leading zeroes
   * @param alignLeft True for left alignment in the width, 
   * false for right alignment
   * @param usePlus True if positive numbers are prefixed with + signs
   * @param pad The character used to fill out the number 
   * to the specified width
   * @param radix The base in which numbers are formatted; e.g. 8, 10, 16
   * @return A String containing a formatted representation of the number
   */
  public static String format(long l, int width, 
   int precision, boolean alignLeft, 
   boolean usePlus, char pad, int radix) {
  
    // Do the initial conversion
    String s = Long.toString(Math.abs(l), radix);
    
    // add leading zeroes if necessary
    if (precision > s.length()) {
       int padLength = precision - s.length();
       String zeroes = "";
       for (int i = 0; i < padLength; i++) zeroes += '0';
       s = zeroes + s;
    }
    
    // add the sign
    if (usePlus && l > 0) s = '+' + s;
    else if (l < 0) s = '-' + s;
    else s = ' ' + s;

    if (s.length() < width) {
       int padLength = width - s.length();
       String padding = "";
       for (int i = 0; i < padLength; i++) padding += pad;
       if (alignLeft) s += padding;
       else s = padding + s;
    }
    
    return s;

  }

  /** 
   *
   * This method formats a floating point number to a minimum of 12 characters,
   * six decimal places, in decimal format,
   * right aligned, without +
   * signs on positive numbers. Spaces 
   * fill out the number to the specified width.
   *
   * @param d The number to be formatted
   * @return A String containing a formatted representation of the number.
   */
  public static String format(double d) {
    return format(d, 12, 6, false, false, false, ' ');
  }

  /** 
   *
   * This method formats a floating point number to specified width,
   * six decimal places, in decimal format,
   * right aligned, without +
   * signs on positive numbers. Spaces 
   * fill out the number to the specified width.
   *
   * @param d The number to be formatted
   * @param width The minimum number of characters in the returned string
   * @return A String containing a formatted representation of the number.
   */
  public static String format(double d, int width) {
    return format(d, width, 6, false, false, false, ' ');
  }

  /** 
   *
   * This method formats a floating point number to specified width,
   * number of decimal places, in decimal format,
   * right aligned, without +
   * signs on positive numbers. Spaces 
   * fill out the number to the specified width.
   *
   * @param d The number to be formatted
   * @param width The minimum number of characters in the returned string
   * @param precision The number of digits after the decimal point
   * @return A String containing a formatted representation of the number.
   */
  public static String format(double d, int width, int precision) {
    return format(d, width, precision, false, false, false, ' ');
  }

  /** 
   *
   * This method formats a floating point number to specified width,
   * number of decimal places, in exponential or decimal format,
   * right aligned, without +
   * signs on positive numbers. Spaces 
   * fill out the number to the specified width.
   *
   * @param d The number to be formatted
   * @param width The minimum number of characters in the returned string
   * @param precision The number of digits after the decimal point
   * @param exponential true if the number is to be displayed in 
   * exponential notation, e.g. 3.50E+09, false otherwise
   * @return A String containing a formatted representation of the number.
   */
  public static String format(double d, int width, 
   int precision, boolean exponential) {
    return format(d, width, precision, exponential, false, false, ' ');
  }

  /** 
   *
   * This method formats a floating point number to a specified width,
   * number of decimal places, in exponential or decimal format,
   * aligned left or right, without +
   * signs on positive numbers. Spaces 
   * fill out the number to the specified width.
   *
   * @param d The number to be formatted
   * @param width The minimum number of characters in the returned string
   * @param precision The number of digits after the decimal point
   * @param exponential true if the number is to be displayed in 
   * exponential notation, e.g. 3.50E+09, false otherwise
   * @param alignLeft True for left alignment in the width, 
   * false for right alignment
   * @return A String containing a formatted representation of the number.
   */
  public static String format(double d, int width, 
   int precision, boolean exponential, boolean alignLeft) {
    return format(d, width, precision, exponential, alignLeft, false, ' ');
  }

  /** 
   *
   * This method formats a floating point number to a specified width,
   * number of decimal places, in exponential or decimal format,
   * aligned left or right, with or without +
   * signs on positive numbers. Spaces 
   * fill out the number to the specified width.
   *
   * @param d The number to be formatted
   * @param width The minimum number of characters in the returned string
   * @param precision The number of digits after the decimal point
   * @param exponential true if the number is to be displayed in 
   * exponential notation, e.g. 3.50E+09, false otherwise
   * @param alignLeft True for left alignment in the width, 
   * false for right alignment
   * @param usePlus True if positive numbers are prefixed with + signs
   * @return A String containing a formatted representation of the number.
   */
  public static String format(double d, int width, 
   int precision, boolean exponential, boolean alignLeft, 
   boolean usePlus) {
    return format(d, width, precision, exponential, alignLeft, usePlus, ' ');
  }

  // need to round better when truncating precision
  /** 
   *
   * This method formats a floating point number to a specified width,
   * number of decimal places, in exponential or decimal format,
   * aligned left or right, with or without +
   * signs on positive numbers, and with a user-specified padding 
   * character used to fill out the number to the specified width.
   *
   * @param d The number to be formatted
   * @param width The minimum number of characters in the returned string
   * @param precision The number of digits after the decimal point
   * @param exponential true if the number is to be displayed in 
   * exponential notation, e.g. 3.50E+09, false otherwise
   * @param alignLeft True for left alignment in the width, 
   * false for right alignment
   * @param usePlus True if positive numbers are prefixed with + signs
   * @param pad The character used to fill out the number 
   * to the specified width
   * @return A String containing a formatted representation of the number.
   */
  public static String format(double d, int width, 
   int precision, boolean exponential, boolean alignLeft, 
   boolean usePlus, char pad) {

    String s = "";
    
    // find mantissa and exponent
    double y = Math.abs(d);
    String mantissa;
    int exponent;
         
    if (Double.isInfinite(y)) {
      s = "Inf";    
    }
    else if (Double.isNaN(y)) {
      s = "NaN";
    }
    else {  // find mantissa and exponent
	    s = Double.toString(y);
	    int e = s.toUpperCase().indexOf('E');
	    if (e != -1) { // in exponential form
	      mantissa = s.substring(0, e);
	      // remove the decimal point
	      // which is always to the right of the first digit
	      mantissa = mantissa.charAt(0) + mantissa.substring(2);
	      exponent = Integer.parseInt(s.substring(e+1)); 
	    }  // end exponential form
	    else { // in decimal form to start with
	      int decimalpoint = s.indexOf('.');
	      mantissa = s.substring(0, decimalpoint) + s.substring(decimalpoint+1);
	      // find first non-zero digit
	      int r = -1;
	      for (int i = 0; i < s.length(); i++) {
	        char c = s.charAt(i);
	        if (c != '0' && Character.isDigit(c)) {
	          r = i;
	          break;
	        }
	      }
	      if (r == -1) { // this has to zero
	        exponent = 0;
	      }
	      else {
	        if (r < decimalpoint) exponent = decimalpoint - r - 1;
	        else exponent = decimalpoint - r;
	      }	      
	    } // end decimal form
        
	    if (exponential) {
	      while (mantissa.length() < precision + 1) mantissa += '0';
	      s = mantissa.charAt(0) + "." + mantissa.substring(1,precision+1);
	      String exponentString = "E";
	      if (exponent >= 0) exponentString += "+";
	      else exponentString += "-";
	      if (Math.abs(exponent) < 10) exponentString += "0";
	      exponentString += Math.abs(exponent);    
	      s += exponentString;  
	    }
	    else { // use decimal notation
	      // convert the exponent to a decimal point
	      // with extra zeroes if necessary
	      if (exponent < 0) {
	        String prefix = "0.";
	        for (int i = exponent; i < -1; i++) prefix += '0';
	        s = prefix + mantissa;
	        if (precision >= 0) s = s.substring(0, precision+3);
	      }
	      else if (exponent < mantissa.length()-1 ) {
	        s = mantissa.substring(0, exponent+1) + '.' 
	         + mantissa.substring(exponent+1);
	         if (precision >= 0) {
	           int end = exponent + 1 + precision;
	           if (end < s.length()) s = s.substring(0, end+1);
	           else {
	             for (int i = 0; i < end - s.length() + 1; i++) s += '0';             
	           }
	         }
	      }
	      else { // exponent >= mantissa.length()
	        s = mantissa;
	        for (int i = 0; i < exponent - mantissa.length() + 1; i++) s += '0';
	        s += ".";
	        for (int i = 0; i < precision; i++) s += '0';
	      }	      
	    }  
    }  
    
    
    // add the sign
    if (d < 0) s = '-' + s;
    else if (d == 0.0) s = ' ' + s;
    // > 0 is not superfluous due to NaN
    else if (usePlus && d > 0) s = '+' + s;
    
    // expand the width
    if (s.length() < width) {
       int padLength = width - s.length();
       String padding = "";
       for (int i = 0; i < padLength; i++) padding += pad;
       if (alignLeft) s += padding;
       else s = padding + s;
    }
    
    return s;

  }
  
  
  // still need to handle %g
  /** 
   *
   * This method formats a floating point number to a specified width,
   * number of leading zeroes,
   * aligned left or right, with or without +
   * signs on positive numbers, with a user-specified padding 
   * character used to fill out the number to the specified width,
   * in a specified base using a C-like formatting string such as 
   * %5f, %15.4F, %-15.3E, %.10f, and so on.<P>  
   * Currently only
   * f, F, e, and E formats are supported. g and G formats are not supported
   *
   * @param d The number to be formatted
   * @param s A String containing formatting instructions
   * @return A String containing a formatted representation of the number
   */
  public static String format(double d, String s) {
   
    int width = -1;
    int precision = 6; 
    boolean exponential = false;
    boolean alignLeft = false;    
    boolean usePlus = false; 
    
    if (s.charAt(0) == '%') s = s.substring(1);
    if (s.charAt(0) == '-') {
      alignLeft = true;
      s = s.substring(1);
    }
    if (s.toUpperCase().endsWith("E")) exponential = true;
    int period = s.indexOf(".");
    if (period != -1) {
      try {
        String prec = s.substring(period+1, s.length()-1);
        precision = Integer.parseInt(prec);
      } 
      catch (Exception e) {
      } 
      try {
        String w = s.substring(0, period);
        width = Integer.parseInt(w);
      } 
      catch (Exception e) {
      } 
    } // end if
    else {
      try {
        String w = s.substring(0, s.length()-1);
        width = Integer.parseInt(w);
      } 
      catch (Exception e) {
      }       
    } // end else
    
    return format(d, width, precision, exponential, alignLeft, usePlus);
   
  }
  
  /** 
   *
   * This method formats an integer to a specified width,
   * number of leading zeroes,
   * aligned left or right, with or without +
   * signs on positive numbers,
   * in octal, decimal, or hexadecimal 
   * using a C-like formatting string such as 
   * %5d, %15.4d, %-15.3x, %.10X, and so on.<P>  
   *
   * @param d The number to be formatted
   * @param s A String containing formatting instructions
   * @return A String containing a formatted representation of the number
   */
  public static String format(long l, String s) {
   
    int width = -1;
    int precision = 0; 
    int radix = 10;
    boolean alignLeft = false; 
    boolean usePlus = false; 
    
    if (s.charAt(0) == '%') s = s.substring(1);
    if (s.charAt(0) == '-') {
      alignLeft = true;
      s = s.substring(1);
    }
    if (s.toUpperCase().endsWith("X")) radix = 16;
    else if (s.toUpperCase().endsWith("O")) radix = 8;

    int period = s.indexOf(".");
    if (period != -1) {
      try {
        String prec = s.substring(period+1, s.length()-1);
        precision = Integer.parseInt(prec);
      } 
      catch (Exception e) {
      } 
      try {
        String w = s.substring(0, period);
        width = Integer.parseInt(w);
      } 
      catch (Exception e) {
      } 
    } // end if
    else {
      try {
        String w = s.substring(0, s.length()-1);
        width = Integer.parseInt(w);
      } 
      catch (Exception e) {
      }       
    } // end else
    
    return format(l, width, precision, alignLeft, usePlus, ' ', radix);
   
  }
    
  public static void main(String[] args) {
  
    if (args == null || args.length == 0) test();
    else {
      for (int i = 0; i < args.length; i++) {
        long n = 0;
        double x = 0;
        try {
          n = Long.valueOf(args[i]).longValue();
          testInteger(n);
        }
        catch (NumberFormatException e1) {
          try {
            x = Double.valueOf(args[i]).doubleValue();
            testFloatingPoint(x);
          }
          catch (NumberFormatException e2) {
            System.err.println(args[i] + "is not a number.");
          }
        }
      }  // end for
    
    } // end else
  
  } // end main()


  private static void test() {
  
    for (int i = -10; i <= 10; i+= 5) testInteger(i);
    for (int i = 1; i < 10; i++) {
      testInteger((long) Math.floor(Math.random() * 1000000));
    }
    testFloatingPoint(7.5);
    testFloatingPoint(Math.PI);
    testFloatingPoint(Math.E);
    testFloatingPoint(897.6);
    testFloatingPoint(765);
    testFloatingPoint(-98.765);
    testFloatingPoint(65.4E23);
    testFloatingPoint(65.4E-23);
    testFloatingPoint(-65.4E23);
    testFloatingPoint(-65.4E-23);
    testFloatingPoint(0.0);
    testFloatingPoint(1);
    testFloatingPoint(Math.sqrt(2));
    testFloatingPoint(Double.POSITIVE_INFINITY);
    testFloatingPoint(Double.NEGATIVE_INFINITY);
    testFloatingPoint(Double.NaN);
  }

  private static void testInteger(long n) {
  
    String s;
    s = format(n, 16, 4, false, false, ' ');
    System.out.print(s);
    s = format(n, 10, 4, false, false, ' ');
    System.out.print(s);
    s = format(n, 4, -1, true, true, '_');
    System.out.print(s);
    s = format(n, -1, -1, true, true, ' ');
    System.out.print(s);
    System.out.println();
  
  }

  // should also print non-formatted value for comparison

  private static void testFloatingPoint(double x) {

    System.out.println("Unformatted: " + x);
    String s = format(x);
    System.out.println(s);
    s = format(x, 10, 4, true);
    System.out.println(s);
    
  }


}

