String extensions using LINQ

Some days ago, I encountered this C# code which defines some more or less standard string operations:

using System;
 
namespace My.Extensions
{
    public static class StringExtensions
    {
        public static string Left(this string s, int count) {
            if(count > s.Length) {
                return s;
            }
 
            return s.Substring(0, count);
        }
 
        public static string Right(this string s, int count) {
            if(s.Length <= count) {
                return s;
            }
 
            return s.Substring(s.Length - count, count);
        }
 
        public static string Shorten(this string s, int count) {
            if(s.Length < count) {
                return String.Empty;
            }
 
            return s.Substring(0, s.Length - count);
        }
    }
}

Personally, I don’t like this kind of implementation. It looks too technical to me. The string manipulations, the condition checks – it’s completely from a technical point of view.

There is a way to implement that functionality in a descriptive way which makes it look more aesthetic. The key is to understand a string as a sequence of characters. Let’s try to rewrite the extension methods using LINQ:

using System.Linq;
 
namespace My.Extensions
{
    public static class StringExtensions
    {
        public static string Left(this string characters, int number) {
            return new string(characters.Take(number).ToArray());
        }
 
        public static string Right(this string characters, int number) {
            return new string(characters.Skip(characters.Length - number).ToArray());
        }
 
        public static string Shorten(this string characters, int number) {
            return new string(characters.Take(characters.Length - number).ToArray());
        }
    }
}

Better? In my eyes, yes! The code is shorter, it looks more precise. The core part of the methods is extremely descriptive. The methods meet their respective purpose by taking or skipping a certain number of characters of the input string – nice.

Unfortunately, there is one thing that is not so nice. There is some noise in the above implementation. We are forced by the .NET framework to switch from IEnumerable to string making a detour via an array of char. But by introducing another extension method, we make our short code example a little bit more readable again:

using System.Collections.Generic;
using System.Linq;
 
namespace My.Extensions
{
    public static class StringExtensions
    {
        public static string Left(this string characters, int number) {
            return characters.Take(number).AsString();
        }
 
        public static string Right(this string characters, int number) {
            return characters.Skip(characters.Length - number).AsString();
        }
 
        public static string Shorten(this string characters, int number) {
            return characters.Take(characters.Length - number).AsString();
        }
    }
 
    public static class EnumerableExtensions
    {
        public static string AsString(this IEnumerable characters) {
            return new string(characters.ToArray());
        }
    }
}

And exactly this is the driving force behind all the code changes we made here: readability! Code should be simple, readable, and self-explanatory.

So, what’s left? With respect to self-explanation, we maybe can find better names for the string extension methods. As the saying goes, I leave this as an exercise for you…

Just to mention, the original implementation as well as the one developed here satisfies this specification:

using Microsoft.VisualStudio.TestTools.UnitTesting;
 
namespace My.Extensions.Tests
{
    [TestClass]
    public class MyExtensionsSpecifications
    {
        private const string Abcdefghij = "abcdefghij";
 
        [TestMethod]
        public void The_first_5_characters_of_abcdefghij_are_abcde() {
            Assert.AreEqual("abcde", Abcdefghij.Left(5));
        }
 
        [TestMethod]
        public void The_first_50_characters_of_abcdefghij_are_abcdefghij() {
            Assert.AreEqual(Abcdefghij, Abcdefghij.Left(50));
        }
 
        [TestMethod]
        public void The_last_3_characters_of_abcdefghij_are_hij() {
            Assert.AreEqual("hij", Abcdefghij.Right(3));
        }
 
        [TestMethod]
        public void The_last_20_characters_of_abcdefghij_are_abcdefghij() {
            Assert.AreEqual(Abcdefghij, Abcdefghij.Right(20));
        }
 
        [TestMethod]
        public void abcdefghij_shortened_by_4_characters_is_abcdef() {
            Assert.AreEqual("abcdef", Abcdefghij.Shorten(4));
        }
 
        [TestMethod]
        public void abcdefghij_shortened_by_12_characters_is_an_empty_string() {
            Assert.AreEqual(string.Empty, Abcdefghij.Shorten(12));
        }
    }
}