CharsBuilder.java

package space.sunqian.common.base.chars;

import space.sunqian.annotations.Nonnull;
import space.sunqian.common.Check;
import space.sunqian.common.io.BufferKit;
import space.sunqian.common.io.IOKit;
import space.sunqian.common.io.IORuntimeException;

import java.io.CharArrayWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.CharBuffer;
import java.util.Arrays;

/**
 * {@code CharsBuilder} is used to build char arrays and their derived objects by appending char data. It is similar to
 * {@link CharArrayWriter}, provides compatible methods, but is not thread-safe. This class is also the subtype of the
 * {@link Writer} and {@link CharSequence}, but the {@code close()} method has no effect.
 *
 * @author sunqian
 */
public class CharsBuilder extends Writer implements CharSequence {

    // Max array size.
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    private final int maxSize;

    private char @Nonnull [] buf;
    private int count;

    /**
     * Constructs with 32-chars initial capacity.
     */
    public CharsBuilder() {
        this(32);
    }

    /**
     * Constructs with the specified initial capacity in chars.
     *
     * @param initialCapacity the specified initial capacity in chars
     * @throws IllegalArgumentException if size is negative
     */
    public CharsBuilder(int initialCapacity) throws IllegalArgumentException {
        this(initialCapacity, MAX_ARRAY_SIZE);
    }

    /**
     * Constructs with the specified initial capacity and the max capacity in bytes.
     *
     * @param initialCapacity the specified initial capacity in bytes
     * @param maxCapacity     the max capacity in bytes
     * @throws IllegalArgumentException if the {@code initialCapacity < 0} or {@code maxCapacity < 0} or
     *                                  {@code initialCapacity > maxCapacity}
     */
    public CharsBuilder(int initialCapacity, int maxCapacity) throws IllegalArgumentException {
        if (initialCapacity < 0) {
            throw new IllegalArgumentException("Negative initial capacity: " + initialCapacity + ".");
        }
        if (maxCapacity < 0) {
            throw new IllegalArgumentException("Negative max capacity: " + maxCapacity + ".");
        }
        if (initialCapacity > maxCapacity) {
            throw new IllegalArgumentException("Initial capacity must <= max capacity!");
        }
        buf = new char[initialCapacity];
        this.maxSize = maxCapacity;
    }

    /**
     * Appends the specified char to this builder.
     *
     * @param b the specified char
     */
    public void write(int b) {
        ensureCapacity(count + 1);
        buf[count] = (char) b;
        count += 1;
    }

    /**
     * Appends all chars from the given array.
     *
     * @param cbuf the given array
     */
    public void write(char @Nonnull [] cbuf) {
        ensureCapacity(count + cbuf.length);
        System.arraycopy(cbuf, 0, buf, count, cbuf.length);
        count += cbuf.length;
    }

    /**
     * Appends the specified number of chars from the given array, starting at the specified offset.
     *
     * @param cbuf the given array
     * @param off  the specified offset
     * @param len  the specified number
     * @throws IndexOutOfBoundsException if the offset or number is out of bounds
     */
    public void write(char @Nonnull [] cbuf, int off, int len) throws IndexOutOfBoundsException {
        Check.checkOffLen(off, len, cbuf.length);
        ensureCapacity(count + len);
        System.arraycopy(cbuf, off, buf, count, len);
        count += len;
    }

    /**
     * Writes the appended data of this builder to the specified writer.
     *
     * @param out the specified writer
     * @throws IORuntimeException if an I/O error occurs
     */
    public void writeTo(@Nonnull Writer out) throws IORuntimeException {
        try {
            out.write(buf, 0, count);
        } catch (Exception e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * Writes the appended buffered data of this builder to the specified buffer.
     *
     * @param out the specified buffer
     * @throws IORuntimeException if an I/O error occurs
     */
    public void writeTo(@Nonnull CharBuffer out) throws IORuntimeException {
        try {
            out.put(buf, 0, count);
        } catch (Exception e) {
            throw new IORuntimeException(e);
        }
    }

    /**
     * Resets this builder, the appended data will be discarded.
     * <p>
     * This method doesn't guarantee releasing the allocated space for the appended data. To trim and release the unused
     * space, use {@link #trim()}.
     */
    public void reset() {
        count = 0;
    }

    /**
     * Trims and releases the allocated but unused space.
     */
    public void trim() {
        if (count < buf.length) {
            buf = Arrays.copyOf(buf, count);
        }
    }

    /**
     * Returns the size of appended data.
     *
     * @return the size of appended data
     */
    public int size() {
        return count;
    }

    /**
     * Returns a new array containing a copy of the appended data.
     *
     * @return a new array containing a copy of the appended data
     */
    public char @Nonnull [] toCharArray() {
        return Arrays.copyOf(buf, count);
    }

    /**
     * Returns a new buffer containing a copy of the appended data.
     *
     * @return a new buffer containing a copy of the appended data
     */
    public @Nonnull CharBuffer toCharBuffer() {
        return CharBuffer.wrap(toCharArray());
    }

    /**
     * Returns a string from a copy of the appended data.
     *
     * @return a string from a copy of the appended data
     */
    public @Nonnull String toString() {
        return new String(buf, 0, count);
    }

    /**
     * No effect for this builder.
     */
    @Override
    public void flush() {
    }

    /**
     * No effect for this builder.
     */
    @Override
    public void close() {
    }

    /**
     * Appends the specified char to this builder.
     *
     * @param c the specified char
     * @return this builder
     */
    public @Nonnull CharsBuilder append(int c) {
        write(c);
        return this;
    }

    /**
     * Appends the specified char to this builder.
     *
     * @param c the specified char
     * @return this builder
     */
    public @Nonnull CharsBuilder append(char c) {
        write(c);
        return this;
    }

    /**
     * Appends all chars from the given array.
     *
     * @param chars the given array
     * @return this builder
     */
    public @Nonnull CharsBuilder append(char @Nonnull [] chars) {
        write(chars);
        return this;
    }

    /**
     * Appends the specified number of chars from the given array, starting at the specified offset.
     *
     * @param chars  the given array
     * @param offset the specified offset
     * @param length the specified number
     * @return this builder
     * @throws IndexOutOfBoundsException if the offset or number is out of bounds
     */
    public @Nonnull CharsBuilder append(
        char @Nonnull [] chars, int offset, int length
    ) throws IndexOutOfBoundsException {
        write(chars, offset, length);
        return this;
    }

    /**
     * Reads and appends all chars from the given buffer.
     *
     * @param chars the given buffer
     * @return this builder
     */
    public @Nonnull CharsBuilder append(@Nonnull CharBuffer chars) {
        int remaining = chars.remaining();
        if (remaining == 0) {
            return this;
        }
        if (chars.hasArray()) {
            write(chars.array(), BufferKit.arrayStartIndex(chars), chars.remaining());
            chars.position(chars.position() + chars.remaining());
        } else {
            char[] data = new char[remaining];
            chars.get(data);
            write(data);
        }
        return this;
    }

    /**
     * Reads and appends all chars from the given reader.
     *
     * @param reader the given reader
     * @return this builder
     * @throws IORuntimeException if an I/O error occurs
     */
    public @Nonnull CharsBuilder append(@Nonnull Reader reader) throws IORuntimeException {
        return append(reader, IOKit.bufferSize());
    }

    /**
     * Reads and appends all chars from the given reader with the specified buffer size for each reading.
     *
     * @param reader  the given reader
     * @param bufSize the specified buffer size for each reading
     * @return this builder
     * @throws IllegalArgumentException if the buffer size {@code <= 0}
     * @throws IORuntimeException       if an I/O error occurs
     */
    public @Nonnull CharsBuilder append(
        @Nonnull Reader reader, int bufSize
    ) throws IllegalArgumentException, IORuntimeException {
        if (bufSize <= 0) {
            throw new IllegalArgumentException("The buffer size must > 0.");
        }
        char[] buffer = new char[bufSize];
        while (true) {
            try {
                int readSize = reader.read(buffer);
                if (readSize < 0) {
                    return this;
                }
                write(buffer, 0, readSize);
            } catch (Exception e) {
                throw new IORuntimeException(e);
            }
        }
    }

    /**
     * Appends all chars from the given builder.
     *
     * @param builder the given reader
     * @return this builder
     */
    public @Nonnull CharsBuilder append(@Nonnull CharsBuilder builder) {
        write(builder.buf, 0, builder.count);
        return this;
    }

    @Override
    public int length() {
        return count;
    }

    @Override
    public char charAt(int index) throws IndexOutOfBoundsException {
        if (index >= count) {
            throw new IndexOutOfBoundsException("Index out of bounds: " + index + ".");
        }
        return buf[index];
    }

    @Override
    public @Nonnull CharSequence subSequence(int start, int end) {
        return toString().subSequence(start, end);
    }

    private void ensureCapacity(int minCapacity) {
        if (buf.length < minCapacity) {
            grow(minCapacity);
        }
    }

    private void grow(int minCapacity) {
        if (minCapacity < 0 || minCapacity > maxSize) {
            throw new IllegalStateException("Buffer out of size: " + minCapacity + ".");
        }
        int oldCapacity = buf.length;
        int newCapacity;
        if (oldCapacity == 0) {
            newCapacity = minCapacity;
        } else {
            newCapacity = oldCapacity * 2;
        }
        newCapacity = newCapacity(newCapacity, minCapacity);
        buf = Arrays.copyOf(buf, newCapacity);
    }

    private int newCapacity(int newCapacity, int minCapacity) {
        if (newCapacity <= 0 || newCapacity > maxSize) {
            return maxSize;
        }
        return Math.max(newCapacity, minCapacity);
    }
}