Runtime verified Test scripts included

Syntax

IndexOf(sourceString, substring[, occurrence])  →  number
2–3 arguments

Parameters

Name Type Required Description
sourceString string | number Yes String to search in
substring string | number Yes Substring to find
occurrence string | number No Which occurrence to locate, as a whole number

Example

%%[
  VAR @pos
  SET @pos = IndexOf("Hello World", "World")
]%%
Position: %%=v(@pos)=%%

Renders Position: 7.

The common pattern is splitting a value at a known separator — take the position, then subtract one to get the length of the part before it:

%%[
  VAR @fullName, @space, @first
  SET @fullName = "Dale Cameron"
  SET @space = IndexOf(@fullName, " ")
  IF @space > 0 THEN
    SET @first = Substring(@fullName, 1, Subtract(@space, 1))
  ENDIF
]%%

The search ignores letter case, and a third argument can reach past the first match — see below.

Return value

number — the 1-based position at which the substring starts.

0 is the only sentinel: it is returned when the substring is absent, when either argument is empty, and when a requested occurrence does not exist. Positions themselves are an open domain, so there is no closed set of values to test for beyond checking against 0.

Behaviour

Positions are 1-based. The leading character of Hello World is position 1, World starts at 7, and the trailing d is 11. A needle equal to the whole source returns 1, and a needle longer than the source returns 0.

Without a third argument only the first match is reported. IndexOf("banana", "na") gives 3 even though na also occurs at 5.

Any empty argument returns 0. An empty substring, an empty source, and both empty all return 0 rather than the position 1 some string APIs give for an empty needle.

Numbers are accepted for both text parameters and searched by their string form. Searching the numeric literal 9876543 for "65" gives 4. Booleans are not usable text: passing true as the source and searching for a fragment of the word it would spell returns 0, as does searching a string that literally contains that word for a boolean needle.

Positions count UTF-16 code units, the same unit Length counts. In a source built as caf + Char(233) + " time", which measures 9, the accented letter is at 4 and the following word at 6.

Argument counts outside two or three abort the page. Zero, one and four arguments each returned HTTP 422 with nothing rendered — including the markers printed before the call — so there is no error value to test for.

The search is case-insensitive

This is the finding most likely to bite, because no source mentions it and every published example happens to search with matching case.

Call Returns
IndexOf("Hello World", "World") 7
IndexOf("Hello World", "WORLD") 7
IndexOf("Hello World", "world") 7
IndexOf("Hello World", "h") 1

There is no flag to make the match case-sensitive. When case matters, extract the located text with Substring and compare it yourself rather than expecting IndexOf to return 0 on a case mismatch.

The undocumented third argument selects an occurrence

A third argument does not abort the way a fourth does — it succeeds and chooses which match to report.

Call Returns
IndexOf("Hello World", "o", 1) 5
IndexOf("Hello World", "o", 2) 8
IndexOf("Hello World", "o", 3) 0
IndexOf("aaaa", "aa", 2) 3
IndexOf("abcabcabc", "abc", -1) 7

Overlapping matches are not counted separately, which is why the second aa inside aaaa is at 3 rather than 2. A count past the last match returns 0. 0 selects the first match, while a negative value resolves to the last match whatever its magnitude — -1, -2 and -9 all returned the third and final match above.

A numeric string works in that position; a decimal, a boolean, and a non-numeric string each abort the page. The argument is undocumented, so it carries no compatibility guarantee.

Both findings are catalogued on Differs from official docs. The docs are silent rather than wrong in both cases, so the entry is not flagged as contradicting them.

Show test script
%%[
  VAR @b
  SET @b = RequestParameter("b")

  /* documented forms plus the case-insensitivity and the accepted types */
  IF @b == "safe" THEN
    OutputLine(Concat("FIRSTCHAR=[", IndexOf("Hello World", "H"), "]"))
    OutputLine(Concat("WORD=[", IndexOf("Hello World", "World"), "]"))
    OutputLine(Concat("LASTCHAR=[", IndexOf("Hello World", "d"), "]"))
    OutputLine(Concat("WHOLE=[", IndexOf("Hello World", "Hello World"), "]"))
    OutputLine(Concat("REPEATED=[", IndexOf("banana", "na"), "]"))
    OutputLine(Concat("MISSING=[", IndexOf("Hello World", "zzz"), "]"))
    OutputLine(Concat("LONGERNEEDLE=[", IndexOf("ab", "abcdef"), "]"))
    OutputLine(Concat("UPPERNEEDLE=[", IndexOf("Hello World", "WORLD"), "]"))
    OutputLine(Concat("LOWERNEEDLE=[", IndexOf("Hello World", "world"), "]"))
    OutputLine(Concat("LOWERH=[", IndexOf("Hello World", "h"), "]"))
    OutputLine(Concat("SPACECHAR=[", IndexOf("Dale Cameron", " "), "]"))
    OutputLine(Concat("EMPTYNEEDLE=[", IndexOf("Hello", ""), "]"))
    OutputLine(Concat("EMPTYHAY=[", IndexOf("", "a"), "]"))
    OutputLine(Concat("BOTHEMPTY=[", IndexOf("", ""), "]"))
    OutputLine(Concat("NUMHAY=[", IndexOf(9876543, "65"), "]"))
    OutputLine(Concat("NUMNEEDLE=[", IndexOf("a1b2c3", 2), "]"))
    OutputLine(Concat("BOOLHAY=[", IndexOf(true, "ru"), "]"))
    OutputLine(Concat("BOOLNEEDLE=[", IndexOf("is true here", true), "]"))
  ENDIF

  /* the undocumented third argument picks which occurrence to locate */
  IF @b == "occurrence" THEN
    OutputLine(Concat("OCC1=[", IndexOf("Hello World", "o", 1), "]"))
    OutputLine(Concat("OCC2=[", IndexOf("Hello World", "o", 2), "]"))
    OutputLine(Concat("OCC3=[", IndexOf("Hello World", "o", 3), "]"))
    OutputLine(Concat("OCCL3=[", IndexOf("Hello World", "l", 3), "]"))
    OutputLine(Concat("OCCNA2=[", IndexOf("banana", "na", 2), "]"))
    OutputLine(Concat("OCCOVERLAP=[", IndexOf("aaaa", "aa", 2), "]"))
    OutputLine(Concat("OCCZERO=[", IndexOf("Hello World", "o", 0), "]"))
    OutputLine(Concat("OCCSTR=[", IndexOf("banana", "na", "2"), "]"))
  ENDIF

  /* a negative occurrence resolves to the LAST match, whatever its size */
  IF @b == "occneg" THEN
    OutputLine(Concat("NEG1_BANANA=[", IndexOf("banana", "na", -1), "]"))
    OutputLine(Concat("NEG2_BANANA=[", IndexOf("banana", "na", -2), "]"))
    OutputLine(Concat("NEG1_ABC=[", IndexOf("abcabcabc", "abc", -1), "]"))
    OutputLine(Concat("NEG2_ABC=[", IndexOf("abcabcabc", "abc", -2), "]"))
    OutputLine(Concat("NEG9_ABC=[", IndexOf("abcabcabc", "abc", -9), "]"))
  ENDIF

  /* positions count UTF-16 code units, matching Length */
  IF @b == "unicode" THEN
    VAR @u
    SET @u = Concat("caf", Char(233), " time")
    OutputLine(Concat("ULEN=[", Length(@u), "]"))
    OutputLine(Concat("UAFTER=[", IndexOf(@u, "time"), "]"))
    OutputLine(Concat("UACCENT=[", IndexOf(@u, Char(233)), "]"))
  ENDIF

  /* every branch below aborts the page: the start marker never renders */
  IF @b == "a0" THEN
    OutputLine(Concat("--- a0 start ---"))
    OutputLine(Concat("A0=[", IndexOf(), "]"))
  ENDIF

  IF @b == "a1" THEN
    OutputLine(Concat("--- a1 start ---"))
    OutputLine(Concat("A1=[", IndexOf("Hello World"), "]"))
  ENDIF

  IF @b == "a4" THEN
    OutputLine(Concat("--- a4 start ---"))
    OutputLine(Concat("A4=[", IndexOf("Hello World", "o", 1, 2), "]"))
  ENDIF

  IF @b == "occstrbad" THEN
    OutputLine(Concat("--- occstrbad start ---"))
    OutputLine(Concat("OCCSTRBAD=[", IndexOf("Hello World", "o", "abc"), "]"))
  ENDIF

  IF @b == "occdec" THEN
    OutputLine(Concat("--- occdec start ---"))
    OutputLine(Concat("OCCDEC=[", IndexOf("banana", "na", 1.5), "]"))
  ENDIF

  IF @b == "occbool" THEN
    OutputLine(Concat("--- occbool start ---"))
    OutputLine(Concat("OCCBOOL=[", IndexOf("banana", "na", true), "]"))
  ENDIF
]%%

Availability

Platform Available
Marketing Cloud Engagement Yes
Marketing Cloud Next Yes, from API 67.0

See also