From 3b5b07daa5b8782ef3dbbbee19440435b847dfb4 Mon Sep 17 00:00:00 2001 From: "tomasz.kowalczewski" Date: Sat, 27 Feb 2021 12:53:36 +0100 Subject: [PATCH] Re #37: Implemented resettable text buffer to replace TextBuilder from javolution. --- .../pl/tkowalcz/tjahzi/http/TextBuilders.java | 2 +- .../tjahzi/javolution/text/TextBuilder.java | 637 ------------------ .../pl/tkowalcz/tjahzi/utils/TextBuilder.java | 84 +++ .../tjahzi/utils/TextBuilderTest.java | 212 ++++++ 4 files changed, 297 insertions(+), 638 deletions(-) delete mode 100644 core/src/main/java/pl/tkowalcz/tjahzi/javolution/text/TextBuilder.java create mode 100644 core/src/main/java/pl/tkowalcz/tjahzi/utils/TextBuilder.java create mode 100644 core/src/test/java/pl/tkowalcz/tjahzi/utils/TextBuilderTest.java diff --git a/core/src/main/java/pl/tkowalcz/tjahzi/http/TextBuilders.java b/core/src/main/java/pl/tkowalcz/tjahzi/http/TextBuilders.java index 759f708..2b7b0f5 100644 --- a/core/src/main/java/pl/tkowalcz/tjahzi/http/TextBuilders.java +++ b/core/src/main/java/pl/tkowalcz/tjahzi/http/TextBuilders.java @@ -1,7 +1,7 @@ package pl.tkowalcz.tjahzi.http; import io.netty.util.concurrent.FastThreadLocal; -import pl.tkowalcz.tjahzi.javolution.text.TextBuilder; +import pl.tkowalcz.tjahzi.utils.TextBuilder; public class TextBuilders { diff --git a/core/src/main/java/pl/tkowalcz/tjahzi/javolution/text/TextBuilder.java b/core/src/main/java/pl/tkowalcz/tjahzi/javolution/text/TextBuilder.java deleted file mode 100644 index 045ff9a..0000000 --- a/core/src/main/java/pl/tkowalcz/tjahzi/javolution/text/TextBuilder.java +++ /dev/null @@ -1,637 +0,0 @@ -/* - * 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 pl.tkowalcz.tjahzi.javolution.text; - -import java.io.Serializable; - -/** - *

An {@link Appendable} text whose capacity expands - * gently without incurring expensive resize/copy operations ever.

- * - *

This class is not intended for large documents manipulations. - * - * @author Jean-Marie Dautelle - * @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 String - * (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 - * (index < 0) || (index >= this.length()). - */ - public final char charAt(int index) { - if (index >= _length) - throw new IndexOutOfBoundsException(); - return index < C1 ? _low[index] : _high[index >> B1][index & M1]; - } - - /** - * Sets the character at the specified position. - * - * @param index the index of the character to modify. - * @param c the new character. - * @throws IndexOutOfBoundsException if (index < 0) || - * (index >= this.length()) - */ - 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 (newLength < 0) - */ - 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 (newLength < 0) - */ - 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 (start < 0) || (end < 0) || - * (start > end) || (end > this.length()) - */ - public final java.lang.CharSequence subSequence(int start, int end) { - if ((start < 0) || (end < 0) || (start > end) || (end > _length)) - throw new IndexOutOfBoundsException(); - return new String(_low, start, end - start); - } - - /** - * Appends the specified character. - * - * @param c the character to append. - * @return this - */ - public final TextBuilder append(char c) { - if (_length >= _capacity) - increaseCapacity(); - _high[_length >> B1][_length & M1] = c; - _length++; - return this; - } - - /** - * Appends the specified character sequence. If the specified character - * sequence is null this method is equivalent to - * append("null"). - * - * @param csq the character sequence to append or null. - * @return this - */ - 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 null this method - * is equivalent to append("null"). - * - * @param csq the character sequence to append or null. - * @param start the index of the first character to append. - * @param end the index after the last character to append. - * @return this - * @throws IndexOutOfBoundsException if (start < 0) || (end < 0) - * || (start > end) || (end > csq.length()) - */ - 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 null this method - * is equivalent to append("null"). - * - * @param str the string to append or null. - * @return this - */ - 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 null this method - * is equivalent to append("null"). - * - * @param str the string to append or null. - * @param start the index of the first character to append. - * @param end the index after the last character to append. - * @return this - * @throws IndexOutOfBoundsException if (start < 0) || (end < 0) - * || (start > end) || (end > str.length()) - */ - 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 = Math.min(C1 - dstBegin, end - i); - str.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 this - */ - 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 this - * @throws IndexOutOfBoundsException if (offset < 0) || - * (length < 0) || ((offset + length) > chars.length) - */ - 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 = Math.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 boolean - * argument. - * - * @param b the boolean to format. - * @return this - */ - public final TextBuilder append(boolean b) { - return b ? append("true") : append("false"); - } - - /** - * Appends the decimal representation of the specified int - * argument. - * - * @param i the int to format. - * @return this - */ - 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 = 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 int - * argument. - * - * @param i the int to format. - * @param radix the radix (e.g. 16 for hexadecimal). - * @return this - */ - 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 long - * argument. - * - * @param l the long to format. - * @return this - */ - 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 = digitLength(i); - append("000000000", 0, 9 - digits); - return append(i); - } - - /** - * Appends the radix representation of the specified long - * argument. - * - * @param l the long to format. - * @param radix the radix (e.g. 16 for hexadecimal). - * @return this - */ - 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]); - } - - 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 this - * @throws IndexOutOfBoundsException if (index < 0) || - * (index > this.length()) - */ - 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 this.delete(start, this.length())). - * - * @return this.delete(0, this.length()) - */ - 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 this - * @throws IndexOutOfBoundsException if (start < 0) || (end < 0) - * || (start > end) || (end > this.length()) - */ - 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 this - */ - 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 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 true if the specified object is a text builder - * having the same character content. - * - * @param obj the object to compare with or null. - * @return true if that is a text builder with the same - * character content as this text; false 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 true if the specified character sequence has the - * same character content as this text; false 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; - } - } - - /** - * Returns the number of digits of the decimal representation of the - * specified int value, excluding the sign character if any. - * - * @param i the int value for which the digit length is returned. - * @return String.valueOf(i).length() for zero or positive values; - * String.valueOf(i).length() - 1 for negative values. - */ - private static int digitLength(int i) { - if (i >= 0) - return (i >= 100000) ? (i >= 10000000) ? (i >= 1000000000) ? 10 - : (i >= 100000000) ? 9 : 8 : (i >= 1000000) ? 7 : 6 - : (i >= 100) ? (i >= 10000) ? 5 : (i >= 1000) ? 4 : 3 - : (i >= 10) ? 2 : 1; - if (i == Integer.MIN_VALUE) - return 10; // "2147483648".length() - return digitLength(-i); // No overflow possible. - } -} diff --git a/core/src/main/java/pl/tkowalcz/tjahzi/utils/TextBuilder.java b/core/src/main/java/pl/tkowalcz/tjahzi/utils/TextBuilder.java new file mode 100644 index 0000000..640618d --- /dev/null +++ b/core/src/main/java/pl/tkowalcz/tjahzi/utils/TextBuilder.java @@ -0,0 +1,84 @@ +package pl.tkowalcz.tjahzi.utils; + +import java.util.Arrays; +import java.util.Objects; + +public class TextBuilder implements Appendable, CharSequence { + + private static final int INITIAL_SIZE = 8; + private static final int CAPACITY_INCREMENT = 64; + + private char[] chars = new char[INITIAL_SIZE]; + private int length; + + public TextBuilder clear() { + length = 0; + return this; + } + + public TextBuilder append(CharSequence string) { + ensureCapacity(string.length()); + + for (int i = 0; i < string.length(); i++) { + chars[length++] = string.charAt(i); + } + + return this; + } + + @Override + public TextBuilder append(CharSequence sequence, int start, int end) { + Objects.checkFromToIndex(start, end, sequence.length()); + ensureCapacity(sequence.length()); + + for (int i = start; i < end; i++) { + chars[length++] = sequence.charAt(i); + } + + return this; + } + + @Override + public TextBuilder append(char c) { + ensureCapacity(1); + chars[length++] = c; + return this; + } + + @Override + public int length() { + return length; + } + + @Override + public char charAt(int index) { + Objects.checkIndex(index, length); + return chars[index]; + } + + @Override + public CharSequence subSequence(int start, int end) { + Objects.checkFromToIndex(start, end, length); + return new String(chars, start, end - start); + } + + public TextBuilder setCharAt(int index, char c) { + Objects.checkIndex(index, length); + chars[index] = c; + + return this; + } + + @Override + public String toString() { + return new String(chars, 0, length); + } + + private void ensureCapacity(int requiredCapacity) { + if (length + requiredCapacity >= chars.length) { + int missingCapacity = length + requiredCapacity - chars.length; + int expandedSize = chars.length + Math.max(CAPACITY_INCREMENT, missingCapacity); + chars = Arrays.copyOf(chars, expandedSize); + } + } +} diff --git a/core/src/test/java/pl/tkowalcz/tjahzi/utils/TextBuilderTest.java b/core/src/test/java/pl/tkowalcz/tjahzi/utils/TextBuilderTest.java new file mode 100644 index 0000000..32e7a4f --- /dev/null +++ b/core/src/test/java/pl/tkowalcz/tjahzi/utils/TextBuilderTest.java @@ -0,0 +1,212 @@ +package pl.tkowalcz.tjahzi.utils; + +import org.apache.commons.lang3.StringUtils; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class TextBuilderTest { + + @Test + void shouldTrackLength() { + // Given + TextBuilder textBuilder = new TextBuilder(); + + // When + textBuilder.append("abcd"); + textBuilder.append('f'); + textBuilder.append("1234567890", 3, 7); + textBuilder.setCharAt(3, 'n'); + + // Then + assertThat(textBuilder.length()).isEqualTo(9); + assertThat(textBuilder.toString()).isEqualTo("abcnf4567"); + } + + @Test + void shouldClearBuilder() { + // Given + TextBuilder textBuilder = new TextBuilder(); + textBuilder.append("abcd"); + + // When + textBuilder.clear(); + + // Then + assertThat(textBuilder.length()).isZero(); + assertThat(textBuilder.toString()).isEmpty(); + } + + @Test + void shouldSetCharAtIndex() { + // Given + TextBuilder textBuilder = new TextBuilder(); + textBuilder.append("1234567890"); + + // When + textBuilder.setCharAt(3, 'f'); + + // Then + assertThat(textBuilder.length()).isEqualTo(10); + assertThat(textBuilder.toString()).isEqualTo("123f567890"); + } + + @Test + void shouldNotAllowSettingCharAtWrongIndex() { + // Given + TextBuilder textBuilder = new TextBuilder(); + textBuilder.append("1234567890"); + + // When & Then + assertThrows( + IndexOutOfBoundsException.class, + () -> textBuilder.setCharAt(-10, 'f') + ); + + assertThrows( + IndexOutOfBoundsException.class, + () -> textBuilder.setCharAt(11, 'f') + ); + } + + @Test + void shouldReturnCharAt() { + // Given + TextBuilder textBuilder = new TextBuilder(); + textBuilder.append("1234567890"); + + // When + char actual = textBuilder.charAt(6); + + // Then + assertThat(actual).isEqualTo('7'); + } + + @Test + void shouldNotAllowReturningCharAtWrongIndex() { + // Given + TextBuilder textBuilder = new TextBuilder(); + textBuilder.append("1234567890"); + + // When & Then + assertThrows( + IndexOutOfBoundsException.class, + () -> textBuilder.charAt(-10) + ); + + assertThrows( + IndexOutOfBoundsException.class, + () -> textBuilder.charAt(11) + ); + } + + @Test + void shouldReturnSubstring() { + // Given + TextBuilder textBuilder = new TextBuilder(); + textBuilder.append("1234567890"); + + // When + CharSequence actual = textBuilder.subSequence(3, 8); + + // Then + assertThat(actual).isEqualTo("45678"); + } + + @Test + void shouldVerifySubstringArguments() { + // Given + TextBuilder textBuilder = new TextBuilder(); + textBuilder.append("1234567890"); + + // When & Then + assertThrows( + IndexOutOfBoundsException.class, + () -> textBuilder.subSequence(-1, 8) + ); + + assertThrows( + IndexOutOfBoundsException.class, + () -> textBuilder.subSequence(50, 8) + ); + + assertThrows( + IndexOutOfBoundsException.class, + () -> textBuilder.subSequence(3, -1) + ); + + assertThrows( + IndexOutOfBoundsException.class, + () -> textBuilder.subSequence(3, 120) + ); + + assertThrows( + IndexOutOfBoundsException.class, + () -> textBuilder.subSequence(6, 5) + ); + } + + @Test + void shouldReturnStringRepresentation() { + // Given + TextBuilder textBuilder = new TextBuilder(); + textBuilder.append("1234567890"); + + // When + CharSequence actual = textBuilder.toString(); + + // Then + assertThat(actual).isEqualTo("1234567890"); + } + + @Test + void shouldConstructJSON() { + // Given + TextBuilder textBuilder = new TextBuilder(); + + // When + textBuilder.append("{ "); + textBuilder.append("aaaa=bbb"); + textBuilder.append(','); + textBuilder.append("foo=bar"); + textBuilder.append(','); + textBuilder.setCharAt(textBuilder.length() - 1, '}'); + + // Then + CharSequence actual = textBuilder.toString(); + assertThat(actual).isEqualTo("{ aaaa=bbb,foo=bar}"); + } + + @Test + void shouldConstructEmptyJSON() { + // Given + TextBuilder textBuilder = new TextBuilder(); + + // When + textBuilder.append("{ "); + textBuilder.setCharAt(textBuilder.length() - 1, '}'); + + // Then + CharSequence actual = textBuilder.toString(); + assertThat(actual).isEqualTo("{}"); + } + + @Test + void shouldGrowBackendStorage() { + // Given + String expected = StringUtils.repeat("abcd", 121); + TextBuilder textBuilder = new TextBuilder(); + + // When + int i; + for (i = 0; i < 121; i++) { + textBuilder.append('a'); + textBuilder.append("bcd"); + } + + // Then + assertThat(textBuilder.length()).isEqualTo(expected.length()); + assertThat(textBuilder.toString()).isEqualTo(expected); + } +}