(This post elaborates on A string view over a Java String.)
This time, I have incorporated both the great answers in the previous iteration:
- https://codereview.stackexchange.com/a/293506/58360 by Alexander Ivanchenko,
- https://codereview.stackexchange.com/a/293489/58360 by Toby Speight.
Now my code looks like follows:
com.github.coderodde.util.StringView.java:
package com.github.coderodde.util;
import java.util.Objects;
import java.util.stream.IntStream;
/**
* This class implements a string view that can be enlarged, shrinked or
* shifted.
*
* @version 1.1.0 (Aug 29, 2024)
* @since 1.1.0 (Aug 29, 2024)
*/
public final class StringView implements CharSequence {
private final String string;
private final int viewOffset;
private final int viewLength;
/**
* Constructs this string view.
*
* @param string the owner string.
* @param viewOffset the offset in the owner string.
* @param viewLength the length of the view.
*/
public StringView(final String string,
final int viewOffset,
final int viewLength) {
this.string =
Objects.requireNonNull(string, "The input string is null.");
checkOnConstruction(viewOffset,
viewLength);
this.viewOffset = viewOffset;
this.viewLength = viewLength;
}
/**
* Constructs this string view. The resulting view will cover the entire
* owner string.
*
* @param string the owner string.
*/
public StringView(final String string) {
this(string, 0, string.length());
}
/**
* Accesses the {@code index}th character of this string view.
*
* @param index the index of the desired character.
*
* @return the {@code index}th character of this string view.
*/
public char charAt(final int index) {
checkIndex(index);
return string.charAt(viewOffset + index);
}
/**
* Return a {@link java.lang.String} holding the contents of this string
* view.
*
* @return the string representation of this string view.
*/
@Override
public String toString() {
return string.substring(viewOffset, viewOffset + viewLength);
}
@Override
public int hashCode() {
int hash = 7;
hash = 97 * hash + hashCodeImpl();
hash = 97 * hash + viewOffset;
hash = 97 * hash + viewLength;
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final StringView other = (StringView) obj;
if (viewOffset != other.viewOffset) {
return false;
}
if (viewLength != other.viewLength) {
return false;
}
return Objects.equals(string, other.string);
}
public String getOwnerString() {
return string;
}
public int getViewOffset() {
return viewOffset;
}
public int getViewLength() {
return viewLength;
}
/**
* Attempts to shift the view by {@code steps} steps to the left.
*
* @param steps the number of steps to shift.
*
* @return a new string view with the shifted contents.
*/
public StringView shiftLeft(final int steps) {
checkStepsNotBelowZero(steps);
final int newViewOffset = Math.max(0, viewOffset - steps);
return new StringView(string, newViewOffset, viewLength);
}
/**
* Attempts to shift the view by {@code steps} steps to the right.
*
* @param steps the number of steps to shift.
*
* @return a new string view with the shifted contents.
*/
public StringView shiftRight(final int steps) {
checkStepsNotBelowZero(steps);
final int newViewOffset = Math.min(string.length(), viewOffset + steps);
return new StringView(string, newViewOffset, viewLength);
}
/**
* Shift either to left or right by {@code steps} steps. If the argument is
* negative, shifts to the left. Otherwise, shifts to the right.
*
* @param steps the number of steps to shift.
*
* @return a new string view with the shifted contents.
*/
public StringView shift(final int steps) {
if (steps < 0) {
return shiftLeft(-steps);
} else {
return shiftRight(steps);
}
}
/**
* Shrinks this string view by {@code length} elements from the tail of the
* string view.
*
* @param length the length to shrinky by.
*
* @return the shrinked string view.
*/
public StringView shrink(final int length) {
checkLengthNotNegative(length);
checkLengthIsNotLargerThanCurrentViewLength(length);
return new StringView(string, viewOffset, viewLength - length);
}
/**
* Grows this string view by {@code length} elements towards the tail of the
* string view.
*
* @param length the length to grow by.
*
* @return the grown string view.
*/
public StringView grow(final int length) {
checkLengthNotNegative(length);
checkViewDoesNotOutgrow(length);
return new StringView(string, viewOffset, viewLength + length);
}
@Override
public int length() {
return viewLength;
}
@Override
public boolean isEmpty() {
return viewLength == 0;
}
@Override
public CharSequence subSequence(final int start,
final int end) {
final int subSequenceLength = end - start;
if (subSequenceLength > viewLength) {
throw new IllegalArgumentException(
String.format(
"Requested sequence has %d character(s). " +
"This view has %d.",
subSequenceLength,
viewLength));
}
if (start + subSequenceLength > viewOffset + viewLength) {
throw new IllegalArgumentException(
String.format(
"Overflow on the right by %d character(s).",
start + subSequenceLength
- viewOffset
- viewLength));
}
return new StringView(string,
viewOffset + start,
subSequenceLength);
}
@Override
public IntStream chars() {
return CharSequence.super.chars();
}
@Override
public IntStream codePoints() {
return CharSequence.super.codePoints();
}
private int hashCodeImpl() {
int hash = 13;
for (int i = viewOffset; i < viewOffset + viewLength; i++) {
hash = 71 * hash + Character.hashCode(string.charAt(i));
}
return hash;
}
private void checkIndex(final int index) {
if (index < 0) {
final String exceptionMessage =
String.format("index (%d) < 0", index);
throw new IndexOutOfBoundsException(exceptionMessage);
}
if (index >= viewLength) {
final String exceptionMessage =
String.format(
"index (%d) >= viewLength (%d)",
index,
viewLength);
throw new IndexOutOfBoundsException(exceptionMessage);
}
}
private void checkViewDoesNotOutgrow(final int length) {
if (viewOffset + length > string.length()) {
final String exceptionMessage =
String.format(
"New view outgrows the string view parent by %d characters.",
viewOffset + length - string.length());
throw new IllegalArgumentException(exceptionMessage);
}
}
private void checkLengthNotNegative(final int length) {
if (length < 0) {
final String exceptionMessage =
String.format("length (%d) < 0", length);
throw new IllegalArgumentException(exceptionMessage);
}
}
private void checkLengthIsNotLargerThanCurrentViewLength(final int length) {
if (length > viewLength) {
final String exceptionMessage =
String.format(
"length (%d) > viewLength (%d)",
length,
viewLength);
throw new IllegalArgumentException(exceptionMessage);
}
}
private void checkStepsNotBelowZero(final int steps) {
if (steps < 0) {
final String exceptionMessage =
String.format("steps (%d) < 0", steps);
throw new IllegalArgumentException(exceptionMessage);
}
}
private void checkOnConstruction(final int viewOffset,
final int viewLength) {
if (viewOffset < 0) {
final String exceptionMessage =
String.format(
"offset (%d) < 0",
viewOffset);
throw new IllegalArgumentException(exceptionMessage);
}
if (viewOffset > string.length()) {
final String exceptionMessage =
String.format(
"offset (%d) > string.length (%d)",
viewOffset,
string.length());
throw new IllegalArgumentException(exceptionMessage);
}
if (viewLength < 0) {
final String exceptionMessage =
String.format(
"viewLength (%d) < 0",
viewLength);
throw new IllegalArgumentException(exceptionMessage);
}
if (viewLength > string.length()) {
final String exceptionMessage =
String.format(
"viewLength (%d) > string.length (%d)",
viewLength,
string.length());
throw new IllegalArgumentException(exceptionMessage);
}
if (viewOffset + viewLength > string.length()) {
final String exceptionMessage =
String.format(
"View outside the right border by %d characters.",
viewOffset + viewLength - string.length());
throw new IllegalArgumentException(exceptionMessage);
}
}
}
com.github.coderodde.util.StringViewTest.java:
package com.github.coderodde.util;
import java.util.PrimitiveIterator.OfInt;
import java.util.stream.IntStream;
import org.junit.Test;
import static org.junit.Assert.*;
public class StringViewTest {
@Test
public void testCharAt() {
StringView view = new StringView("abcd");
assertEquals('a', view.charAt(0));
assertEquals('b', view.charAt(1));
assertEquals('c', view.charAt(2));
assertEquals('d', view.charAt(3));
view = view.shrink(1);
view = view.shift(1);
assertEquals('b', view.charAt(0));
assertEquals('c', view.charAt(1));
assertEquals('d', view.charAt(2));
}
@Test
public void testToString() {
StringView view = new StringView("abcde");
view = view.shrink(2);
view = view.shiftRight(1);
final String s = view.toString();
assertEquals("bcd", s);
}
@Test
public void testShiftLeft() {
StringView view = new StringView("0123456789",3, 3);
view = view.shiftLeft(2);
assertEquals("123", view.toString());
}
@Test
public void testShiftRight() {
StringView view = new StringView("0123456", 3, 2);
view = view.shiftRight(1);
assertEquals("45", view.toString());
}
@Test
public void testShift() {
StringView view = new StringView("12345", 1, 3);
view = view.shift(-1);
assertEquals("123", view.toString());
view = view.shift(2);
assertEquals("345", view.toString());
}
@Test
public void testShrink() {
StringView view = new StringView("abcde12345", 0, 6);
view = view.shiftRight(2);
view = view.shrink(4);
assertEquals("cd", view.toString());
}
@Test
public void testGrow() {
StringView view = new StringView("abcde12345", 2, 4);
view = view.grow(2);
assertEquals("cde123", view.toString());
}
@Test
public void intStreamChars() {
final StringView view = new StringView("abcdef", 1, 4);
final IntStream stream = view.chars();
final OfInt ofInt = stream.iterator();
assertEquals((Integer)("b".codePointAt(0)), ofInt.next());
assertEquals((Integer)("c".codePointAt(0)), ofInt.next());
assertEquals((Integer)("d".codePointAt(0)), ofInt.next());
assertEquals((Integer)("e".codePointAt(0)), ofInt.next());
}
@Test
public void intStreamCodePoints() {
final StringView view = new StringView("abcdef", 1, 4);
final IntStream stream = view.codePoints();
final OfInt ofInt = stream.iterator();
assertEquals((Integer)("b".codePointAt(0)), ofInt.next());
assertEquals((Integer)("c".codePointAt(0)), ofInt.next());
assertEquals((Integer)("d".codePointAt(0)), ofInt.next());
assertEquals((Integer)("e".codePointAt(0)), ofInt.next());
}
@Test
public void substring() {
final StringView view = new StringView("123456", 1, 4);
final CharSequence seq = view.subSequence(1, 4);
assertEquals("345", seq.toString());
}
@Test(expected = IllegalArgumentException.class)
public void substringThrowsOnToLong() {
final StringView view = new StringView("123456", 1, 4);
view.subSequence(2, 6);
}
@Test(expected = IndexOutOfBoundsException.class)
public void throwsOnNegativeIndex() {
new StringView("abc").charAt(-1);
}
@Test(expected = IndexOutOfBoundsException.class)
public void throwsOnTooLargeIndex() {
new StringView("abc").charAt(3);
}
@Test(expected = IllegalArgumentException.class)
public void throwsOnOutGrow() {
StringView view = new StringView("abcd", 1, 2);
view.grow(2);
}
@Test(expected = IllegalArgumentException.class)
public void throwsOnNegativeLength() {
new StringView("abc").shrink(-1);
}
@Test(expected = IllegalArgumentException.class)
public void throwsOnShrinkWhenRequestedLengthSmallerThanViewLength() {
new StringView("12345", 1, 3).shrink(4);
}
@Test(expected = IllegalArgumentException.class)
public void throwsOnBelowZeroStepsLeft() {
new StringView("12345", 1, 3).shiftLeft(-1);
}
@Test(expected = IllegalArgumentException.class)
public void throwsOnBelowZeroStepsRight() {
new StringView("12345", 1, 3).shiftRight(-1);
}
@Test(expected = IllegalArgumentException.class)
public void throwsOnSubSequenceReqestNotLargerThanOriginal() {
new StringView("12345").subSequence(1, 7);
}
@Test(expected = IllegalArgumentException.class)
public void throwsOnSubSequenceOutGrowsParentString() {
new StringView("12345").subSequence(3, 6);
}
@Test(expected = IllegalArgumentException.class)
public void throwsOnConstructionWhenNegativeViewOffset() {
new StringView("abc", -1, 2);
}
@Test(expected = IllegalArgumentException.class)
public void throwsOnConstructionWhenViewOffsetOutsideOfString() {
new StringView("abc", 4, 2);
}
@Test(expected = IllegalArgumentException.class)
public void throwsOnConstructionWhenViewLengthNegative() {
new StringView("abc", 4, -1);
}
@Test(expected = IllegalArgumentException.class)
public void throwsOnConstructionWhenViewLengthLargerThanString() {
new StringView("abc", 4, 4);
}
@Test(expected = IllegalArgumentException.class)
public void throwsOnConstructionWhenViewOutgrowsString() {
new StringView("abcd1234", 4, 5);
}
}
Critique request
Please, feel free to give any constructive commentary.