Skip to content

Commit b82b440

Browse files
dougqhdevflow.devflow-routing-intake
andauthored
Add window-bounded String and char overloads to SubSequence (#11796)
Add window-bounded String overloads to SubSequence equals/equalsIgnoreCase/startsWith/endsWith/indexOf take a String and delegate to String's region/offset methods (regionMatches, startsWith, indexOf) instead of a per-char CharSequence loop. Each guards against this view's [beginIndex, endIndex) window first so the delegated read stays in range, then reuses the JDK's backing-array compare (Latin1 fast path / intrinsics). equals(Object) now routes Strings through the fast path, keeping the charAt loop only for non-String CharSequences. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Add char overloads to SubSequence: startsWith/endsWith/indexOf Single-character leading/trailing/search checks (e.g. a leading '{' or a trailing ';') read charAt(beginIndex)/charAt(endIndex-1) or delegate to String.indexOf(int, from), each bounded to the [beginIndex, endIndex) window. indexOf(char) returns a window-relative offset or -1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Add lastIndexOf overloads; split equals into equals(String)/contentEquals - lastIndexOf(String) and lastIndexOf(char), window-bounded like indexOf, returning a window-relative offset. - Restructure equality to mirror String's API: equals(String) is the region-compare fast path, contentEquals(CharSequence) is the general char-by-char comparison, and equals(Object) dispatches String -> the fast path, any other CharSequence -> contentEquals. This keeps two equal-content views equal() while giving String args the fast path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Compute SubSequence.hashCode allocation-free over the window Replace toString().hashCode() with the String hash polynomial evaluated directly over [beginIndex, endIndex). Same value (so equals/hashCode stay consistent), but hashing a view no longer materializes a substring -- preserving the zero-copy property the class exists for. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Drop redundant CharSequence equalsIgnoreCase/startsWith from SubSequence The #11736 CharSequence (charAt-loop) versions are superseded by the String-delegating overloads here; String-literal callers (SQLCommenter) bind to the String overloads. Removes the redundant pair + their tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Fix SubSequence.subSequence(start,end) end-index overshoot The CharSequence contract treats start/end as offsets in this view's coordinates, so absolute end is beginIndex+end, not beginIndex+start+end (which overshoots by start; only correct when start==0). Latent since test including the nested case the bug broke worst. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Add contentEquals(String) fast path; suppress intentional equals asymmetry Add a String-specialized contentEquals(String) using String.regionMatches, avoiding the CharSequence path's virtual charAt dispatch, and make equals(String) a thin alias for it so contentEquals is the primary content-comparison API. Suppress the SpotBugs EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS on equals(Object) -- the cross-type view equality is intentional and documented. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Merge branch 'master' into dougqh/subsequence-string-methods Co-authored-by: devflow.devflow-routing-intake <devflow.devflow-routing-intake@kubernetes.us1.ddbuild.io>
1 parent e41dc82 commit b82b440

2 files changed

Lines changed: 267 additions & 56 deletions

File tree

internal-api/src/main/java/datadog/trace/util/SubSequence.java

Lines changed: 142 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package datadog.trace.util;
22

3+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
4+
35
/**
46
* A <code>CharSequence</code> that is a view into a sub-sequence of a <code>String</code>. Unlike
57
* <code>String.subSequence</code>, this class doesn't allocate an additional <code>String</code>,
@@ -66,8 +68,10 @@ public int length() {
6668

6769
@Override
6870
public SubSequence subSequence(int start, int end) {
71+
// start/end are offsets in THIS view's coordinates (CharSequence contract), so the absolute
72+
// end is beginIndex + end -- NOT beginIndex + start + end (which overshoots by `start`).
6973
int newBeginIndex = this.beginIndex + start;
70-
int newEndIndex = this.beginIndex + start + end;
74+
int newEndIndex = this.beginIndex + end;
7175

7276
return new SubSequence(this.str, newBeginIndex, newEndIndex);
7377
}
@@ -81,71 +85,169 @@ public void appendTo(StringBuilder builder) {
8185
if (beginIndex != endIndex) builder.append(this.str, beginIndex, endIndex);
8286
}
8387

84-
/** Returns the hash code as <code>backingStr.substr(beginIndex, endIndex).hashCode()</code> */
88+
/**
89+
* The same value as {@code toString().hashCode()} -- the {@link String} hash polynomial over this
90+
* window -- but computed directly over the backing characters so hashing a view does not
91+
* materialize a substring. Stays consistent with {@link #equals}: a view, its content-equal
92+
* {@code String}, and an equal-content view all share this hash.
93+
*/
8594
@Override
8695
public int hashCode() {
87-
return this.toString().hashCode();
96+
int h = 0;
97+
for (int i = this.beginIndex; i < this.endIndex; ++i) {
98+
h = 31 * h + this.str.charAt(i);
99+
}
100+
return h;
88101
}
89102

90103
/**
91-
* Also handles String comparisons this.equals(backingStr.substr(beginIndex, endIndex)) is true
104+
* Dispatches on the argument's runtime type: a {@code String} takes the {@link #equals(String)}
105+
* region-compare fast path; any other {@code CharSequence} is compared via {@link
106+
* #contentEquals(CharSequence)}. So {@code this.equals(backingStr.substring(beginIndex,
107+
* endIndex))} is true, and two views with equal content are equal.
92108
*/
109+
// Intentionally asymmetric: a SubSequence equals any String/CharSequence with the same content,
110+
// so subSeq.equals(str) can be true while str.equals(subSeq) is false. This view-equality
111+
// convenience is by design (see class javadoc); accepted despite the equals() symmetry contract.
93112
@Override
113+
@SuppressFBWarnings("EQ_CHECK_FOR_OPERAND_NOT_COMPATIBLE_WITH_THIS")
94114
public boolean equals(Object obj) {
95-
if (!(obj instanceof CharSequence)) return false;
115+
if (obj instanceof String) return this.equals((String) obj);
116+
if (obj instanceof CharSequence) return this.contentEquals((CharSequence) obj);
96117

97-
return this.equals((CharSequence) obj);
118+
return false;
98119
}
99120

100-
public final boolean equals(CharSequence that) {
101-
int thisLen = this.length();
102-
int thatLen = that.length();
121+
/**
122+
* Equivalent to {@code toString().equals(other)}. A thin alias for {@link
123+
* #contentEquals(String)}, kept for callers working in terms of {@code equals}; prefer {@link
124+
* #contentEquals(String)} directly.
125+
*/
126+
public final boolean equals(String other) {
127+
return this.contentEquals(other);
128+
}
103129

104-
if (thisLen != thatLen) return false;
130+
/**
131+
* Equivalent to {@code toString().contentEquals(that)}: true when {@code that} has the same
132+
* length and characters as this window. The general char-by-char comparison for any {@code
133+
* CharSequence}; prefer {@link #contentEquals(String)} when the argument is known to be a {@code
134+
* String}.
135+
*/
136+
public final boolean contentEquals(CharSequence that) {
137+
if (that == null) return false;
138+
139+
int len = this.length();
140+
if (len != that.length()) return false;
105141

106-
for (int i = 0; i < Math.min(this.length(), that.length()); ++i) {
142+
for (int i = 0; i < len; ++i) {
107143
if (this.charAt(i) != that.charAt(i)) return false;
108144
}
109145
return true;
110146
}
111147

112148
/**
113-
* True if this sub-sequence contains {@code needle} -- the zero-copy equivalent of {@code
114-
* toString().contains(needle)}, with no substring materialized.
149+
* String-specialized {@link #contentEquals(CharSequence)}: true when {@code other} has the same
150+
* length and characters as this window, via {@link String#regionMatches} (backing-array compare,
151+
* no per-char loop). Prefer this to {@link #equals(Object)} -- it states content-comparison
152+
* intent and is free of the {@code equals} contract's cross-type asymmetry.
115153
*/
116-
public final boolean contains(String needle) {
117-
return Strings.regionContains(this.str, this.beginIndex, this.endIndex, needle);
154+
public final boolean contentEquals(String other) {
155+
return other != null
156+
&& other.length() == this.length()
157+
&& this.str.regionMatches(this.beginIndex, other, 0, other.length());
118158
}
119159

120-
/** Case-insensitive content comparison; mirrors {@link String#equalsIgnoreCase(String)}. */
121-
public final boolean equalsIgnoreCase(CharSequence that) {
122-
int len = this.length();
123-
if (that == null || len != that.length()) return false;
160+
/**
161+
* Case-insensitive counterpart of {@link #equals(String)}. Like {@link
162+
* String#equalsIgnoreCase(String)}, a {@code null} argument is {@code false} rather than an
163+
* error.
164+
*/
165+
public final boolean equalsIgnoreCase(String other) {
166+
return other != null
167+
&& other.length() == this.length()
168+
&& this.str.regionMatches(true, this.beginIndex, other, 0, other.length());
169+
}
124170

125-
for (int i = 0; i < len; ++i) {
126-
char a = this.charAt(i);
127-
char b = that.charAt(i);
128-
if (a != b) {
129-
// Same two-way fold String.regionMatches(ignoreCase) uses (handles locale edge cases).
130-
char au = Character.toUpperCase(a);
131-
char bu = Character.toUpperCase(b);
132-
if (au != bu && Character.toLowerCase(au) != Character.toLowerCase(bu)) {
133-
return false;
134-
}
135-
}
136-
}
137-
return true;
171+
/**
172+
* Equivalent to {@code toString().startsWith(prefix)}. The window guard ({@code prefix.length()
173+
* <= length()}) keeps the delegated read inside {@code [beginIndex, endIndex)}.
174+
*/
175+
public final boolean startsWith(String prefix) {
176+
return prefix.length() <= this.length() && this.str.startsWith(prefix, this.beginIndex);
138177
}
139178

140-
/** True if this sub-sequence begins with {@code prefix} (content comparison, no allocation). */
141-
public final boolean startsWith(CharSequence prefix) {
142-
int prefixLen = prefix.length();
143-
if (prefixLen > this.length()) return false;
179+
/**
180+
* Equivalent to {@code length() > 0 && charAt(0) == c}, the single-character {@link
181+
* #startsWith(String)}.
182+
*/
183+
public final boolean startsWith(char c) {
184+
return this.beginIndex < this.endIndex && this.str.charAt(this.beginIndex) == c;
185+
}
144186

145-
for (int i = 0; i < prefixLen; ++i) {
146-
if (this.charAt(i) != prefix.charAt(i)) return false;
147-
}
148-
return true;
187+
/**
188+
* Equivalent to {@code toString().endsWith(suffix)}. Implemented as a prefix match anchored at
189+
* {@code endIndex - suffix.length()} so the read stays inside this window.
190+
*/
191+
public final boolean endsWith(String suffix) {
192+
int suffixLen = suffix.length();
193+
return suffixLen <= this.length() && this.str.startsWith(suffix, this.endIndex - suffixLen);
194+
}
195+
196+
/**
197+
* Equivalent to {@code length() > 0 && charAt(length() - 1) == c}, the single-character {@link
198+
* #endsWith(String)}.
199+
*/
200+
public final boolean endsWith(char c) {
201+
return this.beginIndex < this.endIndex && this.str.charAt(this.endIndex - 1) == c;
202+
}
203+
204+
/**
205+
* Equivalent to {@code toString().indexOf(needle)}: the offset of the first full occurrence of
206+
* {@code needle} within this window relative to the window start, or {@code -1} if it does not
207+
* occur fully in range. {@link String#indexOf(String, int)} returns the earliest occurrence at or
208+
* after {@code beginIndex}, so a single bound check against {@code endIndex} is exact.
209+
*/
210+
public final int indexOf(String needle) {
211+
int idx = this.str.indexOf(needle, this.beginIndex);
212+
return (idx >= 0 && idx + needle.length() <= this.endIndex) ? idx - this.beginIndex : -1;
213+
}
214+
215+
/**
216+
* Equivalent to {@code toString().indexOf(c)}: the offset of the first {@code c} within this
217+
* window relative to the window start, or {@code -1} if it does not occur in range.
218+
*/
219+
public final int indexOf(char c) {
220+
int idx = this.str.indexOf(c, this.beginIndex);
221+
return (idx >= 0 && idx < this.endIndex) ? idx - this.beginIndex : -1;
222+
}
223+
224+
/**
225+
* Equivalent to {@code toString().lastIndexOf(needle)}: the offset of the last full occurrence of
226+
* {@code needle} within this window relative to the window start, or {@code -1} if it does not
227+
* occur fully in range. Searches back from {@code endIndex - needle.length()} -- the latest start
228+
* whose end still fits the window -- so the lower bound is a single check against {@code
229+
* beginIndex}.
230+
*/
231+
public final int lastIndexOf(String needle) {
232+
int idx = this.str.lastIndexOf(needle, this.endIndex - needle.length());
233+
return (idx >= this.beginIndex) ? idx - this.beginIndex : -1;
234+
}
235+
236+
/**
237+
* Equivalent to {@code toString().lastIndexOf(c)}: the offset of the last {@code c} within this
238+
* window relative to the window start, or {@code -1} if it does not occur in range.
239+
*/
240+
public final int lastIndexOf(char c) {
241+
int idx = this.str.lastIndexOf(c, this.endIndex - 1);
242+
return (idx >= this.beginIndex) ? idx - this.beginIndex : -1;
243+
}
244+
245+
/**
246+
* True if this sub-sequence contains {@code needle} -- the zero-copy equivalent of {@code
247+
* toString().contains(needle)}, with no substring materialized.
248+
*/
249+
public final boolean contains(String needle) {
250+
return Strings.regionContains(this.str, this.beginIndex, this.endIndex, needle);
149251
}
150252

151253
@Override

internal-api/src/test/java/datadog/trace/util/SubSequenceTest.java

Lines changed: 125 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -125,27 +125,136 @@ public void contains() {
125125
}
126126

127127
@Test
128-
public void equalsIgnoreCase() {
129-
SubSequence call = SubSequence.of("xx CALL yy", 3, 7); // "CALL"
128+
public void subSequenceOfView() {
129+
// Instance subSequence(start, end): start/end are in THIS view's coordinates (CharSequence
130+
// contract), regardless of where the view sits in the backing string.
131+
SubSequence view = SubSequence.of("abcdefghij", 2, 8); // "cdefgh"
132+
SubSequence mid = view.subSequence(1, 4); // chars [1, 4) of "cdefgh" -> "def"
133+
assertEquals("def", mid.toString());
134+
assertEquals(3, mid.beginIndex()); // absolute begin = 2 + 1
135+
assertEquals(6, mid.endIndex()); // absolute end = 2 + 4 (NOT 2 + 1 + 4)
136+
137+
// full window and empty are exact
138+
assertEquals("cdefgh", view.subSequence(0, view.length()).toString());
139+
assertEquals("", view.subSequence(2, 2).toString());
140+
141+
// nested: subSequence of a non-zero-start view stays correct (the case the old bug broke worst)
142+
assertEquals("ef", mid.subSequence(1, 3).toString()); // chars [1, 3) of "def" -> "ef"
143+
}
144+
145+
@Test
146+
public void equalsString() {
147+
// "call" sits at [6, 10) inside the backing string, flanked by other text.
148+
SubSequence call = SubSequence.of("xxxxx call yyyyy", 6, 10);
149+
assertTrue(call.equals("call"));
150+
assertFalse(call.equals("CALL")); // case-sensitive
151+
assertFalse(call.equals("cal")); // shorter
152+
assertFalse(call.equals("calls")); // longer (would overshoot endIndex)
153+
assertFalse(call.equals((Object) null));
154+
155+
// equals(Object) routes a String through the region-compare fast path...
156+
assertTrue(call.equals((Object) "call"));
157+
// ...and any other CharSequence (incl. another SubSequence) through contentEquals.
158+
assertTrue(call.equals((Object) new StringBuilder("call")));
159+
assertTrue(call.equals((Object) SubSequence.of("xxxxx call yyyyy", 6, 10)));
160+
assertFalse(call.equals((Object) Integer.valueOf(4)));
161+
}
162+
163+
@Test
164+
public void contentEqualsCharSequence() {
165+
SubSequence call = SubSequence.of("xxxxx call yyyyy", 6, 10); // "call"
166+
assertTrue(call.contentEquals("call")); // String is a CharSequence
167+
assertTrue(call.contentEquals(new StringBuilder("call")));
168+
assertTrue(call.contentEquals(SubSequence.of("a call b", 2, 6)));
169+
assertFalse(call.contentEquals("CALL")); // case-sensitive
170+
assertFalse(call.contentEquals("cal")); // length mismatch
171+
assertFalse(call.contentEquals(null));
172+
}
173+
174+
@Test
175+
public void equalsIgnoreCaseString() {
176+
SubSequence call = SubSequence.of("xxxxx CaLl yyyyy", 6, 10);
130177
assertTrue(call.equalsIgnoreCase("call"));
131178
assertTrue(call.equalsIgnoreCase("CALL"));
132-
assertTrue(call.equalsIgnoreCase("CaLl"));
133-
assertFalse(call.equalsIgnoreCase("calls")); // length differs
134-
assertFalse(call.equalsIgnoreCase("cant")); // same length, content differs
179+
assertFalse(call.equalsIgnoreCase("cal"));
180+
assertFalse(call.equalsIgnoreCase("calls"));
181+
assertFalse(call.equalsIgnoreCase(null)); // matches String.equalsIgnoreCase(null)
182+
}
183+
184+
@Test
185+
public void startsWithString() {
186+
SubSequence view = SubSequence.of("xx{call}xx", 2, 8); // "{call}"
187+
assertTrue(view.startsWith("{"));
188+
assertTrue(view.startsWith("{call"));
189+
assertTrue(view.startsWith("{call}"));
190+
assertFalse(view.startsWith("call"));
191+
assertFalse(view.startsWith("{call}x")); // overshoots endIndex even though backing has 'x'
192+
assertTrue(view.startsWith("")); // empty prefix
193+
}
135194

136-
// case-sensitive equals stays case-sensitive
137-
assertFalse(call.equals("call"));
138-
assertTrue(call.equals("CALL"));
195+
@Test
196+
public void endsWithString() {
197+
SubSequence view = SubSequence.of("xx{call}xx", 2, 8); // "{call}"
198+
assertTrue(view.endsWith("}"));
199+
assertTrue(view.endsWith("call}"));
200+
assertTrue(view.endsWith("{call}"));
201+
assertFalse(view.endsWith("call"));
202+
assertFalse(view.endsWith("x{call}")); // undershoots beginIndex even though backing has 'x'
203+
assertTrue(view.endsWith("")); // empty suffix
204+
}
205+
206+
@Test
207+
public void indexOfString() {
208+
SubSequence view = SubSequence.of("aa-bc-bc-aa", 3, 8); // "bc-bc"
209+
assertEquals(0, view.indexOf("bc")); // window-relative offset of the first occurrence
210+
assertEquals(2, view.indexOf("-bc")); // non-zero relative offset
211+
assertEquals(2, view.indexOf("-"));
212+
assertEquals(-1, view.indexOf("aa")); // present in backing string but outside the window
213+
assertEquals(-1, view.indexOf("bc-bc-")); // overshoots endIndex
214+
}
215+
216+
@Test
217+
public void lastIndexOfString() {
218+
SubSequence view = SubSequence.of("aa-bc-bc-aa", 3, 8); // "bc-bc"
219+
assertEquals(3, view.lastIndexOf("bc")); // last "bc" -> relative 3
220+
assertEquals(2, view.lastIndexOf("-"));
221+
assertEquals(-1, view.lastIndexOf("aa")); // outside the window on both ends
222+
assertEquals(-1, view.lastIndexOf("bc-bc-")); // overshoots endIndex
223+
}
224+
225+
@Test
226+
public void startsWithChar() {
227+
SubSequence view = SubSequence.of("xx{call}xx", 2, 8); // "{call}"
228+
assertTrue(view.startsWith('{'));
229+
assertFalse(view.startsWith('c')); // 'c' is at offset 1, not the start
230+
assertFalse(view.startsWith('x')); // backing char before beginIndex, outside the window
231+
assertFalse(SubSequence.EMPTY.startsWith('x')); // empty window
232+
}
233+
234+
@Test
235+
public void endsWithChar() {
236+
SubSequence view = SubSequence.of("xx{call}xx", 2, 8); // "{call}"
237+
assertTrue(view.endsWith('}'));
238+
assertFalse(view.endsWith('l')); // 'l' is one before the end
239+
assertFalse(view.endsWith('x')); // backing char at endIndex, outside the window
240+
assertFalse(SubSequence.EMPTY.endsWith('x')); // empty window
241+
}
242+
243+
@Test
244+
public void indexOfChar() {
245+
SubSequence view = SubSequence.of("aa-bc-bc-aa", 3, 8); // "bc-bc"
246+
assertEquals(0, view.indexOf('b')); // window-relative offset of the first occurrence
247+
assertEquals(1, view.indexOf('c'));
248+
assertEquals(2, view.indexOf('-'));
249+
assertEquals(-1, view.indexOf('a')); // present in backing string but outside the window
139250
}
140251

141252
@Test
142-
public void startsWith() {
143-
SubSequence braceCall = SubSequence.of("xx{call} yy", 2, 7); // "{call"
144-
assertTrue(braceCall.startsWith(""));
145-
assertTrue(braceCall.startsWith("{"));
146-
assertTrue(braceCall.startsWith("{ca"));
147-
assertTrue(braceCall.startsWith("{call"));
148-
assertFalse(braceCall.startsWith("call")); // not the prefix
149-
assertFalse(braceCall.startsWith("{calls and more")); // prefix longer than sequence
253+
public void lastIndexOfChar() {
254+
SubSequence view = SubSequence.of("aa-bc-bc-aa", 3, 8); // "bc-bc"
255+
assertEquals(3, view.lastIndexOf('b')); // last 'b' -> relative 3
256+
assertEquals(4, view.lastIndexOf('c'));
257+
assertEquals(2, view.lastIndexOf('-'));
258+
assertEquals(-1, view.lastIndexOf('a')); // outside the window on both ends
150259
}
151260
}

0 commit comments

Comments
 (0)