2

I have an enum that looks like this

enum Period{DAY, WEEK, MONTH, YEAR}

What i need is a function that adds a specified amout of times the given Period to today while setting the day of month so that it is equal to the start date (if the outcome is valid).

Or maybe it is easier to understand like this: Imagine you get your salary on the 31st every month (where applicable). The function returns the next valid date (from today) when you will receive your next salary. Where the function can distinguish if you get it Daily, Weekly, Monthly, Yearly and how often in the specified interval. It also takes care of invalid dates

Lets have a look at an example:

public static Date getNextDate(Date startDate, Period period, int times){
/* 
Examples:
getNextDate(31.08.2020, MONTH, 1) -> 30.09.2020
getNextDate(31.08.2020, MONTH, 2) -> 31.10.2020
getNextDate(30.05.2020, MONTH, 2) -> 30.09.2020
getNextDate(30.06.2020, MONTH, 2) -> 30.10.2020 (This is the next valid date after today)

Years are pretty simple i guess (Okay, there is at least one edge case):
getNextDate(28.02.2020, YEAR, 1) -> 28.02.2021
getNextDate(29.02.2020, YEAR, 1) -> 28.02.2021 <- Edge case, as 2020 is a gap year
getNextDate(29.02.2020, YEAR, 4) -> 29.02.2024 <- gap year to gap year

For weeks and days there are no edge cases, are there?
getNextDate(29.02.2020, DAY, 1) -> 03.09.2020
getNextDate(29.02.2020, DAY, 3) -> 05.09.2020
getNextDate(29.02.2020, WEEK, 2) -> 12.09.2020 (Same as DAY,14)

Important: If today is already a payment day, this already is the solution
getNextDate(03.09.2020, MONTH, 1) -> 03.09.2020 (No change here, the date matches today)
*/
}

I actually would prefer to use the modern LocalDate API (Just the input is an old date object at the moment, but will be changed later)

I hope i did not forget any edge cases.

Update with what i did

//This is a method of the enum mentioned
public Date getNextDate(Date baseDate, int specific) {
        Date result = null;
        switch (this) {
            case DAY:
                result = DateTimeUtils.addDays(baseDate, specific);
                break;
         
            case WEEK:
                result = DateTimeUtils.addWeeks(baseDate, specific);
                break;
          
            case MONTH:
                result = DateTimeUtils.addMonths(baseDate, specific);
                break;
            case YEAR:
                result = DateTimeUtils.addYears(baseDate, specific);
                break;
        }
        return result;
    }

public Date getNextDateAfterToday(Date baseDate) {
        today = new Date();
        while(!baseDate.equals(today ) && !baseDate.after(today)){
            baseDate= getNextDate(baseDate,1);
        }
        return startHere;
    }

My getNextDate() Method works. The getNextDateAfterToday() also works, but does not return valid dates for edge cases. Example 31.06.2020, MONTH,1 would immediatly be stuc at 30st of every month and never skip back even if the month has 31 days. For 30.09.2020 it would be correct. But for 31.10.2020 it wouldn't

Avinta
  • 678
  • 1
  • 9
  • 26
  • 1
    Please turn to the [help] to learn how/what to ask here. Just dropping requirements "this is what I want" isn't appreciated. When you try something yourself, and you get stuck with a specific problem, we will gladly help. But please understand that this place is not intended to give guidance with the possibly many steps required to get you from your vision to a working program. – GhostCat Sep 03 '20 at 13:01
  • Okay, i will glady edit the question with the function i wrote, which is terrible. I thought it might be easier to just start fresh:) – Avinta Sep 03 '20 at 13:02
  • See [mcve] then. If your code isnt working, then focus on the "broken" part. If your code is functional and complete, then ... well: this community isnt intended for generic reviews of working code. – GhostCat Sep 03 '20 at 13:03
  • ```getNextDate(30.05.2020, MONTH, 2) -> 30.09.2020 getNextDate(30.06.2020, MONTH, 2) -> 30.10.2020``` Why? – Miss Chanandler Bong Sep 03 '20 at 13:08
  • @M.S. Because i need the function to return the next valid FUTURE date (or today if today would be a valid date in the series) – Avinta Sep 03 '20 at 13:13
  • I believe ```30.05.2020``` with 2 months parameter should become ```30.07.2020``` – Miss Chanandler Bong Sep 03 '20 at 13:15
  • Yes. But 30.07 is a past date. I need the next valid future date. Take 30.07 and execute the function again you end at 30.09.2020 (which is the first valid future date int the 2 month series starting at 30.07) – Avinta Sep 03 '20 at 13:17

4 Answers4

2

I finally figured a way (although it seems way, way, way to complicated for what i really wanted to achieve). I changed my getNextDateAfterTodayto this

public Date getNextValidFutureDate(Date entryDate, Date startDate, int specific) {
        Date result = new Date(startDate.getTime());
        while (!result.equals(entryDate) && !result.after(entryDate)) {
            result = getNextDate(result, true, specific);
        }
        LocalDate ldStart = startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        LocalDate ldResult = result.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

        if (ldResult.getDayOfMonth() < ldStart.getDayOfMonth() && this != DAY && this != WEEK && this != YEAR) {
            if (ldResult.lengthOfMonth() >= ldStart.getDayOfMonth()) {
                ldResult = ldResult.with(ChronoField.DAY_OF_MONTH, ldStart.getDayOfMonth());
            } else {
                ldResult = ldResult.with(ChronoField.DAY_OF_MONTH, ldResult.lengthOfMonth());
            }
        }
        return Date.from(ldResult.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }

I did not change the other method to use LocalDate, but will do this in the future. This works with all test cases i posted above. Though i hope i did not miss essential ones

Avinta
  • 678
  • 1
  • 9
  • 26
1

Not using the decade old date api which is badly written and generally unsafe and painful to use might be the best idea. Using java.time might be in your favor here. Changing your method signature to this, is all you'd have to do:

import java.time.LocalDate;
import java.time.Period;

...

public static LocalDate getNextDate(LocalDate startDate, Period period) {
    return startDate.plus(period);
}

And can then be called like:

LocalDate startDate = LocalDate.of(3, 9, 2020);
LocalDate nextDate = getNextDate(startDate, Period.ofDays(20)); // 2020-09-23

Or simply dropping your helper function in the first place and using it directly:

LocalDate nextDate = startDate.plus(Period.ofDays(20));
Lino
  • 19,604
  • 6
  • 47
  • 65
  • 1
    This is not the exactly what i am looking for. This just adds the specified amount of days, month years to a start date. But i need the resulting date to be a future date in the series that matches. When i start at 29.02 and want to add a month i need the outcome to be 29.09.2020 (as this would be the valid outcome if you added one month to 29.02 until you reached a future date). And if i use this in a loop. I end at the same problem i have posted. One month to 31.08 is 30.09 but one month more is 30.10 and not 31.10 (and i started with 31st, so i expect 31st in october again) – Avinta Sep 03 '20 at 13:22
1

… (although it seems way, way, way to complicated for what i really wanted to achieve) …

Your own solution is not bad. I just couldn’t let the challenge rest, so here’s my go. I believe it’s a little bit simpler.

I am going all-in on java.time, the modern Java date and time API. I also skipped your Period enum since the predefined ChronoUnit enum fulfils the purpose. Only it also includes hours, minutes and other units that don’t make sense here, so we need to reject those.

The Date class is poorly designed as well as long outdated. Avoid it if you can (if you cannot avoid it, I am giving you the solution in the end).

public static LocalDate getNextDate(LocalDate startDate, TemporalUnit period, int times) {
    if (! period.isDateBased()) {
        throw new IllegalArgumentException("Cannot add " + period + " to a date");
    }
    
    LocalDate today = LocalDate.now(ZoneId.of("America/Eirunepe"));
    
    if (startDate.isBefore(today)) {
        // Calculate how many times we need to add times units to get a future date (or today).
        // We need to round up; the trick for doing so is count until yesterday and add 1.
        LocalDate yesterday = today.minusDays(1);
        long timesToAdd = period.between(startDate, yesterday) / times + 1;
        return startDate.plus(timesToAdd * times, period);
    } else {
        return startDate;
    }
}

For demonstrating the method I am using this little utility method:

public static void demo(LocalDate startDate, TemporalUnit period, int times) {
    LocalDate nextDate = getNextDate(startDate, period, times);
    System.out.format("getNextDate(%s, %s, %d) -> %s%n", startDate, period, times, nextDate);
}

Now let’s see:

    demo(LocalDate.of(2020, Month.AUGUST, 31), ChronoUnit.MONTHS, 1);
    demo(LocalDate.of(2020, Month.AUGUST, 31), ChronoUnit.MONTHS, 2);
    demo(LocalDate.of(2020, Month.MAY, 30), ChronoUnit.MONTHS, 2);
    demo(LocalDate.of(2020, Month.JUNE, 30), ChronoUnit.MONTHS, 2);
    
    System.out.println();
    
    demo(LocalDate.of(2020, Month.FEBRUARY, 28), ChronoUnit.YEARS, 1);
    demo(LocalDate.of(2020, Month.FEBRUARY, 29), ChronoUnit.YEARS, 1);
    demo(LocalDate.of(2020, Month.FEBRUARY, 29), ChronoUnit.YEARS, 4);
    
    System.out.println();
    
    demo(LocalDate.of(2020, Month.FEBRUARY, 29), ChronoUnit.DAYS, 1);
    demo(LocalDate.of(2020, Month.FEBRUARY, 29), ChronoUnit.DAYS, 3);
    demo(LocalDate.of(2020, Month.FEBRUARY, 29), ChronoUnit.WEEKS, 2);
    
    System.out.println();
    
    demo(LocalDate.of(2020, Month.SEPTEMBER, 4), ChronoUnit.MONTHS, 1);

When running just now, the output was:

getNextDate(2020-08-31, Months, 1) -> 2020-09-30
getNextDate(2020-08-31, Months, 2) -> 2020-10-31
getNextDate(2020-05-30, Months, 2) -> 2020-09-30
getNextDate(2020-06-30, Months, 2) -> 2020-10-30

getNextDate(2020-02-28, Years, 1) -> 2021-02-28
getNextDate(2020-02-29, Years, 1) -> 2021-02-28
getNextDate(2020-02-29, Years, 4) -> 2024-02-29

getNextDate(2020-02-29, Days, 1) -> 2020-09-04
getNextDate(2020-02-29, Days, 3) -> 2020-09-05
getNextDate(2020-02-29, Weeks, 2) -> 2020-09-12

getNextDate(2020-09-04, Months, 1) -> 2020-09-04

I should say that it agrees with your examples from the question.

If you cannot avoid having an old-fashioned Date object and an instance of your own Period enum and/or you indispensably need an old-fashioned Date back, you may wrap my method into one that performs the necessary conversions. First I would extend your enum to know its corresponding ChronoUnit constants:

enum Period {
    DAY(ChronoUnit.DAYS),
    WEEK(ChronoUnit.WEEKS),
    MONTH(ChronoUnit.MONTHS),
    YEAR(ChronoUnit.YEARS);

    private final ChronoUnit unit;
    
    private Period(ChronoUnit unit) {
        this.unit = unit;
    }

    public ChronoUnit getUnit() {
        return unit;
    }
}

Now a wrapper method may look like this;

public static Date getNextDate(Date startDate, Period period, int times) {
    ZoneId zone = ZoneId.of("America/Eirunepe");
    LocalDate startLocalDate = startDate.toInstant().atZone(zone).toLocalDate();
    LocalDate nextDate = getNextDate(startLocalDate, period.getUnit(), times);
    Instant startOfDay = nextDate.atStartOfDay(zone).toInstant();
    return Date.from(startOfDay);
}
Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • I think this is actually what I was looking for and also looks easier. I am not home atm. But I will definelty test this when I am. Thank you for that effort. (Funny how such a small problem can become so complicated) – Avinta Sep 06 '20 at 21:03
0

You can use the class Calendar to resolve your problem like that :

public static Date getNextDate(Date startDate, int period, int times) {

    Calendar calendar = Calendar.getInstance();
    calendar.setTime(startDate);
    calendar.add(period, times);
    return calendar.getTime();
}

The period is an int defined in the Calendar class, you can call your function like that :

System.out.println(getNextDate(new Date(), Calendar.MONTH, 1));
System.out.println(getNextDate(new Date(), Calendar.MONTH, 3));
System.out.println(getNextDate(new Date(), Calendar.YEAR, 1));

If you realy need to use your enum, you can do it !

AyoubMK
  • 526
  • 1
  • 4
  • 19
  • Thank you for your reply. But this sadly is also not my problem. I know how to add a month to a date. But i need that to happen loopwise until i hit a future date where the original day of month is still valid – Avinta Sep 03 '20 at 13:26
  • Using the `Calendar` class for what the OP is trying is possible, but gets complicated. I recommend we don’t. Also the `Calendar` class is poorly designed and long outdated. [java.time, the modern Java date and time API](https://docs.oracle.com/javase/tutorial/datetime/) is so much nicer to work with. – Ole V.V. Sep 05 '20 at 07:39