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
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
… (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);
}
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));
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 !
I am using the below code to retrieve the last day in the previous month - Ex: May. But it is returning 30 days instead of 31.
The code given below
package net.vcmg.date;
import java.util.Calendar;
import java.util.Date;
import org.apache.commons.lang.time.DateUtils;
public class LastDayPreviousMonth {
public static void main(String[] args) {
Date lastDateOfPreviousMonth = addMonths(lastDayOfTheMonth(today()), -1);
System.out.println("lastDateOfPreviousMonth: "+lastDateOfPreviousMonth);
}
//the below method is from Utils.java
public static Date lastDayOfTheMonth(Date d) {
Calendar cal = Calendar.getInstance();
cal.setTime(d);
int actualMax = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
cal.set(Calendar.DAY_OF_MONTH, actualMax);
return cal.getTime();
}
public static Date addMonths(Date date, int numMonths)
{
return DateUtils.addMonths(date, numMonths);
}
public static Date today()
{
return truncDate(now());
}
public static Date now()
{
// will cut-off milliseconds
return new Date( (System.currentTimeMillis()/1000) * 1000);
}
public static Date truncDate (Date date) {
return DateUtils.truncate(date, Calendar.DATE);
}
}
Here, when i call the lastDateOfPreviousMonth in the main method, it is returning 30 days alone. Not the 31 , May contains 31 days actually. Please help.
Java 8
If you are not constraint to use the old Date it will be better to use the new java.time.LocalDate
LocalDate previousMonth = LocalDate.now().minusMonths(1);
LocalDate start = previousMonth.withDayOfMonth(1);
LocalDate end = previousMonth.withDayOfMonth(previousMonth.lengthOfMonth());
System.out.println(start);
System.out.println(end);
Output
2019-05-01
2019-05-31
Edit
For your implementation, change the order of methods
addMonths - get the current date and provide the previous month addMonths(new Date(), -1)
lastDayOfTheMonth - get the last day of the previous month lastDayOfTheMonth(addMonths(new Date(), -1))
Date lastDateOfPreviousMonth = lastDayOfTheMonth(addMonths(new Date(), -1));
Output
lastDateOfPreviousMonth: Fri May 31 10:46:13 EEST 2019
Try this:
public static void main(String[] args) {
Date lastDateOfPreviousMonth = lastDayOfTheMonth(addMonths(today(), -1));
System.out.println("lastDateOfPreviousMonth: " + lastDateOfPreviousMonth);
}
When you call lastDayOfTheMonth for today() day will be 30. And after minus one month result expected will be 30, not 31.
It’s a logical error in the way you have thought out your program/algorithm. You are first finding the last day of the month, in this case June 30. You are then subtracting 1 month. That gives May 30 regardless of the fact that there are 31 days in May (it’s not explicit from the documentation of DateUtils.addMonths that it works this way, but it uses the poorly designed and outdated Calendar class internally, so this is what we should expect).
Instead do things in the opposite order. First find the previous month:
YearMonth lastMonth = YearMonth.now(ZoneId.of("Asia/Kolkata")).minusMonths(1);
2019-05
Since the new month doesn’t begin at the same point in time in all time zones, I recommend that you state your desired time zone as shown.
Only then find the last day of the month:
LocalDate lastDayOfLastMonth = lastMonth.atEndOfMonth();
2019-05-31
Avoid Date and Calendar
I recommend you don’t use Date and Calendar. Those classes are poorly designed and long outdated. Instead use LocalDate and other classes from java.time, the modern Java date and time API. This will also save you from the external dependency on Apache DateUtils since its functionality is generally built into the modern classes.
Links
Documentation of org.apache.commons.lang3.time.DateUtils.addMonths
Oracle tutorial: Date Time explaining how to use java.time.
I'm trying to parse a date from a String.
I'd like to identify the case where due to daylight savings, the clocks go back and a time is effectively "repeated" in the same day.
For example, based on UK daylight savings time, the clocks go back an hour at 2AM, 27/10/2019.
Therefore:
12:30AM 27/10/2019,
One hour later - 1:30AM 27/10/2019,
One hour later - 1:30AM 27/10/2019 (as at 2AM, we went back an hour),
One hour later - 2:30AM 27/10/2019.
Therefore "1:30AM 27/10/2019" is referring to two different times. This is the case I am trying to identify.
I have created the following, but it uses Date & Calendar classes, and some deprecated methods. I'd like to do this using the new java.time functionality - and I'm hoping there's an easier solution.
public static boolean isDateRepeatedDST(final Date date, TimeZone timeZone) {
if (timeZone == null) {
// If not specified, use system default
timeZone = TimeZone.getDefault();
}
if (timeZone.useDaylightTime()) {
// Initially, add the DST offset to supplied date
// Handling the case where this is the first occurrence
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.MILLISECOND, timeZone.getDSTSavings());
// And determine if they are now logically equivalent
if (date.toLocaleString().equals(calendar.getTime().toLocaleString())) {
return true;
} else {
// Then try subtracting the DST offset
// Handling the second occurrence
calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.MILLISECOND, -timeZone.getDSTSavings());
if (date.toLocaleString().equals(calendar.getTime().toLocaleString())) {
return true;
}
}
}
// Otherwise
return false;
}
ZoneId zone = ZoneId.of("Europe/London");
ZonedDateTime dateTime = ZonedDateTime.of(2019, 10, 27, 0, 30, 0, 0, zone);
for (int i = 0; i < 4; i++) {
System.out.println(dateTime);
dateTime = dateTime.plusHours(1);
}
Output:
2019-10-27T00:30+01:00[Europe/London]
2019-10-27T01:30+01:00[Europe/London]
2019-10-27T01:30Z[Europe/London]
2019-10-27T02:30Z[Europe/London]
You can see that the time 01:30 is repeated and that the offset is different the two times it comes.
If you want a test for whether a time is repeated:
public static boolean isDateRepeatedDST(ZonedDateTime dateTime) {
return ! dateTime.withEarlierOffsetAtOverlap().equals(dateTime.withLaterOffsetAtOverlap());
}
We can use it in the loop above if we modify the print statement:
System.out.format("%-37s %s%n", dateTime, isDateRepeatedDST(dateTime));
2019-10-27T00:30+01:00[Europe/London] false
2019-10-27T01:30+01:00[Europe/London] true
2019-10-27T01:30Z[Europe/London] true
2019-10-27T02:30Z[Europe/London] false
I recently discovered a difference between Oracle adds months to a given date (using ADD_MONTHS function) and the way Java adds months to a Calendar object.
For instance in oracle:
select add_months('2009-02-28', +1) from dual;
produced the result: "09-03-31"
And the query:
select add_months('2009-02-28', -1) from dual;
Produces the result "09-01-31"
However in Java, the results of the same calculations (using GregorianCalendar.add() method) are (respectively):
09-03-28
and
09-01-28
Is there some way to make Oracle and Java behave the same? (e.g. some setting in oracle or some parameter in Java)?
When you do month arithmetic you need to decide, from the business point of view, what is the right way to deal with months with differing numbers of days. The flip side to the issue you raised is what happens when going from a longer month, like August, to a shorter one like February (and even from Feb to Feb if leap years are involved).
If you are happy for errors to be reported because the calculation cannot find 'Feb-30', then look at INTERVAL
You can create own DateService class like bellow and use "addMonthToDateLikeOracle" method.
package db;
import org.joda.time.LocalDate;
import java.util.Calendar;
import java.util.Date;
public class DateService {
public static Date addMonthToDateLikeOracle(Date date, int months) {
Calendar c = Calendar.getInstance();
c.setTime(date);
Date retval;
if (DateService.isLastDayOfMonth(date)) {
c.add(Calendar.MONTH, 1);
c.add(Calendar.MONTH, 1);
c.set(Calendar.DATE, 1);
c.add(Calendar.DATE, -1);
retval = c.getTime();
} else {
retval = LocalDate.fromDateFields(date).plusMonths(months).toDate();
}
return retval;
}
public static boolean isLastDayOfMonth(Date date) {
if (date == null) {
throw new RuntimeException("The date parameter cannot be null!");
}
Date endOfMonth = getEndOfMonth(date);
return endOfMonth.equals(date);
}
public static Date getEndOfMonth(Date date) {
Date startOfMonth = getStartOfMonth(date);
Date startOfNextMonth = LocalDate.fromDateFields(startOfMonth).plusMonths(1).toDate();
return LocalDate.fromDateFields(startOfNextMonth).plusDays(-1).toDate();
}
public static Date getStartOfMonth(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.DAY_OF_MONTH, 1);
return calendar.getTime();
}
}
From the Oracle reference on add_months (http://download-west.oracle.com/docs/cd/B19306_01/server.102/b14200/functions004.htm)
If date is the last day of the month or if the resulting month has fewer days than the day component of date, then the result is the last day of the resulting month. Otherwise, the result has the same day component as date.
That means you're going to have to write a method for Java that performs the same check to get the same results (or a function in PL/SQL that behaves the same as Java).
You could write your own add months function in java.
public Date functionAddMonth(Date d, int month)
{
Calendar c = Calendar.getInstance();
c.setTime(d);
c.add(Calendar.DAY, 1);
c.add(Calendar.MONTH, month);
c.add(Calendar.DAY, -1);
return c.getTime();
}