7

Is there an elegant way to find the nearest day of the week for a given date using JodaTime? I initially thought setCopy() would be it, but this sets the day to the particular day in the same week. Thus, if ld is 2011-11-27 and day is "Monday" the following function returns 2011-11-21, and not 2011-11-28 as I want.

    // Note that "day" can be _any_ day of the week, not just weekdays.
    LocalDate getNearestDayOfWeek(LocalDate ld, String day) {
        return ld.dayOfWeek().setCopy(day);
    }

Desired output for various inputs:

2011-12-04, Monday    => 2011-12-05
2011-12-04, Tuesday   => 2011-12-06
2011-12-04, Wednesday => 2011-12-07
2011-12-04, Thursday  => 2011-12-01
2011-12-04, Friday    => 2011-12-02
2011-12-04, Saturday  => 2011-12-03
2011-12-04, Sunday    => 2011-12-04

2011-12-05, Monday    => 2011-12-05
2011-12-05, Tuesday   => 2011-12-06
2011-12-05, Wednesday => 2011-12-07
2011-12-05, Thursday  => 2011-12-08
2011-12-05, Friday    => 2011-12-02
2011-12-05, Saturday  => 2011-12-03
2011-12-05, Sunday    => 2011-12-04

Below is a work-around I came up with that works for the particular constraints in my current situation, but I'd love to get help find a completely generic solution that works always.

    LocalDate getNearestDayOfWeek(LocalDate ld, String day) {
        LocalDate target = ld.dayOfWeek().setCopy(day);
        if (ld.getDayOfWeek() > DateTimeConstants.SATURDAY) {
            target = target.plusWeeks(1);
        }
        return target;
    }
Stig Brautaset
  • 2,602
  • 1
  • 22
  • 39
  • possible duplicate of [Joda-Time: How to get the next friday?](http://stackoverflow.com/questions/1636038/joda-time-how-to-get-the-next-friday) –  Nov 30 '11 at 06:09
  • 2
    I don't think so. That finds the _next_ day, but I'd like to find the _nearest_ day. – Stig Brautaset Nov 30 '11 at 07:16
  • You need to define what you mean by "nearest" and "weekday" to be able to provide the right answer. Perhaps a table of expected input to output? – JodaStephen Nov 30 '11 at 11:20
  • I'd love to, but that begs the question: How do I do a table in SO's Markdown? I could shell out to HTML, but I don't see the table tag supported. I suppose I'd have to use pre-formatted text? – Stig Brautaset Dec 05 '11 at 08:23
  • Added a table of desired results. – Stig Brautaset Dec 05 '11 at 14:58

6 Answers6

5

In Jodatime, this kind of thing should be doable with three or four lines:

   /** Given a reference LocalDate and a day of week, eg DateTimeConstants.MONDAY 
       Returns the nearest date with that day of week */
   public static LocalDate getNearestDayOfWeek(LocalDate t0,int dow) {
        LocalDate t1 = t0.withDayOfWeek(dow);
        LocalDate t2 = t1.isBefore(t0) ? t1.plusWeeks(1) : t1.minusWeeks(1);
        return  Math.abs(Days.daysBetween(t1, t0).getDays()) < 
                Math.abs(Days.daysBetween(t2, t0).getDays()) ? t1 : t2;
   }

Or more compact and efficient:

public static LocalDate getNearestDayOfWeek(LocalDate t0, int dow) {
    LocalDate t1 = t0.withDayOfWeek(dow);
    if (t1.isBefore(t0.minusDays(3)))       return t1.plusWeeks(1);
    else if (t1.isAfter(t0.plusDays(3)))    return t1.minusWeeks(1);
    else return t1;
}

And if you want to pass the day-of-the-week as String:

public static LocalDate getNearestDayOfWeek(LocalDate t0, String dow) {
    return getNearestDayOfWeek(t0,t0.dayOfWeek().setCopy(dow).getDayOfWeek());
}

Example:

    // prints 2011-11-28
   public static  void  main(String[] args) throws Exception {
        LocalDate today = new LocalDate(2011,11,27);
        int dow = DateTimeConstants.MONDAY;
        System.out.println(getNearestDayOfWeek(today ,dow ));
   }
leonbloy
  • 73,180
  • 20
  • 142
  • 190
  • This is good. Strings makes sense in my particular situation. (I was dealing with name of day strings from a config file.) I agree that it's not the best in the general case. – Stig Brautaset Dec 05 '11 at 15:03
  • Posted below is a simple Java 8 approach using TemporalAdjuster, which requires no external libraries. – Vanja D. May 09 '21 at 03:32
2

Something like this. For the dayOfWeek parameter, use the constants defined in org.joda.time.DateTimeConstants:

public LocalDate getNext(int dayOfWeek) {
    LocalDate today = new LocalDate();
    return getNext(dateOfWeek, today);
}

public LocalDate getNext(int dayOfWeek, LocalDate fromDate) {
    int dayOffset = DateTimeConstants.DAYS_PER_WEEK - dayOfWeek + 1;
    LocalDate weekContainingDay = fromDate.plusDays(dayOffset);

    return weekContainingDay.withDayOfWeek(dayOfWeek);
}

Usage:

LocalDate nextSunday = foo.getNext(DateTimeConstants.SUNDAY);
2

There's a good API in Java 8 for this purpose called TemporalAdjuster:

    LocalDate today = LocalDate.now();
    TemporalAdjuster adjustToNextWed = TemporalAdjusters.next(DayOfWeek.WEDNESDAY);
    TemporalAdjuster adjustToNexOrSametWed = TemporalAdjusters.nextOrSame(DayOfWeek.WEDNESDAY);

    LocalDate nextWed = today.with(adjustToNextWed);
    LocalDate nextWedOrToday = today.with(adjustToNextOrSameWed);

The java.time.temporal.TemporalAdjusters class contains factories some common use-cases:

firstDayOfMonth, lastDayOfMonth, firstDayOfNextMonth, firstDayOfYear, lastDayOfYear, firstDayOfNextYear, firstInMonth, lastInMonth, dayOfWeekInMonth, next, nextOrSame, previous, previousOrSame

As you can see, there is no factory for "nearest", but you can create your own. After checking the source of these methods, making your own is quite straightforward:

    // This implementation is expanded and verbose for clarity,
    // see simplified version below
    public static TemporalAdjuster nearest(DayOfWeek dayOfWeek) {
        int targetDay = dayOfWeek.getValue(); // range: +1..+7
        return (temporal) -> {
            int originalDay = temporal.get(DAY_OF_WEEK); // range: +1..+7
            // difference between the target (1..7) and original (1..7) weekdays
            int adjustDays = targetDay - originalDay; // range: -6..+6
            if (adjustDays <= -4) {
                // if the adjustment is 4 or more days ago,
                // next week is closer:
                adjustDays += 7;
            }
            if (adjustDays >= 4) {
                // if the adjustment is 4 or more days in future,
                // previous week is closer:
                adjustDays -= 7;
            }
            return temporal.plus(adjustDays, DAYS);
        };
    }

Using this is then quite straightforward:

LocalDate date = LocalDate.now(); // some date to adjust, from your code
date.with(nearest(DayOfWeek.TUESDAY)); // the nearest Tuesday from the date

It can be simplified to a single expression to convert the range of -6 to +6 to -3..+3:

    public static TemporalAdjuster nearest(DayOfWeek dayOfWeek) {
        int targetDay = dayOfWeek.getValue();
        return (temporal) -> {
            int originalDay = temporal.get(DAY_OF_WEEK);
            final int adjustDays = ((targetDay - originalDay + 10)  % 7) - 3;
            return temporal.plus(adjustDays, DAYS);
        };
    }

Full example:

Here's a full implementation with tests for all combinations of days. You can run the program to verify results, yielding output like:

...
original day: MONDAY, target day THURSDAY, difference: -3
  nearest THURSDAY to MONDAY 2021-05-03 is 2021-05-06

original day: MONDAY, target day FRIDAY, difference: -4
  nearest FRIDAY to MONDAY 2021-05-03 is 2021-04-30
...
original day: MONDAY, target day FRIDAY, difference: 4
  nearest FRIDAY to MONDAY 2021-05-03 is 2021-04-30
....

Solution:

// file TestNearest.java
class TestNearest {

    public static LocalDate nearestDayOfWeek(LocalDate originalDate, DayOfWeek dayOfWeek) {
        return originalDate.with(nearest(dayOfWeek));
    }

    public static TemporalAdjuster nearest(DayOfWeek dayOfWeek) {
        int targetDay = dayOfWeek.getValue();
        return (temporal) -> {
            int originalDay = temporal.get(ChronoField.DAY_OF_WEEK);
            final int adjustment = ((targetDay - originalDay + 10)  % 7) - 3;
            return temporal.plus(adjustment, ChronoUnit.DAYS);
        };
    }

    public static void main(String[] args) {
        for (int original = 1; original <= 7; original++) {
            for (int target = 1; target <= 7; target++) {
                final DayOfWeek originalDayOfWeek = DayOfWeek.of(original);
                final DayOfWeek targetDayOfWeek = DayOfWeek.of(target);
                // Create a test date:
                final LocalDate testOriginalDate = LocalDate.now()
                        .with(TemporalAdjusters.dayOfWeekInMonth(1, originalDayOfWeek));
                final LocalDate nearestDate = nearestDayOfWeek(testOriginalDate, targetDayOfWeek);
                debug(testOriginalDate, targetDayOfWeek, nearestDate);
            }
        }
    }

    private static void debug(LocalDate original, DayOfWeek target, LocalDate result) {
        System.out.println("original day: " + original.getDayOfWeek() +
                ", target day " + target +
                ", difference: " + (target.getValue() - original.getDayOfWeek().getValue()));
        System.out.println("  nearest " + (result.getDayOfWeek()) + " to " +
                (original.getDayOfWeek()) + " " +
                original +
                " is " +
                result);
        System.out.println();
    }
}
Vanja D.
  • 834
  • 13
  • 20
2

Here is an approach to the problem. I know a little bit about JodaTime, but not all the classes and methods. I'm assuming that given a date, you can get the day of the week and the next or previous dates.

There are three cases.

  1. The dayOfTheWeek for a particular date is a weekday. Return date.
  2. The dayOfTheWeekis Saturday. Subtract 1 day from your date. Return the date - 1 day.
  3. The dayOfTheWeekis Sunday. Add 1 day to your date. Return the date + one day.

If dayOfTheWeek is an enumerated type, then a case statement would handle the task in a straight forward manner.

Richard Povinelli
  • 1,419
  • 1
  • 14
  • 28
  • I think you misunderstood my question. Your algorithm seems to find the nearest weekday, which is not what I want. Rather, for any given date I want to be able to ask "what's the nearest Monday? Tuesday? Saturday?" I edited my question to make this clear. – Stig Brautaset Nov 30 '11 at 08:50
  • @stig-brautaset: Yes, I did. I thought I might have when I wrote the answer, but figured a wrong answer might help get a right one :-) I'll try again later. – Richard Povinelli Nov 30 '11 at 14:25
2

This finds the nearest day of the week by defining an interval of closest days of the week. Joda defines a week as starting on Monday. So if today is Tuesday and the day of the week is set to Sunday, the date will be for the following Sunday, not the previous. If the first day of the week is redefined to be Sunday, the date returned will be for the previous Sunday. The following code isn't effected by the definition of the first day of the week.

DateTime getNearestDayOfWeek(DateTime dateTime, String day) {
  //Create an interval containing the nearest days of the week.
  DateTime begin = dateTime.minusHours(DateTimeConstants.HOURS_PER_WEEK/2).dayOfWeek().roundHalfCeilingCopy();
  DateTime end   = dateTime.plusHours(DateTimeConstants.HOURS_PER_WEEK/2).dayOfWeek().roundHalfCeilingCopy();
  Interval interval = new Interval(begin, end);

  //Adjust nearest day to be within the interval. Doesn't depend on definition of first day of the week.
  DateTime nearest  = dateTime.dayOfWeek().setCopy(day);
  if (interval.isAfter(nearest))  //nearest is before the interval
    return nearest.plusWeeks(1);
  else if (interval.isBefore(nearest))  //nearest is after the interval
    return nearest.minusWeeks(1);
  else 
    return nearest;
}
Richard Povinelli
  • 1,419
  • 1
  • 14
  • 28
1

Based on Richard Povinelli's answer, but updated to use Java Time (as of Java 8)

public static LocalDate getNearestDayOfWeek(LocalDate date, DayOfWeek dayOfWeek) {
    LocalDate start = date.minusDays(3);
    LocalDate end = date.plusDays(3);
    LocalDate guessDate = date.with(dayOfWeek);
    // the nearest day is between start and end, so we adjust our guess if required
    if (guessDate.isAfter(end)) {
        // guessed one week to late
        return guessDate.minusWeeks(1);
    } else if (guessDate.isBefore(start)) {
        // guessed one week to early
        return guessDate.plusWeeks(1);
    } else {
        // the guess was correct
        return guessDate;
    }
}
Alex K.
  • 220
  • 3
  • 10
  • This is nice, but I posted an alternative solution using the simpler, lower-level TemporalAdjuster API, which doesn't require creating new LocalDate and doing date-comparisons. – Vanja D. May 09 '21 at 03:38