Reputation: 768
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
Upvotes: 2
Views: 751
Reputation: 86203
… (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);
}
Upvotes: 1
Reputation: 768
I finally figured a way (although it seems way, way, way to complicated for what i really wanted to achieve). I changed my getNextDateAfterToday
to 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
Upvotes: 2
Reputation: 19926
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));
Upvotes: 1
Reputation: 526
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 !
Upvotes: 0