- String
- How Best To Get Last N Characters
How Best In C# To Get Last N Characters

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 Rank | Method | Speed | Concise | Readability(1-5) |
---|---|---|---|---|
1 | String Substring | 1448ms | 11 lines | 5 |
2 | String Remove | 1455ms | 11 lines | 4 |
3 | Span Slice | 2862ms | 11 lines | 3 |
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.