форматирование даты истечения срока действия в формате мм/гг

Привет, я пишу текст редактирования, в котором я хочу указать дату истечения срока действия кредитной карты в формате ММ/ГГ. Алгоритм, который я хочу реализовать, выглядит следующим образом: если пользователь вводит что-либо от 2 до 9. Я изменяю ввод текста с 02/ на 09/. Если пользователь вводит 1, я жду следующую цифру и проверяю, соответствует ли месяц значению int если меньше 12. Вот мой код для этого.

@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 {

                }

            }

Это работает нормально, пока я не нажму кнопку возврата программной клавиши. Обратная косая черта никогда не возвращается. Причина вторая, если условие всегда выполняется. Я в замешательстве, как это решить. Как мне обрабатывать кнопку «Назад» внутри aftertextchanged? Пожалуйста помоги.


person user1051505    schedule 16.12.2013    source источник
comment
YXou не может удалить обратную косую черту, потому что после XX/ до XX выполняется условие сценария, а затем вы снова добавляете / -> это цикл   -  person alex    schedule 16.12.2013
comment
именно то, что я пытался объяснить   -  person user1051505    schedule 16.12.2013
comment
отлично, так что посмотрите на мой ответ, просто проверьте ввод при каждом изменении и, если он действителен, сделайте свое дело   -  person alex    schedule 16.12.2013
comment
Я пытаюсь проверить ввод следующим образом: if (s.toString().matches((?:0[1-9]|1[0-2])/[0-9]{2})) но это не работает должным образом   -  person user1051505    schedule 16.12.2013
comment
Тогда ваше регулярное выражение неверно. Но что плохого в использовании SimpleDateFormat? Это сделано для рабочих/разборных дат.   -  person alex    schedule 16.12.2013
comment
Ничего, я просто тестирую.   -  person user1051505    schedule 16.12.2013
comment
Затем посмотрите на следующую ссылку, там есть регулярное выражение для проверки даты истечения срока действия кредитной карты, хорошо объясненное: срок действия карты/" rel="nofollow noreferrer">robarspages.ca/web-development/ :)   -  person alex    schedule 16.12.2013
comment
Это не решает мою проблему. Я хочу иметь автоматическую косую черту после ввода второго символа. Если я ввел 9, то он автоматически должен стать 09/. Я опубликую свой код по другому вопросу, если хотите. Но это не решение проблемы.   -  person user1051505    schedule 16.12.2013
comment
давайте продолжим это обсуждение в чате   -  person user1051505    schedule 16.12.2013


Ответы (6)


Смотрите мой комментарий выше, чтобы понять вашу проблему. Вы можете использовать это для проверки ввода пользователя с помощью вашего текстового наблюдателя:

SimpleDateFormat formatter = 
    new SimpleDateFormat("MM/yy", Locale.GERMANY);
Calendar expiryDateDate = Calendar.getInstance();
try {
    expiryDateDate.setTime(formatter.parse(mExpiryDate.getText().toString()));
} catch (ParseException e) {
    //not valid
}
// expiryDateDate has a valid date from the user

Таким образом, в целом это будет:

String lastInput ="";

@Override
public void afterTextChanged(Editable s) { 
     String input = s.toString();
     SimpleDateFormat formatter = new SimpleDateFormat("MM/yy", Locale.GERMANY);
    Calendar expiryDateDate = Calendar.getInstance();
    try {
        expiryDateDate.setTime(formatter.parse(input));
    } catch (ParseException e) {
        if (s.length() == 2 && !lastInput.endsWith("/")) {
            int month = Integer.parseInt(input);
            if (month <= 12) {
               mExpiryDate.setText(mExpiryDate.getText().toString() + "/");
            }
        }else if (s.length() == 2 && lastInput.endsWith("/")) {
            int month = Integer.parseInt(input);
            if (month <= 12) {
               mExpiryDate.setText(mExpiryDate.getText().toString().subStr(0,1);
            }
        }
        lastInput = mExpiryDate.getText().toString();
        //because not valid so code exits here
        return;
    }
    // expiryDateDate has a valid date from the user
    // Do something with expiryDateDate here
}

Наконец полное решение:

String input = s.toString();
SimpleDateFormat formatter = new SimpleDateFormat("MM/yy", Locale.GERMANY);
Calendar expiryDateDate = Calendar.getInstance();
try {
   expiryDateDate.setTime(formatter.parse(input));
} catch (ParseException e) {

} catch (java.text.ParseException e) {
if (s.length() == 2 && !mLastInput.endsWith("/")) {
   int month = Integer.parseInt(input);
   if (month <= 12) {
      mExpiryDate.setText(mExpiryDate.getText().toString() + "/");
      mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
   }
}else if (s.length() == 2 && mLastInput.endsWith("/")) {
   int month = Integer.parseInt(input);
    if (month <= 12) {
       mExpiryDate.setText(mExpiryDate.getText().toString().substring(0,1));
       mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
    } else {
       mExpiryDate.setText("");
       mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
       Toast.makeText(getApplicationContext(), "Enter a valid month", Toast.LENGTH_LONG).show();
    }
} else if (s.length() == 1){
    int month = Integer.parseInt(input);
    if (month > 1) {
       mExpiryDate.setText("0" + mExpiryDate.getText().toString() + "/");
       mExpiryDate.setSelection(mExpiryDate.getText().toString().length());
    }
}
else {

}
mLastInput = mExpiryDate.getText().toString();
return;
person alex    schedule 16.12.2013
comment
Кроме того, очень плохая идея выделять SimpleDateFormat в afterTextChanged(). Лучше создать окончательный статический экземпляр. - person W.K.S; 04.01.2016
comment
На самом деле лучше использовать экземпляр ThreadLocal<SimpleDateFormat>, потому что DateFormatters не является потокобезопасным. - person Martin Marconcini; 19.04.2017
comment
@Martin Macaroni: Это здесь не применимо, потому что все работает в потоке пользовательского интерфейса, поэтому он однопоточный. Последний экземпляр SimpleDateFormat имел бы смысл в полном классе, но я просто набросал пример - person alex; 20.04.2017

Решение @alex выше было хорошим, но в нескольких случаях оно не удалось. Например, когда вы пытаетесь удалить косую черту, потому что она никогда не достигает if(s.length() == 2 && mLastInput.endsWith("/")), когда вы пытаетесь удалить косую черту, она будет на складе if(s.length () == 2 && !mLastInput.endsWith("/") и таким образом создастся иллюзия, что косая черта не удаляется.

Это также терпит неудачу, если пользователь завершает дату, т.е. 16/08, а затем возвращает свой курсор к месяцу и удаляет, это также терпит неудачу, если дата, возможно, каким-то образом заканчивается как 0/1. Поэтому я только что внес некоторые изменения в решение @alex выше.

//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

    }
}

Я бы проверил Месяц еще раз, когда пользователь нажмет кнопку «Отправить», просто чтобы быть уверенным.

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

Надеюсь, это поможет....

person Uche Dim    schedule 25.10.2016
comment
Что такое edCardDate? - person KinGPinG; 08.02.2017
comment
@ user1186061 это то, что я называю mExpiryDate в своем реальном коде. Забыл изменить.. Я отредактировал код. Везде, где вы видите edCardDate, это должно быть mExpiryDate. Прости за это.. - person Uche Dim; 09.02.2017

Я использовал решение Uche Dim, исправил некоторые проблемы и почистил код.

Итак, ключевые улучшения в моем коде:

  1. если пользователь попытается ввести «13», будет введено только «1».
  2. когда пользователь начинает вводить год, после того, как он удалит косую черту, она будет добавлена, чтобы сохранить формат ММ/гг.

В целом это почти как поле истечения срока действия новых карточек в Play Store.

Я создал класс Kotlin, но использование также добавлено для Java.

Класс CardExpiryTextWatcher:

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))
        }
    }
}

Использование (Котлин):

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

Использование (Ява):

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

Предупреждение. Дата всегда будет состоять из двух цифр (например, 4 декабря будет 12 апреля, а не 12 апреля), но если пользователь удалит одну цифру из даты, она может стать 12 апреля, поэтому вам нужно запустить следующий метод перед проверкой:

/**
 * 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
}

Примечание. inputCardExpiry — это InputTextLayout, который содержит EditText.

person Sufian    schedule 26.06.2019
comment
это глючит, если я хочу удалить первое число из месяца удаления косой черты - person ghita; 11.10.2019
comment
@ghita, значит, вы удаляете первую цифру месяца (например, удаляете 1 из ввода 10/), и это решение удаляет косую черту (т. е. ввод изменяется на 10)? Если это так, то это ожидаемо. Если это что-то другое, пожалуйста, предоставьте больше информации, чтобы я мог изучить это, когда смогу. Спасибо - person Sufian; 11.10.2019
comment
спасибо Sufian за оперативный ответ. я использовал метод, который Uche Dim описан ниже. - person ghita; 11.10.2019
comment
Исправлена ​​ошибка, на которую ссылался ghita. - person Sufian; 06.12.2019

Может быть, вы можете сделать так:

boolean validateCardExpiryDate(String expiryDate) {
    return expiryDate.matches("(?:0[1-9]|1[0-2])/[0-9]{2}");
}

что переводится как:

группа без захвата ( группа без захвата? ) из: 0, за которым следуют 1-9, или 1, за которым следует 0-2, за которым следует «/», за которым следует 0-9, дважды. ... поэтому для этой версии требуются месяцы с нулевым дополнением (01–12). Добавить ? после первого 0, чтобы предотвратить это.

Надеюсь вам поможет..!!!

person Rushabh Patel    schedule 16.12.2013
comment
Это не работает. Я вызываю этот метод как validateCardExpiryDate(s.toString()); Он всегда возвращает ложь. - person user1051505; 16.12.2013
comment
Вы не прочитали его код и вопрос должным образом, потому что иначе вы бы увидели, в чем на самом деле его проблема. - person alex; 16.12.2013

TextWatchers используются для обновления внешнего свойства (например, в вашем ViewModel) при каждом изменении.

TextWatchers не следует использовать для изменения собственного текста EditText.

Для ввода форматирования вы должны использовать InputFilter вместо TextWatcher. Пожалуйста, попробуйте следующее:

Включите в свой проект следующий класс:

/**
 * InputFilter to ensure user enters valid expiry date in a credit card.
 * User is only allowed to type from beginning-to-end without copy-pasting or inserting characters in the middle.
 * The user may enter any month 01 -> 12.
 * The user can enter, at minimum, the current year or any year that follows.
 *
 * Note: `inputType` of the EditText should be `number` and `digits` should be `0123456789/`.
 *
 * Created by W.K.S on 30/07/2017 (Licensed under GNU Public License - original author must be credited)
 */

public class CreditCardExpiryInputFilter implements InputFilter {

    private final String currentYearLastTwoDigits;

    public CreditCardExpiryInputFilter() {
        currentYearLastTwoDigits = new SimpleDateFormat("yy", Locale.US).format(new Date());
    }

    public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
        //do not insert if length is already 5
        if (dest != null & dest.toString().length() == 5) return "";
        //do not insert more than 1 character at a time
        if (source.length() > 1) return "";
        //only allow character to be inserted at the end of the current text
        if (dest.length() > 0 && dstart != dest.length()) return "";

        //if backspace, skip
        if (source.length() == 0) {
            return source;
        }

        //At this point, `source` is a single character being inserted at `dstart`. 
        //`dstart` is at the end of the current text.

        final char inputChar = source.charAt(0);

        if (dstart == 0) {
            //first month digit
            if (inputChar > '1') return "";
        }
        if (dstart == 1) {
            //second month digit
            final char firstMonthChar = dest.charAt(0);
            if (firstMonthChar == '0' && inputChar == '0') return "";
            if (firstMonthChar == '1' && inputChar > '2') return "";

        }
        if (dstart == 2) {
            final char currYearFirstChar = currentYearLastTwoDigits.charAt(0);
            if (inputChar < currYearFirstChar) return "";
            return "/".concat(source.toString());
        }
        if (dstart == 4){
            final String inputYear = ""+dest.charAt(dest.length()-1)+source.toString();
            if (inputYear.compareTo(currentYearLastTwoDigits) < 0) return "";
        }

        return source;
    }
}

Примените CreditCardExpiryInputFilter к своему EditText:

EditText expiryEditText = findViewById(this, R.id.edittext_expiry_date);
expiryEditText.setFilters(new InputFilter[]{new CreditCardExpiryInputFilter()});

В xml установите inputType на number, а digits на 0123456789/:

<EditText
    android:id="@+id/edittext_expiry_date"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:inputType="number"
    android:digits="0123456789/"
    />
person W.K.S    schedule 30.07.2017
comment
эта штука не работает. Как только вводится второй символ, он возвращает - person Vivek Mishra; 22.10.2018
comment
Что ты пытался напечатать? - person W.K.S; 22.10.2018
comment
12 декабря месяц - person Vivek Mishra; 23.10.2018

Добавьте TextWatcher в свой EditText и выполните проверку с помощью 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("/"));
                    }
                }
            }
        });

Проверка с помощью 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
            }
person AbhinayMe    schedule 29.08.2020