Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to find nearest week day for an arbitrary date?

Tags:

java

jodatime

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;
    }
like image 942
Stig Brautaset Avatar asked Nov 30 '11 05:11

Stig Brautaset


5 Answers

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 ));
   }
like image 97
leonbloy Avatar answered Nov 10 '22 12:11

leonbloy


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;
}
like image 45
Richard Povinelli Avatar answered Nov 10 '22 12:11

Richard Povinelli


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);
like image 35
brian.t.forsythe Avatar answered Nov 10 '22 14:11

brian.t.forsythe


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();
    }
}
like image 20
Vanja D. Avatar answered Nov 10 '22 12:11

Vanja D.


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;
    }
}
like image 27
Alex K. Avatar answered Nov 10 '22 12:11

Alex K.