/* * Javolution - Java(TM) Solution for Real-Time and Embedded Systems * Copyright (C) 2012 - Javolution (http://javolution.org/) * All rights reserved. * * Permission to use, copy, modify, and distribute this software is * freely granted, provided that this notice is preserved. */ package javolution.text; import java.io.Serializable; import javolution.lang.MathLib; /** * <p> An {@link Appendable} text whose capacity expands * gently without incurring expensive resize/copy operations ever.</p> * * <p> This class is not intended for large documents manipulations which * should be performed with the {@link Text} class directly * (<code>O(Log(n))</code> {@link Text#insert insertion} and * {@link Text#delete deletion} capabilities).</p> * * <p> The textual format of any appended object is retrieved * from the current {@link TextContext}.</p> * * @author <a href="mailto:jean-marie@dautelle.com">Jean-Marie Dautelle</a> * @version 5.3, January 20, 2008 */ public class TextBuilder implements Appendable, CharSequence, Serializable { private static final long serialVersionUID = 0x600L; // Version. // We do a full resize (and copy) only when the capacity is less than C1. // For large collections, multi-dimensional arrays are employed. private static final int B0 = 5; // Initial capacity in bits. private static final int C0 = 1 << B0; // Initial capacity (32) private static final int B1 = 10; // Low array maximum capacity in bits. private static final int C1 = 1 << B1; // Low array maximum capacity (1024). private static final int M1 = C1 - 1; // Mask. // Resizes up to 1024 maximum (32, 64, 128, 256, 512, 1024). private char[] _low = new char[C0]; // For larger capacity use multi-dimensional array. private char[][] _high = new char[1][]; /** * Holds the current length. */ private int _length; /** * Holds current capacity. */ private int _capacity = C0; /** * Creates a text builder of small initial capacity. */ public TextBuilder() { _high[0] = _low; } /** * Creates a text builder holding the specified <code>String</code> * (convenience method). * * @param str the initial string content of this text builder. */ public TextBuilder(String str) { this(); append(str); } /** * Creates a text builder of specified initial capacity. * Unless the text length exceeds the specified capacity, operations * on this text builder will not allocate memory. * * @param capacity the initial capacity. */ public TextBuilder(int capacity) { this(); while (capacity > _capacity) { increaseCapacity(); } } /** * Returns the length (character count) of this text builder. * * @return the number of characters (16-bits Unicode). */ public final int length() { return _length; } /** * Returns the character at the specified index. * * @param index the index of the character. * @return the character at the specified index. * @throws IndexOutOfBoundsException if * <code>(index < 0) || (index >= this.length())</code>. */ public final char charAt(int index) { if (index >= _length) throw new IndexOutOfBoundsException(); return index < C1 ? _low[index] : _high[index >> B1][index & M1]; } /** * Copies the character from this text builder into the destination * character array. * * @param srcBegin this text start index. * @param srcEnd this text end index (not included). * @param dst the destination array to copy the data into. * @param dstBegin the offset into the destination array. * @throws IndexOutOfBoundsException if <code>(srcBegin < 0) || * (dstBegin < 0) || (srcBegin > srcEnd) || (srcEnd > this.length()) * || ((dstBegin + srcEnd - srcBegin) > dst.length)</code> */ public final void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) { if ((srcBegin < 0) || (srcBegin > srcEnd) || (srcEnd > this._length)) throw new IndexOutOfBoundsException(); for (int i = srcBegin, j = dstBegin; i < srcEnd;) { char[] chars0 = _high[i >> B1]; int i0 = i & M1; int length = MathLib.min(C1 - i0, srcEnd - i); System.arraycopy(chars0, i0, dst, j, length); i += length; j += length; } } /** * Sets the character at the specified position. * * @param index the index of the character to modify. * @param c the new character. * @throws IndexOutOfBoundsException if <code>(index < 0) || * (index >= this.length())</code> */ public final void setCharAt(int index, char c) { if ((index < 0) || (index >= _length)) throw new IndexOutOfBoundsException(); _high[index >> B1][index & M1] = c; } /** * Convenience method equivalent to {@link #setLength(int, char) * setLength(newLength, '\u0000')}. * * @param newLength the new length of this builder. * @throws IndexOutOfBoundsException if <code>(newLength < 0)</code> */ public final void setLength(int newLength) { setLength(newLength, '\u0000'); } /** * Sets the length of this character builder. * If the length is greater than the current length; the * specified character is inserted. * * @param newLength the new length of this builder. * @param fillChar the character to be appended if required. * @throws IndexOutOfBoundsException if <code>(newLength < 0)</code> */ public final void setLength(int newLength, char fillChar) { if (newLength < 0) throw new IndexOutOfBoundsException(); if (newLength <= _length) _length = newLength; else for (int i = _length; i++ < newLength;) { append(fillChar); } } /** * Returns a {@link java.lang.CharSequence} corresponding * to the character sequence between the specified indexes. * * @param start the index of the first character inclusive. * @param end the index of the last character exclusive. * @return a character sequence. * @throws IndexOutOfBoundsException if <code>(start < 0) || (end < 0) || * (start > end) || (end > this.length())</code> */ public final java.lang.CharSequence subSequence(int start, int end) { if ((start < 0) || (end < 0) || (start > end) || (end > _length)) throw new IndexOutOfBoundsException(); return Text.valueOf(this, start, end); } /** * Appends the specified character. * * @param c the character to append. * @return <code>this</code> */ public final TextBuilder append(char c) { if (_length >= _capacity) increaseCapacity(); _high[_length >> B1][_length & M1] = c; _length++; return this; } /** * Appends the textual representation of the specified object. * This method is equivalent to * {@code TextContext.getFormat(obj.getClass()).format(obj, this)} */ public final TextBuilder append(Object obj) { if (obj == null) return append("null"); TextFormat<Object> textFormat = TextContext.getFormat(obj.getClass()); if (textFormat == null) return append(obj.toString()); return textFormat.format(obj, this); } /** * Appends the specified character sequence. If the specified character * sequence is <code>null</code> this method is equivalent to * <code>append("null")</code>. * * @param csq the character sequence to append or <code>null</code>. * @return <code>this</code> */ public final TextBuilder append(CharSequence csq) { return (csq == null) ? append("null") : append(csq, 0, csq.length()); } /** * Appends a subsequence of the specified character sequence. * If the specified character sequence is <code>null</code> this method * is equivalent to <code>append("null")</code>. * * @param csq the character sequence to append or <code>null</code>. * @param start the index of the first character to append. * @param end the index after the last character to append. * @return <code>this</code> * @throws IndexOutOfBoundsException if <code>(start < 0) || (end < 0) * || (start > end) || (end > csq.length())</code> */ public final TextBuilder append(CharSequence csq, int start, int end) { if (csq == null) return append("null"); if ((start < 0) || (end < 0) || (start > end) || (end > csq.length())) throw new IndexOutOfBoundsException(); for (int i = start; i < end;) { append(csq.charAt(i++)); } return this; } /** * Appends the specified string to this text builder. * If the specified string is <code>null</code> this method * is equivalent to <code>append("null")</code>. * * @param str the string to append or <code>null</code>. * @return <code>this</code> */ public final TextBuilder append(String str) { return (str == null) ? append("null") : append(str, 0, str.length()); } /** * Appends a subsequence of the specified string. * If the specified character sequence is <code>null</code> this method * is equivalent to <code>append("null")</code>. * * @param str the string to append or <code>null</code>. * @param start the index of the first character to append. * @param end the index after the last character to append. * @return <code>this</code> * @throws IndexOutOfBoundsException if <code>(start < 0) || (end < 0) * || (start > end) || (end > str.length())</code> */ public final TextBuilder append(String str, int start, int end) { if (str == null) return append("null"); if ((start < 0) || (end < 0) || (start > end) || (end > str.length())) throw new IndexOutOfBoundsException("start: " + start + ", end: " + end + ", str.length(): " + str.length()); int newLength = _length + end - start; while (_capacity < newLength) { increaseCapacity(); } for (int i = start, j = _length; i < end;) { char[] chars = _high[j >> B1]; int dstBegin = j & M1; int inc = MathLib.min(C1 - dstBegin, end - i); str.getChars(i, (i += inc), chars, dstBegin); j += inc; } _length = newLength; return this; } /** * Appends the specified text to this text builder. * If the specified text is <code>null</code> this method * is equivalent to <code>append("null")</code>. * * @param txt the text to append or <code>null</code>. * @return <code>this</code> */ public final TextBuilder append(Text txt) { return (txt == null) ? append("null") : append(txt, 0, txt.length()); } /** * Appends a subsequence of the specified text. * If the specified character sequence is <code>null</code> this method * is equivalent to <code>append("null")</code>. * * @param txt the text to append or <code>null</code>. * @param start the index of the first character to append. * @param end the index after the last character to append. * @return <code>this</code> * @throws IndexOutOfBoundsException if <code>(start < 0) || (end < 0) * || (start > end) || (end > txt.length())</code> */ public final TextBuilder append(Text txt, int start, int end) { if (txt == null) return append("null"); if ((start < 0) || (end < 0) || (start > end) || (end > txt.length())) throw new IndexOutOfBoundsException(); int newLength = _length + end - start; while (_capacity < newLength) { increaseCapacity(); } for (int i = start, j = _length; i < end;) { char[] chars = _high[j >> B1]; int dstBegin = j & M1; int inc = MathLib.min(C1 - dstBegin, end - i); txt.getChars(i, (i += inc), chars, dstBegin); j += inc; } _length = newLength; return this; } /** * Appends the characters from the char array argument. * * @param chars the character array source. * @return <code>this</code> */ public final TextBuilder append(char chars[]) { append(chars, 0, chars.length); return this; } /** * Appends the characters from a subarray of the char array argument. * * @param chars the character array source. * @param offset the index of the first character to append. * @param length the number of character to append. * @return <code>this</code> * @throws IndexOutOfBoundsException if <code>(offset < 0) || * (length < 0) || ((offset + length) > chars.length)</code> */ public final TextBuilder append(char chars[], int offset, int length) { final int end = offset + length; if ((offset < 0) || (length < 0) || (end > chars.length)) throw new IndexOutOfBoundsException(); int newLength = _length + length; while (_capacity < newLength) { increaseCapacity(); } for (int i = offset, j = _length; i < end;) { char[] dstChars = _high[j >> B1]; int dstBegin = j & M1; int inc = MathLib.min(C1 - dstBegin, end - i); System.arraycopy(chars, i, dstChars, dstBegin, inc); i += inc; j += inc; } _length = newLength; return this; } /** * Appends the textual representation of the specified <code>boolean</code> * argument. * * @param b the <code>boolean</code> to format. * @return <code>this</code> * @see TypeFormat */ public final TextBuilder append(boolean b) { return b ? append("true") : append("false"); } /** * Appends the decimal representation of the specified <code>int</code> * argument. * * @param i the <code>int</code> to format. * @return <code>this</code> */ public final TextBuilder append(int i) { if (i <= 0) { if (i == 0) return append("0"); if (i == Integer.MIN_VALUE) // Negation would overflow. return append("-2147483648"); append('-'); i = -i; } int digits = MathLib.digitLength(i); if (_capacity < _length + digits) increaseCapacity(); _length += digits; for (int index = _length - 1;; index--) { int j = i / 10; _high[index >> B1][index & M1] = (char) ('0' + i - (j * 10)); if (j == 0) return this; i = j; } } /** * Appends the radix representation of the specified <code>int</code> * argument. * * @param i the <code>int</code> to format. * @param radix the radix (e.g. <code>16</code> for hexadecimal). * @return <code>this</code> */ public final TextBuilder append(int i, int radix) { if (radix == 10) return append(i); // Faster. if (radix < 2 || radix > 36) throw new IllegalArgumentException("radix: " + radix); if (i < 0) { append('-'); if (i == Integer.MIN_VALUE) { // Negative would overflow. appendPositive(-(i / radix), radix); return (TextBuilder) append(DIGIT_TO_CHAR[-(i % radix)]); } i = -i; } appendPositive(i, radix); return this; } private void appendPositive(int l1, int radix) { if (l1 >= radix) { int l2 = l1 / radix; // appendPositive(l2, radix); if (l2 >= radix) { int l3 = l2 / radix; // appendPositive(l3, radix); if (l3 >= radix) { int l4 = l3 / radix; appendPositive(l4, radix); append(DIGIT_TO_CHAR[l3 - (l4 * radix)]); } else append(DIGIT_TO_CHAR[l3]); append(DIGIT_TO_CHAR[l2 - (l3 * radix)]); } else append(DIGIT_TO_CHAR[l2]); append(DIGIT_TO_CHAR[l1 - (l2 * radix)]); } else append(DIGIT_TO_CHAR[l1]); } private final static char[] DIGIT_TO_CHAR = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' }; /** * Appends the decimal representation of the specified <code>long</code> * argument. * * @param l the <code>long</code> to format. * @return <code>this</code> */ public final TextBuilder append(long l) { if (l <= 0) { if (l == 0) return append("0"); if (l == Long.MIN_VALUE) // Negation would overflow. return append("-9223372036854775808"); append('-'); l = -l; } if (l <= Integer.MAX_VALUE) return append((int) l); append(l / 1000000000); int i = (int) (l % 1000000000); int digits = MathLib.digitLength(i); append("000000000", 0, 9 - digits); return append(i); } /** * Appends the radix representation of the specified <code>long</code> * argument. * * @param l the <code>long</code> to format. * @param radix the radix (e.g. <code>16</code> for hexadecimal). * @return <code>this</code> */ public final TextBuilder append(long l, int radix) { if (radix == 10) return append(l); // Faster. if (radix < 2 || radix > 36) throw new IllegalArgumentException("radix: " + radix); if (l < 0) { append('-'); if (l == Long.MIN_VALUE) { // Negative would overflow. appendPositive(-(l / radix), radix); return (TextBuilder) append(DIGIT_TO_CHAR[(int) -(l % radix)]); } l = -l; } appendPositive(l, radix); return this; } private void appendPositive(long l1, int radix) { if (l1 >= radix) { long l2 = l1 / radix; // appendPositive(l2, radix); if (l2 >= radix) { long l3 = l2 / radix; // appendPositive(l3, radix); if (l3 >= radix) { long l4 = l3 / radix; appendPositive(l4, radix); append(DIGIT_TO_CHAR[(int) (l3 - (l4 * radix))]); } else append(DIGIT_TO_CHAR[(int) l3]); append(DIGIT_TO_CHAR[(int) (l2 - (l3 * radix))]); } else append(DIGIT_TO_CHAR[(int) l2]); append(DIGIT_TO_CHAR[(int) (l1 - (l2 * radix))]); } else append(DIGIT_TO_CHAR[(int) l1]); } /** * Appends the textual representation of the specified <code>float</code>. * * @param f the <code>float</code> to format. * @return <code>append(f, 10, (abs(f) >= 1E7) || (abs(f) < 0.001), false)</code> */ public final TextBuilder append(float f) { return append(f, 10, (MathLib.abs(f) >= 1E7) || (MathLib.abs(f) < 0.001), false); } /** * Appends the textual representation of the specified <code>double</code>; * the number of digits is 17 or 16 when the 16 digits representation * can be parsed back to the same <code>double</code> (mimic the standard * library formatting). * * @param d the <code>double</code> to format. * @return <code>append(d, -1, (MathLib.abs(d) >= 1E7) || * (MathLib.abs(d) < 0.001), false)</code> */ public final TextBuilder append(double d) { return append(d, -1, (MathLib.abs(d) >= 1E7) || (MathLib.abs(d) < 0.001), false); } /** * Appends the textual representation of the specified <code>double</code> * according to the specified formatting arguments. * * @param d the <code>double</code> value. * @param digits the number of significative digits (excludes exponent) or * <code>-1</code> to mimic the standard library (16 or 17 digits). * @param scientific <code>true</code> to forces the use of the scientific * notation (e.g. <code>1.23E3</code>); <code>false</code> * otherwise. * @param showZero <code>true</code> if trailing fractional zeros are * represented; <code>false</code> otherwise. * @return <code>TypeFormat.format(d, digits, scientific, showZero, this)</code> * @throws IllegalArgumentException if <code>(digits > 19)</code>) */ public final TextBuilder append(double d, int digits, boolean scientific, boolean showZero) { if (digits > 19) throw new IllegalArgumentException("digits: " + digits); if (d != d) // NaN return append("NaN"); if (d == Double.POSITIVE_INFINITY) return append("Infinity"); if (d == Double.NEGATIVE_INFINITY) return append("-Infinity"); if (d == 0.0) { // Zero. if (digits < 0) return append("0.0"); append('0'); if (showZero) { append('.'); for (int j = 1; j < digits; j++) { append('0'); } } return this; } if (d < 0) { // Work with positive number. d = -d; append('-'); } // Find the exponent e such as: value == x.xxx * 10^e int e = MathLib.floorLog10(d); long m; if (digits < 0) { // Use 16 or 17 digits. // Try 17 digits. long m17 = MathLib.toLongPow10(d, (17 - 1) - e); // Check if we can use 16 digits. long m16 = m17 / 10; double dd = MathLib.toDoublePow10(m16, e - 16 + 1); if (dd == d) { // 16 digits is enough. digits = 16; m = m16; } else { // We cannot remove the last digit. digits = 17; m = m17; } } else // Use the specified number of digits. m = MathLib.toLongPow10(d, (digits - 1) - e); // Formats. if (scientific || (e >= digits)) { // Scientific notation has to be used ("x.xxxEyy"). long pow10 = POW10_LONG[digits - 1]; int k = (int) (m / pow10); // Single digit. append((char) ('0' + k)); m = m - pow10 * k; appendFraction(m, digits - 1, showZero); append('E'); append(e); } else { // Dot within the string ("xxxx.xxxxx"). int exp = digits - e - 1; if (exp < POW10_LONG.length) { long pow10 = POW10_LONG[exp]; long l = m / pow10; append(l); m = m - pow10 * l; } else append('0'); // Result of the division by a power of 10 larger than any long. appendFraction(m, exp, showZero); } return this; } private void appendFraction(long l, int digits, boolean showZero) { append('.'); if (l == 0) if (showZero) for (int i = 0; i < digits; i++) { append('0'); } else append('0'); else { // l is different from zero. int length = MathLib.digitLength(l); for (int j = length; j < digits; j++) { append('0'); // Add leading zeros. } if (!showZero) while (l % 10 == 0) { l /= 10; // Remove trailing zeros. } append(l); } } private static final long[] POW10_LONG = new long[] { 1L, 10L, 100L, 1000L, 10000L, 100000L, 1000000L, 10000000L, 100000000L, 1000000000L, 10000000000L, 100000000000L, 1000000000000L, 10000000000000L, 100000000000000L, 1000000000000000L, 10000000000000000L, 100000000000000000L, 1000000000000000000L }; /** * Inserts the specified character sequence at the specified location. * * @param index the insertion position. * @param csq the character sequence being inserted. * @return <code>this</code> * @throws IndexOutOfBoundsException if <code>(index < 0) || * (index > this.length())</code> */ public final TextBuilder insert(int index, java.lang.CharSequence csq) { if ((index < 0) || (index > _length)) throw new IndexOutOfBoundsException("index: " + index); final int shift = csq.length(); int newLength = _length + shift; while (newLength >= _capacity) { increaseCapacity(); } _length = newLength; for (int i = _length - shift; --i >= index;) { this.setCharAt(i + shift, this.charAt(i)); } for (int i = csq.length(); --i >= 0;) { this.setCharAt(index + i, csq.charAt(i)); } return this; } /** * Removes all the characters of this text builder * (equivalent to <code>this.delete(start, this.length())</code>). * * @return <code>this.delete(0, this.length())</code> */ public final TextBuilder clear() { _length = 0; return this; } /** * Removes the characters between the specified indices. * * @param start the beginning index, inclusive. * @param end the ending index, exclusive. * @return <code>this</code> * @throws IndexOutOfBoundsException if <code>(start < 0) || (end < 0) * || (start > end) || (end > this.length())</code> */ public final TextBuilder delete(int start, int end) { if ((start < 0) || (end < 0) || (start > end) || (end > this.length())) throw new IndexOutOfBoundsException(); for (int i = end, j = start; i < _length;) { this.setCharAt(j++, this.charAt(i++)); } _length -= end - start; return this; } /** * Reverses this character sequence. * * @return <code>this</code> */ public final TextBuilder reverse() { final int n = _length - 1; for (int j = (n - 1) >> 1; j >= 0;) { char c = charAt(j); setCharAt(j, charAt(n - j)); setCharAt(n - j--, c); } return this; } /** * Returns the {@link Text} corresponding to this {@link TextBuilder}. * * @return the corresponding {@link Text} instance. */ public final Text toText() { return Text.valueOf(this, 0, _length); } /** * Returns the <code>String</code> representation of this * {@link TextBuilder}. * * @return the <code>java.lang.String</code> for this text builder. */ @Override public final String toString() { return (_length < C1) ? new String(_low, 0, _length) : toLargeString(); } private String toLargeString() { char[] data = new char[_length]; this.getChars(0, _length, data, 0); return new String(data, 0, _length); } /** * Returns the <code>CharArray</code> representation of this * {@link TextBuilder}. * * @return the corresponding {@link CharArray} instance. */ public final CharArray toCharArray() { CharArray cArray = new CharArray(); char[] data; if (_length < C1) { data = _low; } else { data = new char[_length]; this.getChars(0, _length, data, 0); } cArray.setArray(data, 0, _length); return cArray; } /** * Returns the hash code for this text builder. * * @return the hash code value. */ @Override public final int hashCode() { int h = 0; for (int i = 0; i < _length;) { h = 31 * h + charAt(i++); } return h; } /** * Compares this text builder against the specified object for equality. * Returns <code>true</code> if the specified object is a text builder * having the same character content. * * @param obj the object to compare with or <code>null</code>. * @return <code>true</code> if that is a text builder with the same * character content as this text; <code>false</code> otherwise. */ @Override public final boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof TextBuilder)) return false; TextBuilder that = (TextBuilder) obj; if (this._length != that._length) return false; for (int i = 0; i < _length;) { if (this.charAt(i) != that.charAt(i++)) return false; } return true; } /** * Indicates if this text builder has the same character content as the * specified character sequence. * * @param csq the character sequence to compare with. * @return <code>true</code> if the specified character sequence has the * same character content as this text; <code>false</code> otherwise. */ public final boolean contentEquals(java.lang.CharSequence csq) { if (csq.length() != _length) return false; for (int i = 0; i < _length;) { char c = _high[i >> B1][i & M1]; if (csq.charAt(i++) != c) return false; } return true; } /** * Increases this text builder capacity. */ private void increaseCapacity() { if (_capacity < C1) { // For small capacity, resize. _capacity <<= 1; char[] tmp = new char[_capacity]; System.arraycopy(_low, 0, tmp, 0, _length); _low = tmp; _high[0] = tmp; } else { // Add a new low block of 1024 elements. int j = _capacity >> B1; if (j >= _high.length) { // Resizes _high. char[][] tmp = new char[_high.length * 2][]; System.arraycopy(_high, 0, tmp, 0, _high.length); _high = tmp; } _high[j] = new char[C1]; _capacity += C1; } } }