Reputation: 2109
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
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
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
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