In this article, we are going to learn some different ways to identify if a string is a number in C# and compare them with a benchmark to determine the fastest and the slowest approach.

To download the source code for this article, you can visit our GitHub repository.

With that, let’s start.

Use TryParse() Method To Identify If a String is a Number

Before we start coding, let’s create a console application using the Visual Studio wizard or the dotnet new console command.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

The simplest way to identify if a string is a number in C# is by using the int.TryParse() method against the string:

int.TryParse(stringValue, out _);

This method receives a string value as a parameter and tries to parse it into an int. In case the parse was successful, it returns true, otherwise false.

Note that, the TryParse() method has a second argument. That is an out argument that means that if the parse is successful, the TryParse() method will assign the parsed value to this argument. In this case, we use the discard key (_) to indicate that we don’t need this value.

Let’s run this method against an array of string values:

var values = new string[] { "1234","ABC-789", "1.23", "9999999999" };

The first value represents an integer number which our method must parse as a number. The second value is an alphanumeric string, so we expect the int.TryParse() method to return false.

Now, let’s analyze the result:

"1234"       // true
"ABC-789"    // false
"1.23"       // false
"9999999999" // false

As we can see, the values 1.23 and 9999999999 are numbers, but they evaluate as false. That happens because we are using the int.TryParse() method which checks only for the int data type.

Note that 9999999999 is, mathematically, an integer number. However, it isn’t an int because it exceeds the maximum value of the int (Int32) data type, which is 2,147,483,647. Thus,  the int.TryParse() method evaluates it as false.  

It’s also worth mentioning that besides int.TryParse(), we can use this method for every value type, which includes: int, long, double, float, bool and others.

That said, let’s switch the int.TryParse() to double.TryParse() to validate if a string is a valid number:

double.TryParse(stringValue, out _);

Now, let’s run the method against the same array of strings:

"1234"       // true
"ABC-789"    // false
"1.23"       // true
"9999999999" // true

Now, the values 1.23 and 9999999999 both evaluate as true. That’s because the double datatype handles both mathematical integers and non-integer numeric values.

Use Regex to Identify If a String is a Number

Regular expressions can be extremely helpful when checking patterns within a string.

We will use this tool to check whether a given string represents a number or not:

public static bool UsingRegex(string stringValue)
{
    var pattern = @"^-?[0-9]+(?:\.[0-9]+)?$"; 
    var regex = new Regex(pattern);

    return regex.IsMatch(stringValue);
}

Here, we have a method that receives a string (stringValue) as input and returns true if this input represents a number, and false otherwise.

To perform this validation, we create a simple regular expression pattern (^-?[0-9]+(?:\.[0-9]+)?$). First, this pattern checks if the string starts with an optional minus sign (-?) indicating a negative value.

The second step consists of validating if the string has one or more digits ([0-9]+).

Finally, the regular expression checks if the string has an optional decimal point followed by one or more digits (?:\.[0-9]+)?$).

Now that our pattern is ready, we instantiate a new regex object that takes this pattern as a parameter. Next, we use the IsMatch() method to validate the string by passing it as an argument and returning the result.

In this case, we separate decimals point with a dot (.), however, if we want to use commas (,), we could simply modify the regex pattern expression to adapt to this change.

Also, if we want to validate only integers numbers, we can modify the regular expression to ^-?[0-9]+$. 

Compiled Regex

Compiled Regex is very similar to the previously seen Regex, but it gets compiled into IL language and can be executed faster. 

We can learn more about the differences here.

Let’s declare the same regular expression, but in the compiled form:

[GeneratedRegex(@"^-?[0-9]+(?:\.[0-9]+)?$")]
private static partial Regex IsDigitRegex();

public static bool UsingCompiledRegex(string stringValue)
{
    return IsDigitRegex().IsMatch(stringValue); 
}

We create a static partial method that returns a regex instance and annotate it with GeneratedRegexAttribute. The compiler will handle the rest and generate the method body for us during compilation. Then we can use the regex instance returned by the method in the same way as in the previous example.

It’s worth noting that this regular expression syntax is only available since .Net 7. If we were to target an older version, we should use the RegexOptions enum’s Compiled member to achieve a similar result:

new Regex(@"^-?\d+(?:\.\d+)?$", RegexOptions.Compiled)

It will compile the regular expression, just like when we use the GeneratedRegexAttribute. However, the compilation for each happens at different times. The regular expression is compiled at build time when we use the GeneratedRegexAttribute, while the regular expression gets compiled at runtime when we use the RegexOptions.Compiled flag.

Using GeneratedRegexAttribute over RegexOptions.Compiled is preferred because it doesn’t introduce the startup cost that RegexOptions.Compiled does. Moreover, it generates C# code that is later translated into IL, instead of translating directly to IL, so it’s easy to view and debug.

Use Char.IsAsciiDigit() and All() Methods

In this approach, we are going to use the All() extension method to check if every character in the string is a number in conjunction with the char.IsAsciiDigit() method.

We utilize the IsAsciiDigit() method instead of solely relying on the IsDigit() method because IsDigit() incorporates Unicode checks, allowing it to match numbers written in various languages containing non-ASCII characters, such as Arabic or Thai. However, in this article, our focus is on English characters.

Let’s check this approach:

public static bool UsingCharIsDigit(string stringValue)
{
    return stringValue.All(char.IsAsciiDigit);
}

Here, we use the All() extension method that is responsible for iterating over each character in the input string and verifying if it represents a number. If every character is a number, we return true. Otherwise false.

Note that, as with the int.TryParse() method, this approach works for integer numbers as well. However, different from the int.TryParse() method, this approach can validate numbers that exceed the int data type max value.

Use Char.IsAsciiDigit() Method and Foreach Loop

The previous method has the advantage of being very readable. However, LINQ introduces some overhead because LINQ utilizes closures, which can result in slower performance.

Let’s fix that now by introducing a foreach loop instead of the All() method:

public static bool UsingCharIsDigitWithForeach(string stringValue)
{
    foreach (var c in stringValue)
    {
        if (!char.IsAsciiDigit(c))
            return false;
    }

    return true;
}   

We eliminate the closure usage by using a foreach loop. We use an early return strategy by checking if the character is not a digit, and then we instantly return false. This way, the algorithm becomes more efficient when encountering a non-numeric-only string because it doesn’t have to loop through the entire string.

Now let’s try another approach without using the IsAsciiDigit() method.

Use Character Value Comparison and Foreach Loop

We can optimize our code further by checking whether the character’s code is between 0 and 9. This way, we can skip a Unicode lookup that is performed by the IsDigit() method:

public static bool UsingCharIsBetween09(string stringValue)
{
    foreach(var c in stringValue)
    {
        if (c is < '0' or > '9')
            return false;
    }

    return true;
}

We simply replace the IsDigit() method call with a pattern. It checks whether the current character is not between 0 and 9, then early returns false. If all the characters were between 0 and 9, then our method returns true.

What Is the Fastest Way to Verify if a String Is a Number?

After learning the different ways to verify if a string is a number, let’s analyze a benchmark result to determine which is the fastest way to check if a string is a number:

|                      Method |         Mean |      Error |      StdDev |       Median | Allocated |
|---------------------------- |-------------:|-----------:|------------:|-------------:|----------:|
|        UsingCharIsBetween09 |     5.998 ns |  0.1485 ns |   0.4356 ns |     5.987 ns |         - |
| UsingCharIsDigitWithForeach |     7.485 ns |  0.2045 ns |   0.6031 ns |     7.239 ns |         - |
|                 IntTryParse |    10.520 ns |  0.4514 ns |   1.3024 ns |     9.994 ns |         - |
|          UsingCompiledRegex |    26.309 ns |  0.7267 ns |   2.0966 ns |    25.787 ns |         - |
|              DoubleTryParse |    36.563 ns |  0.7488 ns |   0.7005 ns |    36.216 ns |         - |
|            UsingCharIsDigit |    53.405 ns |  1.5270 ns |   4.4542 ns |    52.688 ns |      32 B |
|                  UsingRegex | 1,380.220 ns | 43.8359 ns | 128.5631 ns | 1,344.257 ns |    3192 B |

As we can see in this benchmark result, the UsingRegex approach is significantly slower than any other approach. Despite its slower performance, the regex approach is more flexible, allowing us to validate integers and non-integer values.

When we use the UsingCharIsDigit approach, we have the second-worst result, and we can use this approach only if we deal with mathematical integer numbers.

Although we don’t have much flexibility when we are using the IntTryParse or DoubleTryParse approaches, they still perform this job considerably faster than the UsingRegex approach.

If we need flexibility and don’t mind the drawbacks of compiled Regex, then it’s the overall winner. Being able to check strings for any specified pattern in less time than the DoubleTryParse approach is a fantastic result.

However, if our goal is only to check strings for characters that are integers between 0 and 9, the fastest approach is the UsingCharIsBetween09.

It’s worth mentioning that we can combine multiple approaches to achieve the same result. For example, if we require high performance, we can combine the IntTryParse and DoubleTryParse approaches to handle integer and non-integer values. These two approaches combined are still more efficient than the UsingRegex approach.

Conclusion

In this article, we have learned different ways to verify if a string can be parsed into a number.

We have seen the limitations of the IntTryParse and UsingCharIsDigit approach, as well as the flexibility that using Regex, gives us when we need to change how we verify the string. However, it’s less efficient than all other approaches we have discussed. On the other hand, we discussed how to make Regexes execute faster by making them compiled.

Furthermore, we have learned how to enhance code efficiency at the expense of lines of code by using foreach loops. Additionally, we have discovered that the DoubleTryParse approach can efficiently parse large numbers with or without decimal points.

Ultimately, it is up to us to decide which fits our algorithm and use case better.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!