How Best In C# To Get Last N Characters

How Best To Get Last N Characters Banner Image

Introduction

In C#, often there is a need in string parsing to get characters at the end of a string. There are a few different ways this can be done by substring, slice, and remove methods that are built-in C# functions. So we'll take a look at different aspects to determine which is best to use such as performance and readability. We often look at how compact the code can get between functions but in this case, they will also be the same. Next, we'll look at the good old string substring method.

Get Last N Characters With Substring

Substring is one of the most used methods in string operations and for good reason. It has been reliable for so long and is easy to read and use.

The current use case is that suppose that we have a string that has a date and a time but we only want to get time then we need to last few characters or so. Let's look at the example below.

Code Example For Get Last N Characters With Substring
string text = "08/12/2020 08:29:94";//Starting dateTime
const int numberOfCharacters = 8;//Number of characters to get from the end for the time
string newText = GetLastNCharactersBySubstring(text, numberOfCharacters);//Function call
Console.WriteLine("original:" + text);//Shows starting text
Console.WriteLine("updated:" + newText);//Shows modified text
string GetLastNCharactersBySubstring(string text, int numberOfCharacters)
{
    if (string.IsNullOrEmpty(text))//Check if text is empty or null
    {
        return "";
    }
    if (numberOfCharacters > (text.Length - 1))//Check if the number of characters is not greater than the string size itself
    {
        return "";
    }
    int startIndex = text.Length - numberOfCharacters;//Gets the start index of where the time starts
    string substring = text.Substring(startIndex);//Returns a new string from the start of the time to length - 1 which is the end 
    return substring;
}
Code Output
original:08/12/2020 08:29:94
updated:08:29:94

Substring only grabbed the last eight characters. This works by getting the start index of where the time starts by subtracting the length 19 minus the number of characters we want to get which was 8 so the start index is 11. This form of substring works by providing a start index then it grabs the rest of the characters to the end.

Performance Test For Substring

Next is a series of tests by calling the function again and again millions of times to see how well the performance is. Then we can compare the performance to the other methods to find the fastest one. Let's look at an example.

using System.Diagnostics;
int numberOfTests = 10;//Number of tests 
List<double> testSpeedList = new List<double>();
for (int i = 0; i < numberOfTests; i++)
{
    testSpeedList.Add(SubstringSpeedTest());
}
Console.WriteLine($"Substring Average speed:{Math.Round(testSpeedList.Average())}ms, In {numberOfTests} tests");
double SubstringSpeedTest()
{
    int numberOfFunctionCalls = 100000000;//Number of function calls made
    string dateTime = "08/12/2020 08:29:94";//starting dateTime
    const int numberOfCharacters = 8;
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();//Start the Stopwatch timer
    string alteredText = "";
    for (int i = 0; i < numberOfFunctionCalls; i++)
    {
        alteredText = GetLastNCharactersBySubstring(dateTime, numberOfCharacters);//Function under test
    }
    stopwatch.Stop();//Stop the Stopwatch timer
    Console.WriteLine($"sampleText:{dateTime}, alteredText:{alteredText}, Function calls:{numberOfFunctionCalls}, In {stopwatch.Elapsed.Minutes}m {stopwatch.Elapsed.Seconds}s {stopwatch.Elapsed.Milliseconds}ms");
    return stopwatch.Elapsed.TotalMilliseconds;
}
string GetLastNCharactersBySubstring(string text, int numberOfCharacters)
{
    if (string.IsNullOrEmpty(text))//Check if text is empty or null
    {
        return "";
    }
    if (numberOfCharacters > (text.Length - 1))//Check if the number of characters is not greater than the string size itself
    {
        return "";
    }
    int startIndex = text.Length - numberOfCharacters;//Gets the start index of where the time starts
    string substring = text.Substring(startIndex);//Returns a new string from the start of the time to length - 1 which is the end 
    return substring;
}
Code Output
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 1s 639ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 1s 414ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 1s 427ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 1s 406ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 1s 414ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 1s 442ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 1s 440ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 1s 435ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 1s 430ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 1s 425ms
Substring Average speed:1448ms, In 10 tests

From this test, substring completes the test with an average of 1448ms this will be the starting point to compare to the other tests.

Readability Analysis For Substring

The substring is very readable and the setup is simple. We added checks so that there can't be an exception thrown.

Get Last N Characters With Slice

Slice is not a string function and is more on the new side. It was made with performance in mind so we would expect it to be fast. Setup will be exactly like substring so let's look at an example.

Code Example For Getting Last N Characters With Slice
string text = "08/12/2020 08:29:94";//Starting dateTime
const int numberOfCharacters = 8;//Number of characters to get from the end for the time
string newText = GetLastNCharactersBySlice(text, numberOfCharacters);//Function call
Console.WriteLine("original:" + text);//Shows starting text
Console.WriteLine("updated:" + newText);//Shows modified text
string GetLastNCharactersBySlice(ReadOnlySpan<char> text, int numberOfCharacters)
{
    if (text == null)//Check if text is null
    {
        return "";
    }
    if (numberOfCharacters > (text.Length - 1))//Check if the number of characters is not greater than the string size itself
    {
        return "";
    }
    int startIndex = text.Length - numberOfCharacters;//Gets the start index of where the time starts
    ReadOnlySpan<char> substring = text.Slice(startIndex);//Returns a new string from the start of the time to length - 1 which is the end 
    return substring.ToString();
}
Code Output
original:08/12/2020 08:29:94
updated:08:29:94

The code is very similar to the substring example with some differences with the main change being the ReadOnlySpan struct. This enables the call to the slice method. The setup for the slice is the same as the substring.

Performance Test For Slice

Next, we'll perform the same test as with the substring. We would expect slice to be faster than substring since span is allocated on the stack and rather the heap where garbage collection can slow down the performance. Let's see how it does.

using System.Diagnostics;
int numberOfTests = 10;//Number of tests 
List<double> testSpeedList = new List<double>();
for (int i = 0; i < numberOfTests; i++)
{
    testSpeedList.Add(SliceSpeedTest());
}
Console.WriteLine($"Slice Average speed:{Math.Round(testSpeedList.Average())}ms, In {numberOfTests} tests");
double SliceSpeedTest()
{
    int numberOfFunctionCalls = 100000000;//Number of function calls made
    string dateTime = "08/12/2020 08:29:94";//starting dateTime
    const int numberOfCharacters = 8;
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();//Start the Stopwatch timer
    string alteredText = "";
    for (int i = 0; i < numberOfFunctionCalls; i++)
    {
        alteredText = GetLastNCharactersBySlice(dateTime, numberOfCharacters);//Function under test
    }
    stopwatch.Stop();//Stop the Stopwatch timer
    Console.WriteLine($"sampleText:{dateTime}, alteredText:{alteredText}, Function calls:{numberOfFunctionCalls}, In {stopwatch.Elapsed.Minutes}m {stopwatch.Elapsed.Seconds}s {stopwatch.Elapsed.Milliseconds}ms");
    return stopwatch.Elapsed.TotalMilliseconds;
}
string GetLastNCharactersBySlice(ReadOnlySpan<char> text, int numberOfCharacters)
{
    if (text == null)//Check if text is null
    {
        return "";
    }
    if (numberOfCharacters > (text.Length - 1))//Check if the number of characters is not greater than the string size itself
    {
        return "";
    }
    int startIndex = text.Length - numberOfCharacters;//Gets the start index of where the time starts
    ReadOnlySpan<char> substring = text.Slice(startIndex);//Returns a new string from the start of the time to length - 1 which is the end 
    return substring.ToString();
}
Code Output
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 3s 247ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 2s 794ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 2s 797ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 2s 840ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 2s 829ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 2s 849ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 2s 805ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 2s 831ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 2s 791ms
sampleText:08/12/2020 08:29:94, alteredText:08:29:94, Function calls:100000000, In 0m 2s 834ms
Slice Average speed:2862ms, In 10 tests

In this case, substring is still faster.

Readability Analysis For Slice

It is fairly easy to read the code with slice although the new structs may be new to some making it less readable over time people should become familiar with it.

Get Last N Characters With Remove

String Remove works have the same setup as substring but the key difference is that works by subtracting and returning what is left over. With that in mind, the setup is the same remove as in the two previous methods. Let's look at an example.

Code Example For Get Last N Characters With Remove
string text = "08/12/2020 08:29:94";//Starting dateTime
const int numberOfCharacters = 8;//Number of characters to get from the end for the time
string newText = GetLastNCharactersByRemove(text, numberOfCharacters);//Function call
Console.WriteLine("original:" + text);//Shows starting text
Console.WriteLine("updated:" + newText);//Shows modified text
string GetLastNCharactersByRemove(string text, int numberOfCharacters)
{
    if (string.IsNullOrEmpty(text))//Check if text is empty or null
    {
        return "";
    }
    if (numberOfCharacters > (text.Length - 1))//Check if the number of characters is not greater than the string size itself
    {
        return "";
    }
    int startIndex = text.Length - numberOfCharacters;//Gets the start index of where the time starts
    string substring = text.Remove(startIndex);//Removes characters from 0 to the start index then returns what is left over.
    return substring;
}
Code Output
original:08/12/2020 08:29:94
updated:08:29:94

As with the previous methods, we see that the output is the same as the setup and the number of lines it takes. Now we'll look to see if there are any differences in the performance and readability.

Performance Test For Remove
using System.Diagnostics;
int numberOfTests = 10;//Number of tests 
List<double> testSpeedList = new List<double>();
for (int i = 0; i < numberOfTests; i++)
{
    testSpeedList.Add(SubstringSpeedTest());
}
Console.WriteLine($"Remove Average speed:{Math.Round(testSpeedList.Average())}ms, In {numberOfTests} tests");
double SubstringSpeedTest()
{
    int numberOfFunctionCalls = 100000000;//Number of function calls made
    string dateTime = "08/12/2020 08:29:94";//starting dateTime
    const int numberOfCharacters = 8;
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();//Start the Stopwatch timer
    string alteredText = "";
    for (int i = 0; i < numberOfFunctionCalls; i++)
    {
        alteredText = GetLastNCharactersByRemove(dateTime, numberOfCharacters);//Function under test
    }
    stopwatch.Stop();//Stop the Stopwatch timer
    Console.WriteLine($"sampleText:{dateTime}, alteredText:{alteredText}, Function calls:{numberOfFunctionCalls}, In {stopwatch.Elapsed.Minutes}m {stopwatch.Elapsed.Seconds}s {stopwatch.Elapsed.Milliseconds}ms");
    return stopwatch.Elapsed.TotalMilliseconds;
}
string GetLastNCharactersByRemove(string text, int numberOfCharacters)
{
    if (string.IsNullOrEmpty(text))//Check if text is empty or null
    {
        return "";
    }
    if (numberOfCharacters > (text.Length - 1))//Check if the number of characters is not greater than the string size itself
    {
        return "";
    }
    int startIndex = text.Length - numberOfCharacters;//Gets the start index of where the time starts
    string substring = text.Remove(startIndex);//Removes characters from 0 to the start index then return what is left over.
    return substring;
}
Code Output
  sampleText:08/12/2020 08:29:94, alteredText:08/12/2020 , Function calls:100000000, In 0m 1s 761ms
  sampleText:08/12/2020 08:29:94, alteredText:08/12/2020 , Function calls:100000000, In 0m 1s 407ms
  sampleText:08/12/2020 08:29:94, alteredText:08/12/2020 , Function calls:100000000, In 0m 1s 389ms
  sampleText:08/12/2020 08:29:94, alteredText:08/12/2020 , Function calls:100000000, In 0m 1s 394ms
  sampleText:08/12/2020 08:29:94, alteredText:08/12/2020 , Function calls:100000000, In 0m 1s 401ms
  sampleText:08/12/2020 08:29:94, alteredText:08/12/2020 , Function calls:100000000, In 0m 1s 404ms
  sampleText:08/12/2020 08:29:94, alteredText:08/12/2020 , Function calls:100000000, In 0m 1s 400ms
  sampleText:08/12/2020 08:29:94, alteredText:08/12/2020 , Function calls:100000000, In 0m 1s 453ms
  sampleText:08/12/2020 08:29:94, alteredText:08/12/2020 , Function calls:100000000, In 0m 1s 489ms
  sampleText:08/12/2020 08:29:94, alteredText:08/12/2020 , Function calls:100000000, In 0m 1s 448ms
  Remove Average speed:1455ms, In 10 tests

Remove comes in at 1455ms which is very close to substring which is enough to say that it is tied in performance for this use case.

Readability Analysis For Remove

Conclusion

Overall RankMethodSpeedConciseReadability(1-5)
1String Substring1448ms11 lines5
2String Remove1455ms11 lines4
3Span Slice2862ms11 lines3

The best way to get the last n characters is to use a string substring. It has one of the fastest methods. It is also very readable that it does exactly what it says it does and you don't have to guess. Substring beats remove by being more readable even though the setup for the function parameters is the same. Remove comes off as removing characters rather than getting a subset of characters even though that's what it does in this case. Span slice had a disappointing showing in this use case. Performance is the biggest factor in the code and it was too slow in this case. It has been faster in other use cases so keep in mind to use the best tools for each use case.

Get Latest Updates