6

I have six int variables: currentMonth, currentDay, monthFrom, dayFrom, monthUntil and dayUntil. I need to check if todays month and day falls within a range of from and until variables.

For example if currentMonth = 1, currentDay = 2, monthFrom = 11, dayFrom = 24, monthUntil = 3 and dayUntil = 3 the date is in the interval and the method should return true.

I'm not sure how to do it though. Is there any other option than to check every possible outcome using ifs?

B. Hurray
  • 382
  • 4
  • 16
  • 3
    What do you mean by "every possible outcome"? And is there any reason you don't just use `LocalDate` values for each of today/from/until? – Jon Skeet Apr 14 '17 at 10:40
  • @JonSkeet Excuse my english. What I've meant was if there is any simpler option than to solve this with using many ifs? I'm a begginer at Java, I've never heard of `LocalDate` class. I'm gonna check it out now. – B. Hurray Apr 14 '17 at 10:49
  • @B.Hurray Solving this with if statements is going to take a long time. You'd have to take into account the fact that each month has different days and Februaries change on leap-years. Check my answer. Contains one single if-statement wrapped into a nice method. This is also if your date system is based off of the Gregorian. Other calendars might be a mess. –  Apr 14 '17 at 11:03
  • Urgh - I've just realized that your sample `monthFrom` is later than your sample `monthUntil`. Is that deliberate? – Jon Skeet Apr 14 '17 at 11:07
  • @JonSkeet Yes . – B. Hurray Apr 14 '17 at 11:08
  • 1
    Oops @B.Hurray! I just fixed the answer. Slight mistake with `GregorianCalendar testDate = new GregorianCalendar(0, month, day);` Change that `0` to a `yearRoll` otherwise it won't work correctly! –  Apr 14 '17 at 11:08
  • Without year portion may cause problem, you can't say the date is valid or not – Yu Jiaao Apr 14 '17 at 11:08
  • Is that always in the same calendar year? Or would Dcember 28 count as between Dec 25 and January 6? – Ole V.V. Apr 14 '17 at 11:16
  • @OleV.V. Not necessarily. Yes, that would count. – B. Hurray Apr 14 '17 at 11:23
  • Should "1/1" be between "1/1" and "1/1", i.e. what do you expect if everything denotes the same day? – Marvin Apr 14 '17 at 11:27
  • If interpreting current as in 2017, from in 2016 and until in 2018, the date will always be in the range. I can also choose years where it will never be. I understand that I don’t know which year those dates belong in, that’s fine, only, can I assume all three are in the same year? Or any other assumption about the years involved? Otherwise I’m afraid the problem can’t be solved. – Ole V.V. Apr 14 '17 at 11:29
  • 1
    Also duplicate of: [How to check if current date is between two reoccurring dates in java?](http://stackoverflow.com/q/29990001) – Basil Bourque Apr 15 '17 at 19:15

4 Answers4

4

Just do a quick range check with the calendar:

Note: Make sure to import java.util.GregorianCalendar;

public static boolean isDateInRange(int month, int day,
                                    int monthFrom, int dayFrom,
                                    int monthUntil, int dayUntil) {
    int yearRoll = 0;
    int currentRoll = 0;
    if (monthUntil < monthFrom) yearRoll = -1; // Ensures date is calculated correctly.
    if (month >= monthFrom && yearRoll < 0) currentRoll = -1;

    GregorianCalendar testDate = new GregorianCalendar(currentRoll, month, day);
    GregorianCalendar startDate = new GregorianCalendar(yearRoll, monthFrom, dayFrom);
    GregorianCalendar endDate = new GregorianCalendar(0, monthUntil, dayUntil);

    // This makes it pass if its between OR EQUAL to the interval.
    // Remove if you only want to pass dates explicitly BETWEEN intervals.
    if (testDate.compareTo(startDate) == 0 || testDate.compareTo(endDate) == 0) {
        return true;
    }

    return !(testDate.before(startDate) || testDate.after(endDate));
}

This will also take into account the fact that say February is between November and March. Since November is a part of the previous year, it will move the from date back a year to ensure passing.

What it doesn't take into account however, is the fact that February has an extra day on leap-years. To add extra-precision, you need integers for the years. You can do the following:

public static boolean isDateInRange(int year, int month, int day,
                                    int yearFrom, int monthFrom, int dayFrom,
                                    int yearUntil, int monthUntil, int dayUntil) {

    GregorianCalendar testDate = new GregorianCalendar(year, month, day);
    GregorianCalendar startDate = new GregorianCalendar(yearFrom, monthFrom, dayFrom);
    GregorianCalendar endDate = new GregorianCalendar(yearUntil, monthUntil, dayUntil);

    return !(testDate.before(startDate) || testDate.after(endDate));
}

And here is an implementation with the date values you gave plus a few more:

public static void main(String[] args) {
    System.out.println(isDateInRange(1, 2,
                                     11, 24,
                                     3, 3));
    System.out.println(isDateInRange(11, 25,
                                     11, 24,
                                     3, 3));
    System.out.println(isDateInRange(1, 2,
                                     1, 1,
                                     3, 3));
    System.out.println(isDateInRange(1, 22,
                                     1, 21,
                                     1, 25));
}

And the results are:

true
true
true
true

Will also work with @Marvin's tests.

  • 2
    I would strongly recommend using the `java.time` classes instead of `Calendar` here. I don't think the second piece of code is correct, given it doesn't handle the sample in the question (where fromMonth is later than untilMonth). The use of year 0 for `endDate` is also very suspicious... – Jon Skeet Apr 14 '17 at 11:21
  • @JonSkeet I chose 0 just as a starting date for simplicity. I wrote that it isn't compliant with Februaries changes. As for the sample handling, I don't understand what you mean sorry. I would be happy to use `java.Time` instead, would be a good learning curve for me aswell as the OP. –  Apr 14 '17 at 11:28
  • Testing `isDateInRange(4, 1, 1, 24, 3, 31));` returns me `true`, while it should return `false`. – B. Hurray Apr 14 '17 at 13:12
  • @B.Hurray Thats interesting... I'm looking at it right now and it almost looks like a flaw with `GregorianCalendar`. Give me one minute. –  Apr 14 '17 at 13:18
  • @B.Hurray Ah, thats because 0 is January. Therefore, 3 is April. There's only 30 days in April, opposed to 31. The 31st day is infact the first of May. There is no difference in date. Your question only asked for BETWEEN, but if you want it to include the range boundary thats fine. I'm adding it into the answer now. –  Apr 14 '17 at 13:28
  • @B.Hurray So in actual fact, it is supposed to be true. The same will happen for Jon Skeet and Marvins answers. If you want 1 to be January, `-1` from every integer as you provide it to the function. So then if you have December equal to 12, minusing one reduces it to 11, which is the correct index for it. –  Apr 14 '17 at 13:28
  • Seems to be working great now. Thanks for the detailed explanation. – B. Hurray Apr 14 '17 at 13:36
  • @B.Hurray Thats good to hear. Sorry about that! If your using Eclipse for programming, hovering over `GregorianCalendar` will tell you: `Month value is 0-based. e.g., 0 for January.` Always useful to know! –  Apr 14 '17 at 13:37
  • 1
    LocalDate is actually 1-based, so there's a difference. My method will return `false` as expected ;) – Marvin Apr 14 '17 at 14:24
  • It still doesn’t seem to work correctly in all cases. April 15 is *not* between May 10 and May 20, but `isDateInRange(Calendar.APRIL, 15, Calendar.MAY, 10, Calendar.MAY, 20` yields `true`. I am using the `Calendar` month constants to avoid the -1 problem, so it ought to work. Also April 20 is *not* between April 5 and April 10, but `isDateInRange(Calendar.APRIL, 20, Calendar.APRIL, 5, Calendar.APRIL, 10)` yields true. – Ole V.V. Apr 15 '17 at 14:41
  • @OleV.V. Thankyou! Remove the `=` from `<=` and it's solved. Updating now. *cough* So many issues with this answer... :O :( –  Apr 15 '17 at 15:14
  • I’m afraid it’s a little more complicated than you think if you want all corner cases. This one may be sought-after, but I consider Dec 25 between April 16 and April 1. `isDateInRange(Calendar.DECEMBER, 25, Calendar.APRIL, 16, Calendar.APRIL, 1)` returns `false`. – Ole V.V. Apr 15 '17 at 16:04
  • I think that instead of comparing the month values, you should compare the `GregorianCalendar` objects and roll one year back if they are in the “wrong order”. Only problem with this is it will roll February 29 back to February 28 in the previous year, giving inaccurate results for this particular date. – Ole V.V. Apr 15 '17 at 16:12
  • @OleV.V. Oh of course, I'd be an idiot not to agree with you. The issue is, the OP wants to restrict themselves to integer variables. If I was to do the same thing, I would be explicitly storing `Date`'s and not even bother with integers. –  Apr 15 '17 at 23:11
4

I would suggest using java.time.MonthDay to represent each value involved. You then need to consider two alternative situations:

  • from is before or equal to until, in which case you need to perform a test of from <= current && current <= until.
  • from is later than until, in which case you need to perform a test of current <= until || current >= from

So:

public static boolean isBetween(
    int currentMonth, int currentDay,
    int fromMonth, int fromDay,
    int untilMonth, int untilDay)
{
    MonthDay current = MonthDay.of(currentMonth, currentDay);
    MonthDay from = MonthDay.of(fromMonth, fromDay);
    MonthDay until = MonthDay.of(untilMonth, untilDay);

    if (from.compareTo(until) <= 0)
    {
        return from.compareTo(current) <= 0 &&
            current.compareTo(until) <= 0;
    }
    else
    {
        return current.compareTo(until) <= 0 ||
            current.compareTo(from) >= 0;
    }
}

The two return statements could be combined, but it's probably simpler not to.

(This gives the same results as Marvin's code for his test cases.)

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • @Marvin: Not sure what you mean by that. An example would help. – Jon Skeet Apr 14 '17 at 11:05
  • @Marvin: If you meant when `from` is later than `until`, I've just addressed that. – Jon Skeet Apr 14 '17 at 11:09
  • @Marvin: Oh, I think I see what you mean... Yes, will need to edit. But there are issues in yours too :) – Jon Skeet Apr 14 '17 at 11:12
  • @Marvin: Have a look now - I think it's sorted... easier than rolling. – Jon Skeet Apr 14 '17 at 11:37
  • 1
    Yep, looks nice and clean. That question turned out to be far more interesting than anticipated. – Marvin Apr 14 '17 at 11:48
  • 1
    Wow thats beautiful. <3 Nice to see an implementation of `MonthDay` exists too! –  Apr 14 '17 at 11:55
  • 1
    One thing, though: Why not use `MonthDay.isAfter`/`MonthDay.isBefore`? – Marvin Apr 14 '17 at 12:43
  • Great answer! Doesn't seem to work on Android though. Cannot resolve MonthDay :/ – B. Hurray Apr 14 '17 at 13:28
  • @B.Hurray: You never mentioned Android anywhere in the question. How were we meant to know you needed that? – Jon Skeet Apr 14 '17 at 15:06
  • 1
    If you want this elegant solution on Android, get the [ThreeTenABP](https://github.com/JakeWharton/ThreeTenABP), it includes the `MonthDay` class and much other useful stuff. That is ThreeTen for JSR-310, where the `java.time` classes are described, and ABP for Android Backport because the classes were intriduced in Java 8 and backported to Java 7 for Androif (there’s a backport for Java 6 too). – Ole V.V. Apr 15 '17 at 10:54
2

You can create LocalDate objects from your inputs and let java do the checks. The only difficulty is to determine which years all those dates should fall in.

import java.time.LocalDate;

public class DateBetween {

    public static void main(String[] args) {
        System.out.println(isBetween(1, 2, 11, 24, 3, 3)); // true
        System.out.println(isBetween(4, 4, 1, 20, 6, 3)); // true
        System.out.println(isBetween(1, 1, 2, 3, 7, 8)); // false
        System.out.println(isBetween(11, 4, 2, 3, 7, 8)); // false
        System.out.println(isBetween(2, 29, 2, 3, 7, 8)); // true
        System.out.println(isBetween(2, 29, 11, 24, 3, 3)); // true
        System.out.println(isBetween(1, 1, 1, 1, 1, 1)); // true
    }

    private static boolean isBetween(int currentMonth, int currentDay, int monthFrom, int dayFrom, int monthUntil,
            int dayUntil) {
        // Default to 2000 so that Feb 29st will be valid.
        int currentYear = 2000;
        LocalDate dateFrom = LocalDate.of(currentYear, monthFrom, dayFrom);
        LocalDate dateUntil = LocalDate.of(currentYear, monthUntil, dayUntil);
        if (dateFrom.isAfter(dateUntil)) {
            // Assume dateUntil is in the next year (e.g. for 11/24/2000 -
            // 6/3/2001)
            dateUntil = dateUntil.plusYears(1);
        }
        // Day to check in current year (for non-overlapping ranges)
        LocalDate currentDateThisYear = LocalDate.of(currentYear, currentMonth, currentDay);
        // Day to check in next year (for overlapping ranges)
        LocalDate currentDateNextYear = currentDateThisYear.plusYears(1);
        if (!(currentDateThisYear.isBefore(dateFrom) || currentDateThisYear.isAfter(dateUntil))) {
            return true;
        } else if (!(currentDateNextYear.isBefore(dateFrom) || currentDateNextYear.isAfter(dateUntil))) {
            return true;
        }
        // Neither of the days to check are in the range
        return false;
    }
}

The method considers dateUntil to be in the next year if it is after dateFrom. For overlapping ranges the "current" date is checked for the current as well as for the next year, as e.g. in the range from "Nov 1st" (2017) to "Mar 1st" (2018) you might want to check for "Dec 24st" or "Jan 1st".

Obviously this will become a lot easier if you can also provide currentYear, yearFrom and yearUntil.

Marvin
  • 13,325
  • 3
  • 51
  • 57
  • I would avoid using the *actual* current year, as it causes problems for leap years. Additionally, by only checking the *months* of from/until, you're doing the wrong thing if they have the same month but the from day is after the until day. – Jon Skeet Apr 14 '17 at 11:11
  • @JonSkeet Updated it to work with Feb 29st as well (borrowing your approach here) and fixed the from/until comparison. Thanks! – Marvin Apr 14 '17 at 11:17
  • 1
    Drop the year trouble. Use `MonthDay` rather than `LocalDate`. It too has `isBefore` and `isAfter` methods. – Ole V.V. Apr 14 '17 at 11:18
  • @OleV.V.: Ooh, indeed. Had forgotten about MonthDay entirely :) – Jon Skeet Apr 14 '17 at 11:19
  • @OleV.V. That sounds promising, thank you. Although that doesn't work too well for overlapping years. – Marvin Apr 14 '17 at 11:21
  • Yes, it's still painful. Surprisingly annoying problem... – Jon Skeet Apr 14 '17 at 11:40
  • Not as bad as I thought - I'd just made a mistake elsewhere. – Jon Skeet Apr 14 '17 at 11:45
  • Ah.. I see. You stuck checks for both leap-year *and* normal-year in your returns. Very nice! –  Apr 14 '17 at 11:47
0

Here’s the hack.

I didn’t want to use neither GregorianCalendar nor LocalDate when you don’t have years for your dates because they force you to pick a year anyway. Particularly with February 29 this is not just picking any year you can think of.

First I want you to think about, is there a way you can solve the year problem where you get your dates from, and also, why are you having your dates as two ints when there are date and calendar classes to choose from? If it doesn’t make sense to add a year component to your dates, I would still think you could benefit from always storing them as MonthDay rather than two ints. On Android you will have to weigh the cost of the dependency on ThreeTenABP, of course, but it seems to me to be a future-proof one in this case: let’s hope there will come Java 8 for Android with java.time built in sooner or later.

In the meantime, if you don’t want to use MonthDay, here’s the solution without any library classes at all. Just pretend there are 32 days in a month and calculate a day number for each of the dates involved. It won’t make sense as a day number, but it will have the convenient property that we want: the day numbers will be in the same order as the days in the calendar.

public static boolean isBetween(int currentMonth, int currentDay, 
        int monthFrom, int dayFrom, 
        int monthUntil, int dayUntil) {
    final int daysInMonth = 32; // just imagine; pick any number >= 31
    int current = currentMonth * daysInMonth + currentDay;
    int from = monthFrom * daysInMonth + dayFrom;
    int until = monthUntil * daysInMonth + dayUntil;

    if (until < from) { // until is in the following year
        return current >= from || current <= until;
    } else {
        return current >= from && current <= until;
    }
}

Please adjust the use of < and <= etc. to your exact requirements.

One of the downsides (apart for the trickyness or obscurity) is you get no input validation at all. I may nonsensically call isBetween(55, -45, 83, 98, -29, 84) and it will return either false or true with no sign that anything is not quite right.

Link: Get java.time classes for Android in ThreeTenABP.

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161