/* $RCSfile$
* $Author$
* $Date$
* $Revision$
*
* FormatStringBuffer: printf style output formatter for Java
* Copyright (C) 2002 Antti S. Brax
* All rights reserved.
*
* Downloaded from: http://www.cs.helsinki.fi/u/abrax/HACK/JAVA/PRINTF.html
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - The names of the contributors may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openscience.cdk.tools;
import org.openscience.cdk.annotations.TestClass;
import org.openscience.cdk.annotations.TestMethod;
import java.text.DecimalFormat;
import java.text.NumberFormat;
/**
* A class for formatting output similar to the C <tt>printf</tt> command.
*
* <P>Some features provided by ANSI C-standard conformant <tt>printfs</tt>
* are not supported because of language constraints.
*
* <P>Supported conversion specifiers are: 'c', 'd', 'e', 'E', 'f', 'g'
* (works like 'f'), 'i', 'o', 's', 'x' and 'X'.
*
* <P>Supported conversion flags are: '#', '0', '-', ' ' (a space) and '+'.
*
* <P>Support for conversion flag '*' is under development.
*
* @author Antti S. Brax (asb@iki.fi, base implementation)
* @author Fred Long (flong(AT)skcc.org, implemented 'e', 'E' and 'g')
* @version 1.7
* @cdk.module standard
* @cdk.githash
* @cdk.license BSD
*/
@TestClass("org.openscience.cdk.tools.FormatStringBufferTest")
public class FormatStringBuffer {
// ==================================================================== //
/** Pad with zero instead of space. */
private static final int ZEROPAD = 1;
/** Unsigned/signed long. */
//private static final int SIGN = 2;
/** Show plus sign. */
private static final int PLUS = 4;
/** Space if plus. */
private static final int SPACE = 8;
/** Left justified. */
private static final int LEFT = 16;
/** Prepend hex digits with '0x' and octal with '0' */
private static final int SPECIAL = 32;
/** Use upper case hex digits. */
private static final int LARGE = 64;
/** Use scientific notation */
private static final int SCI = 128;
/** Use uppercase E */
private static final int UPPER = 256;
/** Use grouping character */
private static final int GROUPING = 512;
// ==================================================================== //
/** Format a char. */
private static final int CHAR = 0;
/** Format a String. */
private static final int STRING = 1;
/** Format a decimal number. */
private static final int DECIMAL = 2;
/** Format a floating point number. */
private static final int FLOAT = 3;
// ==================================================================== //
/** The format string. */
private String format = null;
/** The buffer. */
private StringBuffer buffer = null;
/** The current index. */
private int index = 0;
// ==================================================================== //
/**
* Create a new <tt>FormatStringBuffer</tt>.
*
* @param format the format string.
*/
public FormatStringBuffer(String format) {
reset(format);
}
/**
* Reset this <tt>FormatStringBuffer</tt>.
*
* @param format the format string.
*/
@TestMethod("testReset_String")
public FormatStringBuffer reset(String format) {
reset();
this.format = format;
return this;
}
/**
* Reset this <tt>FormatStringBuffer</tt> with the format string
* given in the constructor or last call to <tt>reset(String)</tt>.
* This is automatically called after <tt>toString()</tt>.
*/
@TestMethod("testReset")
public FormatStringBuffer reset() {
this.buffer = new StringBuffer();
this.index = 0;
return this;
}
// ==================================================================== //
/**
* Get the nect format token from the format string. Copy every
* character from <tt>format</tt> to <tt>buffer</tt> between
* <tt>index</tt> and the next format token.
*/
private Format getFormat() {
char ch;
while (index < format.length()) {
if ((ch = format.charAt(index)) != '%') {
buffer.append(ch);
index++;
continue;
}
Format fmt = new Format();
// Process flags.
boolean repeat = true;
while (repeat) {
if (index + 1 >= format.length())
throw new IllegalArgumentException("Malformed format");
switch (ch = format.charAt(++index)) { // Skip the first '%'
case '-': fmt.flags |= LEFT; break;
case '+': fmt.flags |= PLUS; break;
case ' ': fmt.flags |= SPACE; break;
case '#': fmt.flags |= SPECIAL; break;
case '0': fmt.flags |= ZEROPAD; break;
case '\'': fmt.flags |= GROUPING; break;
default: repeat = false; break;
}
}
// Get field width.
if (Character.isDigit(ch)) {
// Explicit number.
fmt.fieldWidth = skipDigits();
}
if (index >= format.length())
throw new IllegalArgumentException("Malformed format");
// Get precision.
if ((ch = format.charAt(index)) == '.') {
if (++index >= format.length())
throw new IllegalArgumentException("Malformed format");
fmt.precision = skipDigits();
if (fmt.precision < 0) {
fmt.precision = 0;
}
}
if (index >= format.length())
throw new IllegalArgumentException("Malformed format");
switch (ch = format.charAt(index++)) {
case 'c':
fmt.type = CHAR;
return fmt;
case 's':
fmt.type = STRING;
return fmt;
case '%':
buffer.append('%');
continue;
// Octal, hexadecimal and decimal.
case 'o':
fmt.type = DECIMAL;
fmt.base = 8;
return fmt;
case 'X':
fmt.flags |= LARGE;
case 'x':
fmt.type = DECIMAL;
fmt.base = 16;
return fmt;
case 'd':
case 'i':
fmt.type = DECIMAL;
return fmt;
// Floating point
case 'f':
case 'g':
fmt.type = FLOAT;
return fmt;
case 'e':
fmt.type = FLOAT;
fmt.flags |= SCI;
return fmt;
case 'E':
fmt.type = FLOAT;
fmt.flags |= SCI;
fmt.flags |= UPPER;
return fmt;
default:
buffer.append('%');
buffer.append(ch);
continue;
}
}
return null;
}
/**
* Skip digits and return the number they form.
*/
private int skipDigits() {
char ch;
int i = 0;
while (index < format.length()) {
if (Character.isDigit(ch = format.charAt(index))) {
index++;
i = i * 10 + Character.digit(ch, 10);
} else {
break;
}
}
return i;
}
// ==================================================================== //
/**
* Format a <tt>char</tt>.
*/
@TestMethod("testFormat_char")
public FormatStringBuffer format(char ch) {
Format fmt = getFormat();
if (fmt.type != CHAR)
throw new IllegalArgumentException("Expected a char format");
if ((fmt.flags & LEFT) != LEFT)
while (--fmt.fieldWidth > 0)
buffer.append(' ');
buffer.append(ch);
while (--fmt.fieldWidth > 0)
buffer.append(' ');
return this;
}
/**
* Format a <tt>float</tt>.
*/
@TestMethod("testFormat_floatr")
public FormatStringBuffer format(float flt) {
return format((double)flt);
}
/**
* Format a <tt>double</tt>.
*/
@TestMethod("testFormat_double")
public FormatStringBuffer format(double dbl) {
Format fmt = getFormat();
if (fmt.type != FLOAT)
throw new IllegalArgumentException("Expected a float format");
NumberFormat nf;
if ((fmt.flags & SCI) > 0) {
nf = new DecimalFormat("0.#E00");
} else {
nf = NumberFormat.getInstance();
}
nf.setGroupingUsed((fmt.flags & GROUPING) != 0);
if (fmt.precision != -1) {
nf.setMaximumFractionDigits(fmt.precision);
nf.setMinimumFractionDigits(fmt.precision);
} else {
nf.setMaximumFractionDigits(Integer.MAX_VALUE);
nf.setMinimumFractionDigits(1);
}
String str = nf.format(dbl);
if ((fmt.flags & SCI) == SCI && (fmt.flags & UPPER) == 0) {
str = str.replace('E', 'e');
}
if ((fmt.flags & PLUS) == PLUS && dbl >= 0.0)
str = "+" + str;
int len = str.length();
if ((fmt.flags & LEFT) != LEFT)
while (len < fmt.fieldWidth--)
buffer.append(' ');
for (int i = 0; i < len; ++i)
buffer.append(str.charAt(i));
while (len < fmt.fieldWidth--)
buffer.append(' ');
return this;
}
/**
* Format a <tt>float</tt>.
*/
@TestMethod("testFormat_int")
public FormatStringBuffer format(int i) {
return format((long)i);
}
/**
* Format a <tt>float</tt>.
*/
@TestMethod("testFormat_long")
public FormatStringBuffer format(long l) {
Format fmt = getFormat();
if (fmt.type != DECIMAL)
throw new IllegalArgumentException("Expected a float format");
// Decide padding character.
char pad = ' ';
if ((fmt.flags & ZEROPAD) == ZEROPAD) {
pad = '0';
}
// Convert numberto String.
String str;
String prefix = "";
switch (fmt.base) {
case 8:
str = Long.toOctalString(l);
if ((fmt.flags & SPECIAL) == SPECIAL) {
fmt.fieldWidth -= 1;
prefix = "0";
}
break;
case 16:
str = Long.toHexString(l);
if ((fmt.flags & SPECIAL) == SPECIAL) {
fmt.fieldWidth -= 2;
prefix = "0x";
}
break;
default:
str = String.valueOf(Math.abs(l));
break;
}
if ((fmt.flags & LARGE) == LARGE) {
str = str.toUpperCase();
prefix = prefix.toUpperCase();
}
int len = str.length();
if (l < 0 || (fmt.flags & PLUS) == PLUS) {
fmt.fieldWidth--;
}
// Place the sign character first if zero padding.
if ((fmt.flags & ZEROPAD) == ZEROPAD) {
if (l < 0 && fmt.base == 10) {
buffer.append('-');
} else if ((fmt.flags & PLUS) == PLUS && fmt.base == 10) {
buffer.append('+');
}
buffer.append(prefix);
}
// Pad.
if ((fmt.flags & LEFT) != LEFT)
while (len < fmt.fieldWidth--)
buffer.append(pad);
// Place the sign character now if not zero padding.
if ((fmt.flags & ZEROPAD) != ZEROPAD) {
if (l < 0 && fmt.base == 10) {
buffer.append('-');
} else if ((fmt.flags & PLUS) == PLUS && fmt.base == 10) {
buffer.append('+');
}
buffer.append(prefix);
}
for (int i = 0; i < len; ++i)
buffer.append(str.charAt(i));
while (len < fmt.fieldWidth--)
buffer.append(' ');
return this;
}
/**
* Format a <tt>String</tt>.
*/
@TestMethod("testFormat_String")
public FormatStringBuffer format(String str) {
if (str == null)
str = "<NULL>";
Format fmt = getFormat();
if (fmt.type != STRING)
throw new IllegalArgumentException("Expected a String format");
int len = str.length();
if (fmt.precision != -1 && len > fmt.precision)
len = fmt.precision;
if ((fmt.flags & LEFT) != LEFT)
while (len < fmt.fieldWidth--)
buffer.append(' ');
for (int i = 0; i < len; ++i)
buffer.append(str.charAt(i));
while (len < fmt.fieldWidth--)
buffer.append(' ');
return this;
}
// ==================================================================== //
/**
* Get the result of the formatting. <tt>reset()</tt> is automatically
* called from this method.
*/
@TestMethod("testToString")
public String toString() {
if (index < format.length())
buffer.append(format.substring(index));
String str = buffer.toString();
this.reset();
return str;
}
// ==================================================================== //
/**
* A container class for several format parameters.
*/
private class Format {
public int flags = 0;
public int fieldWidth = -1;
public int precision = -1;
public int type = -1;
public int base = 10;
}
}