private void updateInputValues(@NonNull @Size(2) String[] parts) { int inputMonth; int inputYear; if (parts[0].length() != 2) { inputMonth = INVALID_INPUT; } else { try { inputMonth = Integer.parseInt(parts[0]); } catch (NumberFormatException numEx) { inputMonth = INVALID_INPUT; } } if (parts[1].length() != 2) { inputYear = INVALID_INPUT; } else { try { inputYear = DateUtils.convertTwoDigitYearToFour(Integer.parseInt(parts[1])); } catch (NumberFormatException numEx) { inputYear = INVALID_INPUT; } } mIsDateValid = DateUtils.isExpiryDataValid(inputMonth, inputYear); }
parts = DateUtils.separateDateStringParts(rawNumericInput); if (!DateUtils.isValidMonth(parts[0])) { inErrorState = true;
/** * Gets the expiry date displayed on this control if it is valid, or {@code null} if it is not. * The return value is given as a length-2 {@code int} array, where the first entry is the * two-digit month (from 01-12) and the second entry is the four-digit year (2017, not 17). * * @return an {@code int} array of the form {month, year} if the date is valid, or {@code null} * if it is not */ @Nullable @Size(2) public int[] getValidDateFields() { if (!mIsDateValid) { return null; } final int[] monthYearPair = new int[2]; final String rawNumericInput = getText().toString().replaceAll("/", ""); final String[] dateFields = DateUtils.separateDateStringParts(rawNumericInput); try { monthYearPair[0] = Integer.parseInt(dateFields[0]); final int twoDigitYear = Integer.parseInt(dateFields[1]); final int fourDigitYear = DateUtils.convertTwoDigitYearToFour(twoDigitYear); monthYearPair[1] = fourDigitYear; } catch (NumberFormatException numEx) { // Given that the date should already be valid when getting to this method, we should // not his this exception. Returning null to indicate error if we do. return null; } return monthYearPair; }
/** * Checks whether or not the input month and year has yet expired. * * @param expiryMonth An integer representing a month. Only values 1-12 are valid, * but this is called by user input, so we have to check outside that range. * @param expiryYear An integer representing the full year (2017, not 17). Only positive values * are valid, but this is called by user input, so we have to check outside * for otherwise nonsensical dates. This code cannot validate years greater * than {@link #MAX_VALID_YEAR 9980} because of how we parse years in * {@link #convertTwoDigitYearToFour(int, Calendar)}. * @return {@code true} if the current month and year is the same as or later than the input * month and year, {@code false} otherwise. Note that some cards expire on the first of the * month, but we don't validate that here. */ static boolean isExpiryDataValid(int expiryMonth, int expiryYear) { return isExpiryDataValid(expiryMonth, expiryYear, Calendar.getInstance()); }
/** * Converts a two-digit input year to a four-digit year. As the current calendar year * approaches a century, we assume small values to mean the next century. For instance, if * the current year is 2090, and the input value is "18", the user probably means 2118, * not 2018. However, in 2017, the input "18" probably means 2018. This code should be * updated before the year 9981. * * @param inputYear a two-digit integer, between 0 and 99, inclusive * @return a four-digit year */ @IntRange(from = 1000, to = 9999) static int convertTwoDigitYearToFour(@IntRange(from = 0, to = 99) int inputYear) { return convertTwoDigitYearToFour(inputYear, Calendar.getInstance()); }
@Override public void afterTextChanged(Editable s) { // Note: we want to show an error state if the month is invalid or the // final, complete date is in the past. We don't want to show an error state for // incomplete entries. boolean shouldShowError = false; if (parts[0].length() == 2 && !DateUtils.isValidMonth(parts[0])) { // This covers the case where the user has entered a month of 15, for instance. shouldShowError = true; } // Note that we have to check the parts array because afterTextChanged has odd // behavior when it comes to pasting, where a paste of "1212" triggers this // function for the strings "12/12" (what it actually becomes) and "1212", // so we might not be properly catching an error state. if (parts[0].length() == 2 && parts[1].length() == 2) { boolean wasComplete = mIsDateValid; updateInputValues(parts); // Here, we have a complete date, so if we've made an invalid one, we want // to show an error. shouldShowError = !mIsDateValid; if (!wasComplete && mIsDateValid && mExpiryDateEditListener != null) { mExpiryDateEditListener.onExpiryDateComplete(); } } else { mIsDateValid = false; } setShouldShowError(shouldShowError); } });
/** * Set the expiration date. Method invokes completion listener and changes focus * to the CVC field if a valid date is entered. * * Note that while a four-digit and two-digit year will both work, information * beyond the tens digit of a year will be truncated. Logic elsewhere in the SDK * makes assumptions about what century is implied by various two-digit years, and * will override any information provided here. * * @param month a month of the year, represented as a number between 1 and 12 * @param year a year number, either in two-digit form or four-digit form */ public void setExpiryDate( @IntRange(from = 1, to = 12) int month, @IntRange(from = 0, to = 9999) int year) { mExpiryDateEditText.setText(DateUtils.createDateStringFromIntegerInput(month, year)); }
@Test public void separateDateStringParts_withEmptyInput_returnsNonNullEmptyOutput() { String[] parts = DateUtils.separateDateStringParts(""); String[] expected = {"", ""}; assertArrayEquals(expected, parts); }
@Test public void isExpiryDataValid_whenDateIsSameCalendarYearAndMonth_returnsTrue() { Calendar testCalendar = Calendar.getInstance(); testCalendar.set(Calendar.YEAR, 2018); testCalendar.set(Calendar.MONTH, Calendar.JANUARY); assertTrue(DateUtils.isExpiryDataValid(1, 2018, testCalendar)); }
@Test public void convertTwoDigitYearToFour_whenDateIsEarlyCenturyAndYearIsLarge_addsLowerBase() { Calendar earlyCenturyCalendar = Calendar.getInstance(); earlyCenturyCalendar.set(Calendar.YEAR, 2502); // In the year 2502, when you say "95", you probably mean 2495. assertEquals(DateUtils.convertTwoDigitYearToFour(95, earlyCenturyCalendar), 2495); // A more practical test earlyCenturyCalendar.set(Calendar.YEAR, 2017); assertEquals(DateUtils.convertTwoDigitYearToFour(99, earlyCenturyCalendar), 1999); }
@Test public void isValidMonth_forNonNumericInput_returnsFalse() { assertFalse(DateUtils.isValidMonth(" ")); assertFalse(DateUtils.isValidMonth("abc")); // This is looking for a valid numeric month, not month names. assertFalse(DateUtils.isValidMonth("January")); assertFalse(DateUtils.isValidMonth("\n")); } }
@Test public void createDateStringFromIntegerInput_whenDateHasThreeDigitYear_returnsEmpty() { assertEquals("", DateUtils.createDateStringFromIntegerInput(12, 101)); }
@Test public void separateDateStringParts_withValidDate_properlySeparatesString() { String[] parts = DateUtils.separateDateStringParts("1234"); String[] expected = {"12", "34"}; assertArrayEquals(expected, parts); }
@Test public void isExpiryDataValid_whenDateIsAfterCalendarYear_returnsTrue() { Calendar testCalendar = Calendar.getInstance(); testCalendar.set(Calendar.YEAR, 2018); testCalendar.set(Calendar.MONTH, Calendar.JANUARY); assertTrue(DateUtils.isExpiryDataValid(1, 2019, testCalendar)); }
@Test public void convertTwoDigitYearToFour_whenDateIsNearCenturyButYearIsSmall_addsIncreasedBase() { Calendar lateCenturyCalendar = Calendar.getInstance(); lateCenturyCalendar.set(Calendar.YEAR, 2081); assertEquals(DateUtils.convertTwoDigitYearToFour(8, lateCenturyCalendar), 2108); }
@Test public void isValidMonth_forInvalidNumericInput_returnsFalse() { assertFalse(DateUtils.isValidMonth("15")); assertFalse(DateUtils.isValidMonth("0")); assertFalse(DateUtils.isValidMonth("-08")); }
@Test public void createDateStringFromIntegerInput_whenDateHasOneDigitMonthAndYear_addsZero() { assertEquals("0102", DateUtils.createDateStringFromIntegerInput(1, 2)); }
@Test public void separateDateStringParts_withPartialDate_properlySeparatesString() { String[] parts = DateUtils.separateDateStringParts("123"); String[] expected = {"12", "3"}; assertArrayEquals(expected, parts); }
@Test public void isExpiryDataValid_whenDateIsSameCalendarYearButLaterMonth_returnsTrue() { Calendar testCalendar = Calendar.getInstance(); testCalendar.set(Calendar.YEAR, 2018); testCalendar.set(Calendar.MONTH, Calendar.JANUARY); assertTrue(DateUtils.isExpiryDataValid(2, 2018, testCalendar)); }
@Test public void convertTwoDigitYearToFour_whenDateIsNearCenturyAndYearIsLarge_addsNormalBase() { Calendar lateCenturyCalendar = Calendar.getInstance(); lateCenturyCalendar.set(Calendar.YEAR, 2088); assertEquals(DateUtils.convertTwoDigitYearToFour(95, lateCenturyCalendar), 2095); }