EmilioV
EmilioV

Reputation: 242

How to mask a string using different parameters in an effective and fast way

I'd like to mask a string using different parameters like percentage of char to be masked, the mask character and the position to apply the mask (In the beginning, in the middle or at the end of the string). I've come up with a solution but I presume that this is not the best solution. This is my code:

public static string MaskChars(this string value, char maskToApply = 'X', int percentToApply = 25, MaskOption maskOptions = MaskOption.InTheMiddleOfString)
    {
        string valueTrimmed = value.Trim();
        int len = valueTrimmed.Length;

        if (len == 0)
            return Empty;
        if (percentToApply >= 100)
            return maskToApply.ToString(CultureInfo.InvariantCulture).Replicate(len);

        var charsToMask = (int)Math.Round((decimal)(percentToApply * len) / 100);
        if (charsToMask == 0)
            charsToMask = 1;

        int top = len - charsToMask;
        int maskCounter = 0;
        var builder = new StringBuilder(len);

        for (int i = 0; i < len; i++)
        {
            if (maskCounter < charsToMask)
            {
                switch (maskOptions)
                {
                    // Apply mask in the middle of the string
                    case MaskOption.InTheMiddleOfString:
                        if (i >= charsToMask && i < top)
                        {
                            builder.Append(maskToApply);
                            maskCounter++;
                        }
                        break;
                    // Apply mask at the begining of the string
                    case MaskOption.AtTheBeginingOfString:
                        if (i < charsToMask)
                        {
                            builder.Append(maskToApply);
                            maskCounter++;
                        }
                        break;
                    // Apply mask at the end of the string
                    case MaskOption.AtTheEndOfString:
                        if (i >= top)
                        {
                            builder.Append(maskToApply);
                            maskCounter++;
                        }
                        break;
                }
            }
            else
            {
                builder.Append(valueTrimmed[i]);
            }
        }

        return builder.ToString();
    }

where:

public enum MaskOption : byte
{
    AtTheBeginingOfString = 1,
    InTheMiddleOfString = 2,
    AtTheEndOfString = 3
}

and Replicate is a simple method to replicate a string

public static string Replicate(this string value, int count)
    {
        if (IsNullOrEmpty(value))
            return Empty;
        if (count <= 0)
            return value;

        var builder = new StringBuilder();
        builder.Append(value);
        for (int i = count; i >= 1; i--)
            builder.Append(value);

        return builder.ToString();
    }

Upvotes: 1

Views: 2297

Answers (2)

Assil
Assil

Reputation: 690

You can try another way of doing the same thin.. Less parameters and easier to test and easier to read.

Mind you that I did not post the interface and small details like Guard.

   public class StringMask : IStringMask
    {
        /// <summary>
        /// The Mask character
        /// </summary>
        private readonly char MaskCharacter;

        /// <summary>
        /// The instance
        /// </summary>
        private readonly string Instance;

        /// <summary>
        /// The Mask
        /// </summary>
        private BitArray Mask;

        /// <summary>
        /// Initializes a new instance of the <see cref="StringMask"/> class.
        /// </summary>
        /// <param name="instance">The string you would like to mask.</param>
        /// <param name="maskCharacter">The Mask character.</param>
        public StringMask(string instance, char maskCharacter)
        {
            MaskCharacter = maskCharacter;
            Instance = instance;
            Mask = new BitArray(instance.Length, false);
        }



        /// <summary>
        /// Shows the first [number] of characters and masks the rest.
        /// </summary>
        /// <param name="number">The number of the characters to show.</param>
        /// <returns>IStringMask.</returns>
        public IStringMask ShowFirst(int number)
        {
            Validate(number);

            for (int i = 0; i < number; i++)
            {
                Mask[i] = true;
            }
            return this;
        }

        /// <summary>
        /// Shows the last [number] of characters and masks the rest.
        /// </summary>
        /// <param name="number">The number of the characters to show.</param>
        /// <returns>IStringMask.</returns>
        public IStringMask ShowLast(int number)
        {
            Validate(number);

            for (int i = 0; i < number; i++)
            {
                Mask[Instance.Length - i - 1] = true;
            }
            return this;

        }

        /// <summary>
        /// Returns a <see cref="System.String" /> that represents this instance.
        /// </summary>
        /// <returns>A <see cref="System.String" /> that represents this instance.</returns>
        public override string ToString()
        {
            var sb = new StringBuilder();
            for (int i = 0; i < Instance.Length; i++)
            {
                if (Mask[i])
                    sb.Append(Instance[i]);
                else
                    sb.Append(MaskCharacter);
            }

            return sb.ToString();
        }

        private void Validate(int number)
        {
            Guard.IsBetweenExclusive(number, 0, Instance.Length, nameof(number));
        }
    }

While you can see here the Tests for that:

 [TestFixture]
    internal class MaskTests
    {
        private string input = "40770058698999513265";
        private char maskChar = 'X';
        private IStringMask mask;

        [SetUp]
        public void Initiate()
        {
            mask = new StringMask(input, maskChar);
        }

        [Test]
        public void MaskShowLast()
        {
            var output = mask.ShowLast(10);
            Console.WriteLine(output);
            Assert.AreEqual("XXXXXXXXXX8999513265", output.ToString());
        }



        [Test]
        public void MaskInTheMiddle()
        {
            var output = mask.ShowLast(5).ShowFirst(5);
            Console.WriteLine(output);
            Assert.AreEqual("40770XXXXXXXXXX13265", output.ToString());
        }

        [Test]
        public void MaskInTheMiddleTooShort()
        {
            Assert.Throws<ArgumentOutOfRangeException>(() => mask.ShowLast(0).ShowFirst(0));
        }

        [Test]
        public void MaskInTheMiddleTooLong()
        {
            Assert.Throws<ArgumentOutOfRangeException>(()=> mask.ShowLast(500).ShowFirst(500));
        }

        [Test]
        public void MaskAtTheEnd()
        {
            var output = mask.ShowFirst(10);
            Console.WriteLine(output);
            Assert.AreEqual("4077005869XXXXXXXXXX", output.ToString());

        }
    }

Upvotes: 1

Rufus L
Rufus L

Reputation: 37030

First I ran your code to see what the expected behavior was, and it doesn't look right to me. Here's the test code and the output:

var testStr = "This is my string to mask characters in!";
Console.WriteLine(testStr.MaskChars('X', 25, Extensions.MaskOption.AtTheBeginingOfString));
Console.WriteLine(testStr.MaskChars('X', 25, Extensions.MaskOption.InTheMiddleOfString));
Console.WriteLine(testStr.MaskChars(maskOptions: Extensions.MaskOption.AtTheEndOfString));

Output

enter image description here

I was under the impression that the string should remain the same length, and that the masked characters would just change position within the string. Also, I'm not sure why you're trimming the string (I wouldn't do that in this method, I would let the caller decide if they wanted to trim it first), but I left that part in.

Here's how I would simplify the code to do that:

public static string MaskChars(this string input, char maskChar, 
    int percentToApply, MaskOption maskOptions)
{
    // I would remove this. The caller can trim the string first if they want.
    var result = input.Trim(); 

    if (result.Length == 0 || percentToApply < 1) return result;
    if (percentToApply >= 100) return new string(maskChar, result.Length);

    var maskLength = Math.Max((int) Math.Round(percentToApply * result.Length / 100m), 1);
    var mask = new string(maskChar, maskLength);

    switch (maskOptions)
    {
        case MaskOption.AtTheBeginingOfString:
            result = mask + result.Substring(maskLength);
            break;
        case MaskOption.AtTheEndOfString:
            result = result.Substring(0, result.Length - maskLength) + mask;
            break;
        case MaskOption.InTheMiddleOfString:
            var maskStart = (result.Length - maskLength) / 2;
            result = result.Substring(0, maskStart) + mask + 
                result.Substring(maskStart + maskLength);
            break;
    }

    return result;
}

Output

enter image description here

The last thing I would do is get rid of the default argument values in the method, and create some overload methods instead that use default values for the missing arguments. This way the users can adjust the values the want (in your implementation, if they want to only change the MaskOption, then they have to re-state the other default values or use named parameters as I did above):

private static char defaultMaskChar = 'X';
private static MaskOption defaultMaskOption = MaskOption.InTheMiddleOfString;
private static int defaultPercentToApply = 25;

public static string MaskChars(this string input)
{
    return MaskChars(input, defaultMaskChar);
}

public static string MaskChars(this string input, char maskChar)
{
    return MaskChars(input, maskChar, defaultPercentToApply);
}

public static string MaskChars(this string input, int percentToApply)
{
    return MaskChars(input, defaultMaskChar, percentToApply);
}

public static string MaskChars(this string input, MaskOption maskOption)
{
    return MaskChars(input, defaultMaskChar, defaultPercentToApply, maskOption);
}

public static string MaskChars(this string input, char maskChar, int percentToApply)
{
    return MaskChars(input, maskChar, percentToApply, defaultMaskOption);
}

public static string MaskChars(this string input, char maskChar, MaskOption maskOption)
{
    return MaskChars(input, maskChar, defaultPercentToApply, maskOption);
}

public static string MaskChars(this string input, int percentToApply, MaskOption maskOption)
{
    return MaskChars(input, defaultMaskChar, percentToApply, maskOption);
}

Upvotes: 3

Related Questions