23

I have a card number as a string, for example:

string  ClsCommon.str_CardNumbe r = "3456123434561234";

The length of this card number can vary from 16 to 19 digits, depending on the requirement.

My requirement is that I have to show the first six digits and the last 4 digits of a card number and mask the other characters in between with the character 'X'.

I have tried using subString and implemented it separately for 16,17,18,19 digits..

I split string(ClsCommon.str_CardNumber) to 5 strings (str_cardNum1, str_cardNum2, str_cardNum3, str_cardNum4, str_cardNum5 - 4 digits for each string..remaining digits for 5th string)

All the strings are placed in ClsCommon file. Based on that I implemented the below, which works perfectly:

if (ClsCommon.str_CardNumber.Length == 16) {
    txtmskcrdnum.Text = string.Concat(ClsCommon.str_cardNum1, " ", ClsCommon.str_cardNum2.Substring(0, 2), "XX", " ", "XXXX", " ", ClsCommon.str_cardNum4);

}
if (ClsCommon.str_CardNumber.Length == 17) {
    txtmskcrdnum.Text = string.Concat(ClsCommon.str_cardNum1, " ", ClsCommon.str_cardNum2.Substring(0, 2), "XX", " ", "XXXX", " ", "X", ClsCommon.str_cardNum4.Substring(1, 3), " ", ClsCommon.str_cardNum5);
}
if (ClsCommon.str_CardNumber.Length == 18) {
    txtmskcrdnum.Text = string.Concat(ClsCommon.str_cardNum1, " ", ClsCommon.str_cardNum2.Substring(0, 2), "XX", " ", "XXXX", " ", "XX", ClsCommon.str_cardNum4.Substring(2, 2), " ", ClsCommon.str_cardNum5);
}


if (ClsCommon.str_CardNumber.Length == 19) {
    txtmskcrdnum.Text = string.Concat(ClsCommon.str_cardNum1, " ", ClsCommon.str_cardNum2.Substring(0, 2), "XX", " ", "XXXX", " ", "XXX", ClsCommon.str_cardNum4.Substring(3, 1), " ", ClsCommon.str_cardNum5);
}
txtmskcrdnum.Text = ClsCommon.str_CardNumber.PadLeft(ClsCommon.str_CardNumber.Length, 'X').Substring(ClsCommon.str_CardNumber.Length - 4);

For multiple lengths, the above approach is not useful.

I want a single approach which displays the first 6 and last 4 digits and masks other digits with X. The final string should have a space between every 4 digits.

rpax
  • 4,468
  • 7
  • 33
  • 57
Kartiikeya
  • 2,358
  • 7
  • 33
  • 68

13 Answers13

39

This will work with any card number length:

var cardNumber = "3456123434561234";

var firstDigits = cardNumber.Substring(0, 6);
var lastDigits = cardNumber.Substring(cardNumber.Length - 4, 4);

var requiredMask = new String('X', cardNumber.Length - firstDigits.Length - lastDigits.Length);

var maskedString = string.Concat(firstDigits, requiredMask, lastDigits);
var maskedCardNumberWithSpaces = Regex.Replace(maskedString, ".{4}", "$0 ");
Yannick Meeus
  • 5,643
  • 1
  • 35
  • 34
  • if initial number is given with spaces (or other dividers) e.g. `var cardNumber = "3456 1234 3456 1234";` the final result will be *incorrect one* `3456 1XX XXXX XXX1 234`; the other issue is that usually we want to *preserve* the given format (e.g. `3456-1234-3456-1234`) and just mask digits (`3456-12XX-XXXX-1234`). – Dmitry Bychenko Jun 23 '15 at 10:59
  • 3
    @DmitryBychenko, agreed, but that's outside of the scope of the question. We tend to completely remove all non-digit characters, validate the card number via Luhn and Bin checks, then spit it back out masked and sometimes, formatted. – Yannick Meeus Jun 23 '15 at 11:00
  • 2
    I agree with yannick's answer. Strip out all special characters, mask the digits, then re-format the number with dashes or spaces. Your just making life difficult trying to preserve formatting the whole way through. – Lee Harrison Jun 23 '15 at 13:40
4

Try this one. Simple and straight forward.

public static class StringExtensions
{
    public static string Masked(this string source, int start, int count)
    {
        return source.Masked('x', start, count);
    }

    public static string Masked(this string source, char maskValue, int start, int count)
    {
        var firstPart = source.Substring(0, start);
        var lastPart = source.Substring(start + count);
        var middlePart = new string(maskValue, count);

        return firstPart + middlePart + lastPart;
    }
}
jmvtrinidad
  • 3,347
  • 3
  • 22
  • 42
3

I would do something like this (pseudo C# - take as rough idea to build upon).

Untested code ahead...

string MaskDigits(string input)
{
    //take first 6 characters
    string firstPart = input.Substring(0, 6);

    //take last 4 characters
    int len = input.Length;
    string lastPart = input.Substring(len - 4, 4);

    //take the middle part (XXXXXXXXX)
    int middlePartLenght = input.Substring(6, len - 4).Count();
    string middlePart = new String('X', 5);

    return firstPart + middlePart + lastPart;
}
Yam
  • 25
  • 1
  • 3
David Votrubec
  • 3,968
  • 3
  • 33
  • 44
1

I'm sure there is a cleaner way to do this:

int currentChar = 0;
string maskable = "11111144441111";

string masked = maskable;
int length = masked.Length;

int startMaskPoint = 6;
int endMaskPoint = length - 4 - startMaskPoint;

masked = masked.Remove(startMaskPoint, endMaskPoint);

int numRemoved = length - masked.Length;
string Mask = "";
while (numRemoved != 0)
{
    Mask = Mask + "#";
    numRemoved--;
}

masked = masked.Insert(startMaskPoint, Mask);
string returnableString = masked;
while (length > 4)
{
    returnableString = returnableString.Insert(currentChar + 4, " ");
    currentChar = currentChar + 5;
    length = length - 4;
}
chris85
  • 23,846
  • 7
  • 34
  • 51
Ted James
  • 111
  • 7
  • i think you have that backwards - the first 6 and last 4 characters need to display, with everything else masked. – user1666620 Jun 23 '15 at 10:25
  • After reading @Yannicks answer, id go with his method. I didn't know you could create a string like that :) – Ted James Jun 23 '15 at 10:31
1

Possible implementation (acccepts varios formats e.g. numbers can be divided into groups etc.):

private static String MaskedNumber(String source) {
  StringBuilder sb = new StringBuilder(source);

  const int skipLeft = 6;
  const int skipRight = 4;

  int left = -1;

  for (int i = 0, c = 0; i < sb.Length; ++i) {
    if (Char.IsDigit(sb[i])) {
      c += 1;

      if (c > skipLeft) {
        left = i;

        break;
      }
    }
  }

  for (int i = sb.Length - 1, c = 0; i >= left; --i)
    if (Char.IsDigit(sb[i])) {
      c += 1;

      if (c > skipRight)
        sb[i] = 'X';
    }

  return sb.ToString();
}

// Tests 

  // 3456-12XX-XXXX-1234
  Console.Write(MaskedNumber("3456-1234-3456-1234"));
  // 3456123XXXXX1234
  Console.Write(MaskedNumber("3456123434561234"));

this implementation just masks the digits and preserve the format.

Dmitry Bychenko
  • 180,369
  • 20
  • 160
  • 215
1

One method:

string masked = null;
for (int i = 0; i < str_CardNumber.Length; i++) {
    masked += (i > 5 && i < str_CardNumber.Length - 4) ? 'X' : str_CardNumber[i];
    if ((i + 1) % 4 == 0)
        masked += " ";
}
Alex K.
  • 171,639
  • 30
  • 264
  • 288
1

How about replacing a specific matched group using Regex :

        string cardNumber = "3456123434561234";
        var pattern = "^(.{6})(.+)(.{4})$";
        var maskedNumber = Regex.Replace(cardNumber, pattern, (match) =>
        {
           return Regex.Replace(String.Format("{0}{1}{2}",
           match.Groups[1].Value, // the first 6 digits
           new String('X', match.Groups[2].Value.Length), // X times the 'X' char
           match.Groups[3].Value) /*the last 4 digits*/,".{4}", "$0 "); //finally add a separator every 4 char
        });
WizLiz
  • 2,068
  • 5
  • 25
  • 39
1

Linq saves coding lines, small code snippet.

Replaces with (*) char above 6 and bellow CardPan length minus 4

var CardPan = "1234567890123456";
var maskedPan = CardPan.Aggregate(string.Empty, (value, next) =>
{
    if (value.Length >= 6 && value.Length < CardPan.Length - 4)
    {
        next = '*';
    }
    return value + next;
});
1

I think this one is the simplest form. PadLeft/PadRight did not help me. I don't know and not found in the suggested list, but Right function is not in asp.net core c# code.

Suppose I have credit card no( "2512920040512345") and mask all digit except last 4 digit ("XXXXXXXXXXXX2345").

//C# code will give you the desired output. Replace the string with your CreditCardno variable
new string('X', "2512920040512345".Length-4)+ "2512920040512345".Substring( "2512920040512345".Length-4, 4)
Ajay2707
  • 5,690
  • 6
  • 40
  • 58
0

Many of the given solutions parse the input multiple times. Below I present a solution that parses the input only once. But I have no experience in C#, so the function is written in Scheme.

The function is divided into two:

(1) visit-first-6 parses the first six characters and concatenates them to the rest of the computation. When visit-first-6 has parsed the first six characters, it calls visit-rest.

(2) visit-rest exploits the fact that we can delay some computation until we have gained more knowledge. In this case, we wait to determine whether the element in the list should be shown until we know how many characters are left.

(define (mask xs)
  (letrec ([visit-first-6 (lambda (xs chars-parsed)
                            (cond
                              [(null? xs)
                               ;; Shorter than 6 characters.
                               '()]
                              [(< chars-parsed 6)
                               ;; Still parsing the first 6 characters
                               (cons (car xs)
                                     (visit-first-6 (cdr xs)
                                                    (1+ chars-parsed)))]
                              [else
                               ;; The first 6 characters have been parsed.
                               (visit-rest xs
                                           (lambda (ys chars-left)
                                             ys))]))]
           [visit-rest (lambda (xs k)
                         (if (null? xs)
                             ;; End of input
                             (k '() 0)
                             ;; Parsing rest of the input
                             (visit-rest (cdr xs)
                                         (lambda (rest chars-left)
                                           (if (< chars-left 4)
                                               ;; Show the last 4 characters
                                               (k (cons (car xs) rest)
                                                  (1+ chars-left))
                                               ;; Don't show the middle characters
                                               (k (cons "X"
                                                        rest)
                                                  (1+ chars-left)))))))])
    (visit-first-6 xs
                   0)))

Running mask in the Petite Chez Scheme interpreter

> (mask '(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18))
(1 2 3 4 5 6 "X" "X" "X" "X" "X" "X" "X" "X" 15 16 17 18)
> (mask '())
()
> (mask '(1 2 3 4))
(1 2 3 4)
> (mask '(1 2 3 4 5))
(1 2 3 4 5)
> (mask '(1 2 3 4 5 6 7 8 9))
(1 2 3 4 5 6 7 8 9)
> (mask '(1 2 3 4 5 6 7 8 9 10))
(1 2 3 4 5 6 7 8 9 10)
> (mask '(1 2 3 4 5 6 7 8 9 10 11))
(1 2 3 4 5 6 "X" 8 9 10 11)

NB. I saw this as a funny exercise and I figured I might as well share it. Yannick Meeus has already provided an easily understandable solution. So, this only serves for the interested.

Little Helper
  • 1,870
  • 3
  • 12
  • 20
0
str.substring(0, 5) +
                str.substring(5, str.length() - 3)
                .replaceAll("[\\d]", "x") +
                str.substring(str.length() - 3, str.length());

//Here is the simple way to do

0

The better way to do it is by using string format as seen below:

YourData = string.Format("************{0}", YourData.Trim().Substring(12, 4));
0
"3456123434561234".replace(/\D/g, '').replace(/^(.{6})(.+)(.{4})$/g, '$1******$3');
>'345612******1234'
"3456-1234-3456-1234".replace(/\D/g, '').replace(/^(.{6})(.+)(.{4})$/g, '$1******$3');
>'345612******1234'
"3456 1234 3456 1234".replace(/\D/g, '').replace(/^(.{6})(.+)(.{4})$/g, '$1******$3');
>'345612******1234'