trinalbadger587
trinalbadger587

Reputation: 2109

How do you get IndexOf of a string with multiple possible chars?

I need a function which can get the first index of one of multiple possible chars. I don't want to use regex because of the bad performance. I tried getting the min of two IndexOf(s) but it doesn't work when it is contained in one string and not the other because -1 is smaller than both indexes.

public static int IndexOf (this string s, char a, char b) => 
    Math.Min(s.IndexOf(a), s.IndexOf(b));

Upvotes: 4

Views: 3349

Answers (3)

Rufus L
Rufus L

Reputation: 37020

If I understand what you're asking, it's to get the smallest index between the indexes of two characters in a string, but the problem is that if only one of them exists, the index of the other is returned because it's -1.

One way to solve this is to test for -1 in the first string, and then decide what to do with the second:

public static int IndexOf (this string s, char a, char b) => s.IndexOf(a) == -1
    // If it's not in 'a', return its index in 'b'
    ? s.IndexOf(b)                               
    : s.IndexOf(b) == -1       
        // Else if it's not in 'b', return its index in 'a'              
        ? s.IndexOf(a)                    
        // Otherwise, return the smallest index between 'a' and 'b'       
        : Math.Min(s.IndexOf(a), s.IndexOf(b));  

However, there is a problem with this extension method!!

Because there is an implicit conversion from char to int, this method will be hidden by a native overload of the IndexOf method that takes a char and an int, which returns "the zero-based index of the first occurrence of the specified character, starting at the specified position."

I believe this is because native methods are evaluated and chosen (if there's an implicit match) before any extension methods are evaluated, but I may be wrong.

To get around this problem, we can simply give the method a different name:

public static int IndexOfFirst (this string s, char a, char b) => s.IndexOf(a) == -1
    ? s.IndexOf(b)                               
    : s.IndexOf(b) == -1       
        ? s.IndexOf(a)                    
        : Math.Min(s.IndexOf(a), s.IndexOf(b)); 

Also, we can make use of a params argument to let this method handle 0 to many characters from which to find the first index:

public static int IndexOfFirst(this string s, params char[] args) =>
    (args?.Any(arg => s.IndexOf(arg) > -1)).GetValueOrDefault()
        ? args.Select(arg => s.IndexOf(arg))
              .Where(index => index > -1)
              .Min()
        : -1;

Upvotes: 1

Dmitrii Bychenko
Dmitrii Bychenko

Reputation: 186668

I suggest a bit more complex, but I hope more convenient solution:

// 1. Let's return not only index, but the char found as well
// 2. Let's accept arbitrary number of characters
// 3. Let's not interfere with existing IndexOf, IndexOfAny methods : IndexOfAnyChar
public static (int index, char value) IndexOfAnyChar(this string s, params char[] toFind) {
  //DONE: input parameters validation
  if (null == s)
    return (-1, default(char)); // or throw ArgumentNullException(nameof(s))
  else if (null == toFind || toFind.Length <= 0)
    return (-1, default(char)); // or throw ArgumentNullException(nameof(toFind))

  int bestIndex = -1;
  char bestChar = default(char);

  foreach (char c in toFind) {
    // for the long strings let's provide count for efficency
    int index = s.IndexOf(c, 0, bestIndex < 0 ? s.Length : bestIndex);

    if (index >= 0) {
      bestIndex = index;
      bestChar = c;
    }
  }

  return (bestIndex, bestChar);
}

Demo:

var result = "abcde".IndexOfAnyChar('e', 'z', 'd');

// to get index only:
// int index = "abcde".IndexOfAnyChar('e', 'z', 'd').index; 

Console.Write(result);

Outcome:

(3, d)

Upvotes: 4

trinalbadger587
trinalbadger587

Reputation: 2109

Simple answer:

using System;
public static int IndexOf (this string s, char a, char b) => unchecked((int)Math.Min((uint)s.IndexOf(a), (uint)s.IndexOf(b))); 

or for more parameters:

using System.Linq;
public static int IndexOf (this string s, params char[] arr) => unchecked((int)arr.Min(i => (uint)s.IndexOf(i)));

This works because -1 as a uint on an unchecked settings, -1 is equivalent to uint.MaxValue which means that that is considered the highest possible value meaning that min will pick a smaller index if one exists.

EDIT: If the chars you are dealing with are the same letter if different cases, you can do:

using System;
public static int IndexOf (this string s, char a) => s.IndexOf(a, StringComparison.OrdinalIgnoreCase); 

Upvotes: -1

Related Questions