0

I have an array of unique dates from each time the user completes a task. I want to check if the dates within the array are consecutive from and including todays date.

If the array contains dates: "2017/6/2, 2017/6/3, 2017/6/4, 2017/6/5" then based on today's date being 2017/6/5 the function would return 4 as there are 4 consecutive dates from and including today.

If the array contains dates "2017/6/2, 2017/6/3, 2017/6/4" then it would return 0 as the array does not include today's date. Otherwise the count would be broken upon a non consecutive date.

List<Date> dateList = new ArrayList<Date>();
int count = 0;
Date todayDate = new Date();

for (int i=0; i<dateList.size(); i++){
    // Check if dates within the array are consecutive from todayDate, if so then increment count by 1.
}
Ryan.H
  • 331
  • 4
  • 17
  • Is the dateList sorted? – Eric Jun 05 '17 at 15:23
  • [Calculate the difference between two dates](https://stackoverflow.com/questions/1555262/calculating-the-difference-between-two-java-date-instances) and iterate through the list, if false anywhere, contract is failed. – Compass Jun 05 '17 at 15:23
  • yes it is sorted and only contains unique dates. – Ryan.H Jun 05 '17 at 15:23
  • You can use Joda Time: 1. Sort the day in the list. 2. Find "today" in the list 3. Check from today index with function: `Days.daysBetween(indexDay.withTimeAtStartOfDay() , (index+n/-n)Day.withTimeAtStartOfDay() ).getDays() ` to see if it's 2 sequence days. – Kingfisher Phuoc Jun 05 '17 at 16:12
  • @KingfisherPhuoc JodaTime is being discontinued and replaced by the newer APIs. In [joda's site](http://www.joda.org/joda-time) it says **Note that Joda-Time is considered to be a largely “finished” project. No major enhancements are planned. If using Java SE 8, please migrate to java.time (JSR-310)**. If you use java >= 8, use the new `java.time` API, and for java <= 7 there's the [ThreeTen Backport](http://www.threeten.org/threetenbp/) –  Jun 05 '17 at 16:24

3 Answers3

2

If you're using Java 8, consider using the new java.time API. It's easier, less bugged and less error-prone than the old APIs.

If you're using Java <= 7, you can use the ThreeTen Backport, a great backport for Java 8's new date/time classes. And for Android, there's the ThreeTenABP (more on how to use it here).

Although you can also use JodaTime, it's being discontinued and replaced by the new APIs, do I don't recommend start a new project with joda. Even in joda's website it says: "Note that Joda-Time is considered to be a largely “finished” project. No major enhancements are planned. If using Java SE 8, please migrate to java.time (JSR-310).".


As you want to compare just the date (day/month/year), and not the time (hour/minute/second), the best choice is to use the LocalDate class. For java 8, this class is in java.time package, and in ThreeTen Backport, the package is org.threeten.bp. But the classes and methods names are the same.

The code would be like this:

public int count(List<LocalDate> dateList, LocalDate today) {
    if (!dateList.contains(today)) { // today is not in the list, return 0
        return 0;
    }

    int count = 0;
    LocalDate prev = dateList.get(0); // get first date from list
    for (int i = 1; i < dateList.size(); i++) {
        LocalDate next = dateList.get(i);
        if (prev.plusDays(1).equals(next)) {
            // difference between dates is one day
            count++;
        } else {
            // difference between dates is not 1
            // Do what? return 0? throw exception?
        }
        prev = next;
    }

    return count + 1; // didn't count the first element, adding 1
}

Testing this method:

List<LocalDate> dateList = new ArrayList<>();
dateList.add(LocalDate.of(2017, 6, 2));
dateList.add(LocalDate.of(2017, 6, 3));
dateList.add(LocalDate.of(2017, 6, 4));
dateList.add(LocalDate.of(2017, 6, 5));
LocalDate today = LocalDate.now();

System.out.println(count(dateList, today)); // 4

Another test (when today is not in the list)

List<LocalDate> dateList = new ArrayList<>();
dateList.add(LocalDate.of(2017, 6, 2));
dateList.add(LocalDate.of(2017, 6, 3));
dateList.add(LocalDate.of(2017, 6, 4));
LocalDate today = LocalDate.now();

System.out.println(count(dateList, today)); // 0

Notes:

  • As it wasn't specified what to do when the days are not consecutive (return 0 or throw exception), I left this part commented. But it should be straightforward to add this to the code
  • If you want to convert java.util.Date to LocalDate, you can do as follows (using the code of this answer, full explanation is in this link in case you have any questions):

    public LocalDate convert(Date date) {
        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
    }
    
    // if your Date has no toInstant method, try this:
    public LocalDate convert(Date date) {
        return Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()).toLocalDate();
    }
    
  • I understood that you want to check for consecutive days (so, a 1-day difference between the dates). But if you want to check if the previous date is before the next (no matter how many days), you can change the if (prev.plusDays(1).equals(next)) to if (prev.isBefore(next))

  • I'm not sure if that's the case, but if you want, you can also parse a String directly to a LocalDate (so you don't need to create lots of Date objects), using a DateTimeFormatter:

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/M/d");
    LocalDate d = LocalDate.parse("2017/6/2", formatter); // 2017-06-02
    
  • Hi, thanks for the detailed answer. I am using jdk1.8.0_91 yet I can't seem to import the LocalDate class nor java.time classes. Do you know for reason for this? – Ryan.H Jun 05 '17 at 16:35
  • 1
    @Ryan.H - You've tagged the question for Android. While you may be using JDK 1.8, Android doesn't include the `java.time` classes (as well as many other parts of the Java API). Android has its own API, which is primarily based on Java 1.7. – Ted Hopp Jun 05 '17 at 16:52
  • I missed that little detail. I've edited the answer and included information for Android's ThreeTenABP (so you can use the ThreeTen Backport). @TedHopp, thanks for reminding me! –  Jun 05 '17 at 16:56
  • @Ryan.H I've update the answer with more info about the missing classes (and how to use the ThreeTen Backport in Android) and some other minor details, hope it helps. –  Jun 05 '17 at 17:09
  • Thanks for the clarification both. I have added the backport but it is not recognising the toInstant() method. Trying to find out the reason for this! – Ryan.H Jun 05 '17 at 17:11
  • @Ryan.H I've updated the answer (instead of `date.toInstant()`, you can use `Instant.ofEpochMilli(date.getTime())` - a little longer, but equivalent. –  Jun 05 '17 at 17:15
  • Now I realized that `Date.toInstant()` method was introduced in Java 8, together with `java.time` classes. So if one is not available, the other won't be either. –  Jun 05 '17 at 17:24
  • 1
    Yep I came to the same conclusion as well! Thanks for your help, really good answer. One thing that needs adding is the AndroidThreeTen.init(this); call within onCreate for it all to work :) – Ryan.H Jun 05 '17 at 17:40
  • 1
    In the backport there’s a `DateTimeUtils` class with the conversions to and from the legacy classes. I think you’re after [`DateTimeUtils.toInstant(Date)`](http://www.threeten.org/threetenbp/apidocs/org/threeten/bp/DateTimeUtils.html#toInstant-java.util.Date-). – Ole V.V. Jun 05 '17 at 17:46
1

There are a lot of ways to write it more clear:

  1. Use new Date API;
  2. Use libraries;

But, in such case, with usage of old Date classes, I would do that in such a way:

public static void main(String[] args) {
    long millisInDay = TimeUnit.DAYS.toMillis(1);
    List<Date> dates = Arrays.asList(new Date("2017/6/2"), new Date("2017/6/3"), new Date("2017/6/4"), new Date("2017/6/5"));
    System.out.println(getSequentialNumber(millisInDay, dates));
}

private static int getSequentialNumber(long millisInDay, List<Date> dates) {
    int count = 0;
    Date now = setMidnight(Calendar.getInstance().getTime());
    for (int i = dates.size() - 1; i >= 0; i--) {
        Date date = setMidnight(dates.get(i));
        if (date.getTime() == now.getTime()) {
            count++;
        }
        now.setTime(now.getTime() - millisInDay);
    }
    return count;
}

private static Date setMidnight(Date date) {
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(date);
    calendar.set(Calendar.MINUTE, 0);
    calendar.set(Calendar.MILLISECOND, 0);
    calendar.set(Calendar.HOUR, 0);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.HOUR_OF_DAY, 0);
    return calendar.getTime();
}
dvelopp
  • 3,535
  • 2
  • 26
  • 46
1

If I understand the requirement correctly, you have an array of Date objects, ordered by date, and guaranteed not to have two Date objects for the same day, but possibly with gaps between the days. Your goal is to return the length of the maximum sub-array that contains only consecutive days and also includes the current day, or to return 0 if there is no such sub-array. The current day may fall anywhere inside that sub-array, not necessarily at the beginning or end.

It's not clear if you need to support crossing year boundaries, but I'll assume so. I also assume that all the Date objects in the list are for the same time zone which is also the time zone for the device on which you are running. If that's not the case, you should refer to this answer for more information on testing whether two Date objects refer to the same day.

It's fairly simple to do this if you work with Calendar objects instead of Date objects. You don't need any third-party libraries, as both Date and Calendar are parts of the standard Android API. I suggest doing this in two phases: first search for the current date in the array and then scan in both directions for either a gap in the dates or an array boundary. Then just count how far you could go in each direction.

public int getDateSpanCount(List<Date> dateList) {
    final int n = dateList.size();
    final Calendar today = Calendar.getInstance();
    final Calendar other = Calendar.getInstance();
    int count = 0;

    // First search for today in the date array
    int posToday = -1;
    for (int i=0; i<n; i++) {
        other.setTime(dateList.get(i));
        if (areSameDay(today, other)) {
            posToday = i;
            break;
        }
    }

    // If today is in the list, count the size of the sub-array containing today
    if (posToday >= 0) {
        count++; // count today, at least
        final Calendar probe = Calendar.getInstance();

        // scan backwards from position of today's date
        for (int prevPos = posToday - 1; prevPos >= 0; prevPos--) {
            final Date prev = dateList.get(prevPos);
            probe.setTime(prev);
            other.add(Calendar.DAY_OF_YEAR, -1);
            if (areSameDay(probe, other)) {
                count++;
                other.setTime(prev);
            } else {
                break;
            }
        }

        // reset the other time
        other.setTime(today.getTime());

        // scan forward from position of today's date
        for (int nextPos = posToday + 1; nextPos < n; nextPos++) {
            final Date next = dateList.get(nextPos);
            probe.setTime(next);
            other.add(Calendar.DAY_OF_YEAR, 1);
            if (areSameDay(probe, other)) {
                count++;
                other.setTime(next);
            } else {
                break;
            }
        }
    }
    return count;
}

/** Test whether two Calendar objects are set to the same day */
private static boolean areSameDay(Calendar c1, Calendar c2) {
    // see discussion above if dates may not all be for the local time zone
    return c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR) &&
           c1.get(Calendar.DAY_OF_YEAR) == c2.get(Calendar.DAY_OF_YEAR);
}
Ted Hopp
  • 222,293
  • 47
  • 371
  • 489
  • 1
    Thanks for the response. Really interesting studying your approach to the problem! Works like a charm. This answer not needing dependencies is obviously preferable. In the areSameDay method i think the cal1 and cal2 are meant to be c1 and c2 if I'm not mistaken! – Ryan.H Jun 05 '17 at 18:20
  • @Ryan.H - Yes, `cal1` and `cal2` were wrong. Thanks for catching that. – Ted Hopp Jun 05 '17 at 19:50
  • 1
    While I certainly think twice before adding a dependency on a third-party library, probably also three times, ThreeTenABP seems rather futureproof: `java.time`/JSR-310 is bound to come to Android sooner or later. – Ole V.V. Jun 05 '17 at 20:15
  • 2
    @OleV.V. - It appears you are right. The `java.time` package appears in the [API docs for Android O preview](https://developer.android.com/reference/java/time/package-summary.html). It's also quite possible that Google will eventually come out with a compatibility library to back-port `java.time` support for previous API levels. – Ted Hopp Jun 05 '17 at 20:43