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.
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.
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.