2

For the expiration date of credit/debit cards, I am using an array that contains all years since 2020 to 2030:

String[] expirationYearArray = { "2020", "2021", "2022", "2023", "2024", "2025", "2026", "2027", "2028", "2029", "2030" };

That is a bad approach because years are hard-coded. That means that after 3 years, in 2023, users will still have the option to choose 2020, 2021 and 2022 as the expiration date of credit cards. That is clearly wrong. What I want to do is to fill the array with Strings from current year to 10 more years from the current year. What I am thinking to do is to use a Java built-in function to get the current year. Then use a for loop to make 10 iterations and in each iteration convert from Int to String so that I convert 2020 to "2020" and then push that "2020" as the first element of the expirationYearArray array. The for or while loop would continue to do that until I reach the tenth iteration. Does this approach make sense to you? Please let me know if you see a different or more optimal option that may do the same with less code or if it makes sense to you the way I am envisioning it. Thank you.

Jaime Montoya
  • 6,915
  • 14
  • 67
  • 103
  • 2
    be careful when current time around new year, which means that some time zones in next year, while others are in previous – Iłya Bursov Mar 10 '20 at 18:51
  • @IłyaBursov That is a really good point I had not thought about! Thank you! – Jaime Montoya Mar 10 '20 at 18:52
  • So, what you want to do is basically a list that is updated automatically? Sorry, your whole question is hard to comprehend. – akuzminykh Mar 10 '20 at 18:52
  • @akuzminykh Yes, pretty much a list that is updated automatically. I was thinking about an array to end up having this in the array: `String[] expirationYearArray = { "2020", "2021", "2022", "2023", "2024", "2025", "2026", "2027", "2028", "2029", "2030" };`. But I need something that automatically updates, not hard-coded. – Jaime Montoya Mar 10 '20 at 18:59
  • @IłyaBursov On a second thought, do you think time zones is a problem? This is for an Android app, so if in the Java code I use a function that gets the current year, that will be based on the phone/device, not on the server, so I do not think time zones should be a problem. Do you agree? – Jaime Montoya Mar 10 '20 at 19:01
  • 1
    @JaimeMontoya in general - it is not a big deal, there are not so many users who will use card with expiration date 12/Current year during the time when some time zones in next year already. But, you can take your phone and try to use it in another country, without changing time zone, so your phone's timezone will be different from local one – Iłya Bursov Mar 10 '20 at 19:08

4 Answers4

2

tl;dr

In 2 lines, using streams and lambda syntax.

Year currentYear = Year.now( ZoneId.of( "Pacific/Auckland" ) );  // Determining the current year requires a time zone when near the ending/beginning of the year.
List < Year > years =                                            // A list of objects of the class `Year`. Better to use a specific type than use a mere integer number.
        IntStream                                                // Generates a stream of `int` primitive numbers.
        .range(                                                  // Get numbers from the specified beginning to the specified ending.
            currentYear.getValue() ,                             // Get `int` number of the current year.
            currentYear.plusYears( 10 ).getValue()               // Yet the `int` number of a later year.
        )                                                        // Returns a `IntStream` object.
        .boxed()                                                 // Converts the `int` primitive values into a stream of `Integer` objects.
        .map( integer -> Year.of( integer ) )                    // Uses each `Integer` object to instantiate a `Year` object. The `Integer` object is auto-boxed back into an `int` primitive`. Primitives cannot be mapped in a stream with a lambda. So we had to do this funky `int` -> `Integer` -> `int` conversion juggling.
        .collect( Collectors.toList() )                          // Gather the `Year` objects produced by the `map` method into a `List` collection.
;

In a few lines, using conventional syntax.

Year currentYear = Year.now( ZoneId.of( "Asia/Tokyo" ) ) ;
List< Year > years = new ArrayList<>( 10 ) ;
for( int i = 0 ; i < 10 ; i ++ ) 
{
    years.add( currentYear.plusYears( i ) ) ;
}

See this code run live at IdeOne.com.

years.toString(): [2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029]

java.time.Year

Java offers a specific class to represent a year, appropriately named Year. I suggest using that class rather than a mere integer to make your code type-safe and more self-documenting.

Time zone is crucial

Getting the current year requires a time zone. For any given moment, the date various around the world by time zone. While "tomorrow" in Tokyo Japan, it is simultaneously "yesterday" in Montréal Québec.

Therefore, around the last day / first day of the year, it may be "next year" in Tokyo Japan while simultaneously "last year" in Montréal Québec. So when determining the current year, specify the desired/expected time zone.

ZoneId z = ZoneId.of( "America/Montreal" ) ;
Year currentYear = Year.now( z ) ;

Get your next ten years by iterating.

List< Year > years = new ArrayList<>( 10 ) ;
Year year = currentYear ;
for( int i = 0 ; i < 10 ; i ++ ) 
{
    // Remember this year.
    years.add( year ) ;
    // Set up the next loop.
    year = currentYear.plusYears( i ) ;
}

Make an unmodifiable list from that ArrayList by calling List.copyOf.

List< Year > years = List.copyOf( years ) ;  // Returns an unmodifiable `List`, with adding/removing elements disallowed.

java.time.Month

For expiration year-month, you also need the a widget for picking month. There you can use the Month class.

List< Month > months = List.copyOf( Month.values() ) 

The toString method of Month gives you the name of the month in English in all uppercase, based on the name of enum element. You might want to make a list of localized names for display to users.

List< String > monthsLocalized = new ArrayList<>( months.size() ) ;
Locale locale = Locale.CANADA_FRENCH ;
for( Month month : months ) 
{
    monthsLocalized.add( month.getDisplayName( TextStyle.FULL , locale ) ;
}
monthsLocalized = List.copyOf( monthsLocalized ) ;

java.time.YearMonth

For your final result of a user picking a year and a month, use YearMonth. Oddly, the YearMonth factory methods do not take a Year object, only a mere integer. So call Year::getValue to get the number.

YearMonth yearMonth = YearMonth.of( selectedYear.getValue() , selectedMonth ) ;

Premature Optimization

You expressed concern about optimizing for performance. This is not the kind of code you need to optimize. Indeed, do not fall into the trap of premature optimization. Even the most experienced programmers have been shown to be notoriously bad at intuitively guessing where the bottlenecks live in their codebase. Optimize only after having empirically proven a significant slowdown that materially affects the performance of your code and the running of the business. Most often, writing simple straightforward short lines of Java code will result in highly optimized executing code after the Java compiler and runtime optimizer (HotSpot, OpenJ9) do their job.

About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.

Where to obtain the java.time classes?

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • Thank you Basil. Concerning my comment about "more optimal option that may do the same with less code", I was not thinking about performance in terms of computing power and resources. I was more thinking about writing shorter code that is more readable. Your version is incredibly short. Thank you. – Jaime Montoya Mar 10 '20 at 20:21
  • Important to mention that `java.time.ZoneId.of()` requires `API level 26`. I tried it with `API level min 23` and it did not work. – Jaime Montoya Mar 10 '20 at 20:35
  • 1
    @JaimeMontoya This Question is not tagged as *Android*. And fyi, for earlier Android, see the *ThreeTen-Backport* project further adapted to Android in the *ThreeTenABP* project. You get a library that provides most of the *java.time* functionality in virtually the same API. – Basil Bourque Mar 10 '20 at 20:36
2

It's a good idea to avoid hard-coding the list of years.

As for the range of years you generate, you might check with your card processor for the rules. For example, expired cards can be charged in some cases when they were setup for a recurring charge, like a subscription, but they probably shouldn't be accepted for new plans. Likewise, credit cards typically expire sooner than 10 years; your processor may reject cards with an unrealistically long expiration.

Again, processors may not be consistent in the enforcement of time zones. Some might consider a year to end when it ends in their local time zone, others might use UTC. The most lenient policy would be to use the "anywhere on earth" zone, with an offset of -12:00 from UTC.

To make your logic testable, pass it a Clock instance. Typically, the clock would be the system default clock, but in this case, you might set the zone to be "anywhere in the world". By passing a clock, you can remove the dependency on the actual time, and test how your calculation will behave at any time.

private static final ZoneOffset aoe = ZoneOffset.ofHours(-12);

private static final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("uuuu");

public static void main(String[] args) {
    Clock clock = Clock.system(aoe);
    List<Year> years = nextYears(clock, 10L);
    years.stream().map(fmt::format).forEach(System.out::println);
}

private List<Year> nextYears(Clock clock, long count) {
    return Stream.iterate(Year.now(clock), year -> year.plusYears(1L))
        .limit(count)
        .collect(Collectors.toList());
}
erickson
  • 265,237
  • 58
  • 395
  • 493
1

Here are several variants:

  public static void main(String[] args) {

    System.out.println(Arrays.toString(getExpirationYears(LocalDateTime.now().getYear())));
    System.out.println(Arrays.toString(getExpirationYears(Calendar.getInstance().get(Calendar.YEAR))));
    System.out.println(Arrays.toString(getExpirationYears(1900 + new Date().getYear())));
    System.out.println(Arrays.toString(getExpirationYearByStreams(1900 + new Date().getYear())));
}

static String[] getExpirationYears(int year) {
    String[] expirationYearArray = new String[10];
    for (int i = 0; i < expirationYearArray.length; i++) {
        expirationYearArray[i] = String.valueOf(year + i);
    }
    return expirationYearArray;
}


static String[] getExpirationYearByStreams(int year) {
   return IntStream.range(year, year+10)
            .boxed()
            .map(String::valueOf)
            .toArray(String[]::new);

}
Nonika
  • 2,490
  • 13
  • 15
  • Don’t use `Date`, it’s poorly designed and long outdated. And even if you insist, don’t use its `getYear` method, it’s been deprecated for more than 20 years because it works unreliably across time zones. – Ole V.V. Mar 10 '20 at 20:14
  • 1
    @OleV.V. I know and personally never use Date class. But I didn't knew api level of OP-s android project. – Nonika Mar 10 '20 at 20:22
0

This is how I achieved it in my code, following Nonika's advice (see his answer):

package com.myapp;
..........
public class Payment extends BaseActivity implements ResponseListener {
    ..........
    int currentYear = Calendar.getInstance().get(Calendar.YEAR);
    String[] cardYearArray = new String[10];
    ..........
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ..........
        cardYearArray=getExpirationYears(currentYear);
        SpinnerAdapter creditCardYearAdapter = new SpinnerAdapter(Payment.this,
            cardYearArray);
        ..........
    }
    static String[] getExpirationYears(int year) {
        String[] expirationYearArray = new String[10];
        for (int i = 0; i < expirationYearArray.length; i++) {
            expirationYearArray[i] = String.valueOf(year + i);
        }
        return expirationYearArray;
    }
    ..........
}

Now I see the years appearing correctly as I needed it:

enter image description here

Jaime Montoya
  • 6,915
  • 14
  • 67
  • 103