formatting expiry date in mm/yy format

Viewed 17171

Hi I am writing an edittext in which I want expiry date of the credit card in MM/YY format. The algorithm I want to implement is as follows: If user enters anything from 2 to 9. I change the text input to 02/ to 09/ If the user enters 1, then I wait for the next digit and check if the int value month if less than 12. Here is my code for this.

@Override
            public void afterTextChanged(Editable s) { 
            String input = s.toString();
                if (s.length() == 1) {
                        int month = Integer.parseInt(input);
                        if (month > 1) {
                            mExpiryDate.setText("0" + mExpiryDate.getText().toString() + "/");
                            mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
                            mSeperator = true;
                        }

                }
                else if (s.length() == 2) {
                        int month = Integer.parseInt(input);
                        if (month <= 12) {
                            mExpiryDate.setText(mExpiryDate.getText().toString() + "/");
                            mExpiryDate.setSelection(mExpiryDate.getText().toString().length());                            
                            mSeperator = true;
                        }
                }
                else {

                }

            }

This works fine until I press a softkey back button. The back slash never goes back. The reason is the second if condition is always met. I am confused about how to solve this. How do I handle the back button inside aftertextchanged? Please help.

6 Answers

@alex solution above was good but it failed in a couple of instances. Like when you try to delete the slash, because it never really reaches if(s.length() == 2 && mLastInput.endsWith("/")) when you try to delete slash it will be stock at if(s.length() == 2 && !mLastInput.endsWith("/") and so it will give the illusion that the slash does not delete.

It also fails if the user complete the date i.e 08/16, and then take their cursor back to month and delete, it also fails if the date maybe some how ends up like 0/1. So I just made some changes to @alex's solution above.

//Make sure for mExpiryDate to be accepting Numbers only
boolean isSlash = false; //class level initialization 
private void formatCardExpiringDate(Editable s){
    String input = s.toString();
    String mLastInput = "";

    SimpleDateFormat formatter = new SimpleDateFormat("MM/yy",     Locale.ENGLISH);
    Calendar expiryDateDate = Calendar.getInstance();

    try {
        expiryDateDate.setTime(formatter.parse(input));
    } catch (java.text.ParseException e) {
        if (s.length() == 2 && !mLastInput.endsWith("/") && isSlash) {
            isSlash = false;
            int month = Integer.parseInt(input);
            if (month <= 12) {
                     mExpiryDate.setText(mExpiryDate.getText().toString().substring(0, 1));
                mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
            } else {
                s.clear();
                mExpiryDate.setText("");
                mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
                Toast.makeText(context.getApplicationContext(), "Enter a valid month", Toast.LENGTH_LONG).show();
            }
        }else if (s.length() == 2 && !mLastInput.endsWith("/") && !isSlash) {
            isSlash = true;
            int month = Integer.parseInt(input);
            if (month <= 12) {
                mExpiryDate.setText(mExpiryDate.getText().toString() + "/");
                mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
            }else if(month > 12){
                edCardDate.setText("");
                mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
                s.clear();
                _toastMessage("invalid month", context);
            }


        } else if (s.length() == 1) {

            int month = Integer.parseInt(input);
            if (month > 1 && month < 12) {
                isSlash = true;
                mExpiryDate.setText("0" + mExpiryDate.getText().toString() + "/");
                mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
            }
        }

        mLastInput = mExpiryDate.getText().toString();
        return;
    }
}
//wrap method formatCardExpiringDate around try catch or wrap the entire code in try catch, catching NumberFormateException. To take care of situations when s.length() == 2 and there is a a number in from of the slash
@Override
public void afterTextChanged(Editable s) { 
   try{
     formatCardExpiringDate(s)
    }catch(NumberFormatException e){
      s.clear(); 
      //Toast message here.. Wrong date formate

    }
}

I would check Month again when user click on submit just to be extra sure.

String expdate[] = mExpiryDate.getText().toString().split("/");
if(Integer.ParseInt(expDate[0]) > 12){
  // Toast message "wrong date format".... 
}

I hope this helps....

I used the solution of Uche Dim, fixed some issues and cleaned up the code.

So key improvements in my code are:

  1. if a user tries to enter "13", only "1" will get entered.
  2. when the user starts entering the year, after he has deleted the slash, a slash will be added so it keeps the format of MM/yy.

All in all this is almost like the Play Store's expiry field of new cards.

I've created a Kotlin class but usage is also added for Java.

CardExpiryTextWatcher class:

class CardExpiryTextWatcher(private val mTextInputLayout: TextInputLayout,
                            private val mServerDate: Date,
                            private val mListener: DateListener) : TextWatcher {

    private val mExpiryDateFormat = SimpleDateFormat("MM/yy", Locale.US).apply {
        isLenient = false
    }
    private var mLastInput = ""
    private var mIgnoreAutoValidationOnce = false

    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
    }

    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
    }

    @SuppressLint("SetTextI18n")
    override fun afterTextChanged(s: Editable) {
        val input = s.toString()
        when (s.length) {
            1 -> handleMonthInputForFirstCharacter(input)
            2 -> handleMonthInputForSecondCharacter(input)
            3 -> addSlashIfNotAddedAtEnd(input)
            4 -> addSlashIfNotAddedInMiddle(input)
            5 -> validateDateAndCallListener(input)
        }
        mLastInput = mTextInputLayout.editText!!.text.toString()
    }

    private fun validateDateAndCallListener(input: String) {
        try {
            if (mIgnoreAutoValidationOnce) {
                mIgnoreAutoValidationOnce = false
                return
            }
            if (input[2] == '/') {
                val date = mExpiryDateFormat.parse(input)
                validateCardIsNotExpired(date)
            }
        } catch (e: ParseException) {
            mTextInputLayout.error = mTextInputLayout.context.getString(R.string.card_exp_date_error)
        }
    }

    private fun validateCardIsNotExpired(cardExpiry: Date) {
        if (DateUtils.isDateBefore(cardExpiry, mServerDate)) {
            mTextInputLayout.error = mTextInputLayout.context.getString(R.string.card_expired)
            return
        }
        mListener.onExpiryEntered(cardExpiry)
    }

    @SuppressLint("SetTextI18n")
    private fun addSlashIfNotAddedAtEnd(input: String) {
        val lastCharacter = input[input.length - 1]
        if (lastCharacter != '/' && !input.startsWith('/')) {
            val month = input.substring(0, 2)
            mTextInputLayout.editText!!.setText("$month/$lastCharacter")
            mTextInputLayout.editText!!.setSelection(mTextInputLayout.editText!!.text.toString().length)
        }
    }

    @SuppressLint("SetTextI18n")
    private fun addSlashIfNotAddedInMiddle(input: String) {
        if (input.contains('/')) {
            return
        }
        val month = input.substring(0, 2)
        val year = input.substring(2, 4)
        mIgnoreAutoValidationOnce = true
        mTextInputLayout.editText!!.setText("$month/$year")
        mTextInputLayout.editText!!.setSelection(2)
    }

    @SuppressLint("SetTextI18n")
    private fun handleMonthInputForSecondCharacter(input: String) {
        if (mLastInput.endsWith("/")) {
            return
        }
        val month = Integer.parseInt(input)
        if (month > 12) {
            mTextInputLayout.editText!!.setText(mLastInput)
            mTextInputLayout.editText!!.setSelection(mTextInputLayout.editText!!.text.toString().length)
            mTextInputLayout.error = mTextInputLayout.context.getString(R.string.card_exp_date_error)
        } else {
            mTextInputLayout.editText!!.setText("${mTextInputLayout.editText!!.text}/")
            mTextInputLayout.editText!!.setSelection(mTextInputLayout.editText!!.text.toString().length)
        }
    }

    @SuppressLint("SetTextI18n")
    private fun handleMonthInputForFirstCharacter(input: String) {
        val month = Integer.parseInt(input)
        if (month in 2..11) {
            mTextInputLayout.editText!!.setText("0${mTextInputLayout.editText!!.text}/")
            mTextInputLayout.editText!!.setSelection(mTextInputLayout.editText!!.text.toString().length)
        }
    }

    interface DateListener {
        fun onExpiryEntered(date: Date)
    }

    companion object {

        @JvmStatic
        fun attachTo(textInputLayout: TextInputLayout, serverDate: Date, listener: DateListener) {
            textInputLayout.editText!!.addTextChangedListener(
                    CardExpiryTextWatcher(textInputLayout, serverDate, listener))
        }
    }
}

Usage (Kotlin):

CardExpiryTextWatcher.attachTo(inputCardExpiry, mServerDate, object : CardExpiryTextWatcher.DateListener {
    override fun onExpiryEntered(date: Date) {
        // TODO implement your handling
    }
})

Usage (Java):

CardExpiryTextWatcher.attachTo(inputCardExpiry, mServerDate, new CardExpiryTextWatcher.DateListener() {
    @Override
    public void onExpiryEntered(@NonNull Date date) {
        // TODO implement your handling
    }
});

Caveat: The date will always be 2 digit long (e.g. December 4 will be 04/12, not 4/12) but if the user removes one digit from the date, it can become 4/12 so you need to run following method before validation:

/**
 * Makes sure that the date's day is of 2 digits, (e.g. 4/12 will be converted to 04/12)
 * */
fun normalizeExpiryDate(expiryDate: String): String {
    if (expiryDate.length == 4 && expiryDate.indexOf('/') == 1) {
        return "0$expiryDate"
    }
    return expiryDate
}

Note: inputCardExpiry is the InputTextLayout which contains the EditText.

Add TextWatcher to your EditText & do the Validation with REGEX.

TextWatcher:

etCardExpiry.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {

            }

            @Override
            public void afterTextChanged(Editable editable) {
                if (editable.length() > 0 && (editable.length() % 3) == 0) {
                    final char c = editable.charAt(editable.length() - 1);
                    if ('/' == c) {
                        editable.delete(editable.length() - 1, editable.length());
                    }
                }
                if (editable.length() > 0 && (editable.length() % 3) == 0) {
                    char c = editable.charAt(editable.length() - 1);
                    if (Character.isDigit(c) && TextUtils.split(editable.toString(), String.valueOf("/")).length <= 2) {
                        editable.insert(editable.length() - 1, String.valueOf("/"));
                    }
                }
            }
        });

Validation with REGEX:

if (etCardExpiry.getText().toString().isEmpty()) {
                etCardExpiry.setError("Expiry cannot be empty. Format: MM/YY");
                return;
            } else if (etCardExpiry.getText().toString().length() < 5) {
                etCardExpiry.setError("Please check Card expiry & try again");
                return;
            } else if (etCardExpiry.getText().toString().matches("(?:0[1-9]|1[0-2])/[0-9]{2}")) {
                etCardExpiry.setError("Please check Card expiry format & try again");
                return;
            } else {
                // Passed Card Expiry validation
            }
Related