Calendar behavior is not compatible with LocalDate? - java

Hi buddies I'm in a trouble trying to migrate a behavior from calendar to localdate.
payDate.set(Calendar.DAY_OF_MONTH,payDay)
Lets imagine that payDate had the current date, 2020-01-29
for business reasons payDay can had the value of 0, so, when the previous code line is executed with the previous scenario, the result is that payDate update the date to 2019-12-31,
that is to say the the date back to the last day of the past month.
I'm not sure, the technical reason of this, if someone can explain to me this I'll be so thankful, I tried checking the java doc but it was not helpful.
So I need to replicate that behavior with LocalDate java library. From my point of view; the similar of set method from Calendar with the value of DAY_OF_MONTH in LocalDate is:
payDate.withDayOfMonth(payDay)
But when the below scenario is presented and payDay is equal to 0 I get an error:
java.time.DateTimeException: Invalid value for DayOfMonth (valid values 1 - 28/31): 0
Also I had some ideas about how can I get the same result of calendar in localDate when the rule comes on (if payDay is 0, return to the last day of previous month), but are too verbose.
If you know a similar behavior on LocalDate please help me. Thanks.

TL;DR: Use payDate = payDate.plusDays(payDay - payDate.getDayOfMonth());
The behavior of Calendar you're describing is documented in the javadoc:
Leniency
Calendar has two modes for interpreting the calendar fields, lenient and non-lenient. When a Calendar is in lenient mode, it accepts a wider range of calendar field values than it produces. When a Calendar recomputes calendar field values for return by get(), all of the calendar fields are normalized. For example, a lenient GregorianCalendar interprets MONTH == JANUARY, DAY_OF_MONTH == 32 as February 1.
When a Calendar is in non-lenient mode, it throws an exception if there is any inconsistency in its calendar fields. For example, a GregorianCalendar always produces DAY_OF_MONTH values between 1 and the length of the month. A non-lenient GregorianCalendar throws an exception upon calculating its time or calendar field values if any out-of-range field value has been set.
To show the effect of this, try setting the date of a Calendar to January 70, 2020:
Calendar cal = Calendar.getInstance();
cal.clear();
cal.set(2020, Calendar.JANUARY, 70);
System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(cal.getTime()));
Output
2020-03-10
You would get the same result if you did:
cal.set(2020, Calendar.JANUARY, 1);
cal.add(Calendar.DAY_OF_MONTH, 69);
LocalDate is always non-lenient, so you can't set the day-of-month value to a value that is out-of-range. You can however get the same result as what Calendar does, by changing the operation to "add" instead of "set".
So, if you have a particular date, e.g. the 2020-01-29 date mentioned in the question, and you want to "set" the day-of-month value to 70 or 0, with same lenient overflow logic as Calendar has, do this:
LocalDate date = LocalDate.parse("2020-01-29");
date = date.plusDays(70 - date.getDayOfMonth());
System.out.println(date);
LocalDate date = LocalDate.parse("2020-01-29");
date = date.plusDays(0 - date.getDayOfMonth());
System.out.println(date);
Output
2020-03-10
2019-12-31
As you can see, date.plusDays(dayToSet - date.getDayOfMonth()) will give you the desired result.

Here’s how I would go about it:
LocalDate payDate = LocalDate.now(); // or whatever
int payDay = 0;
if (payDay == 0) {
// simulate `GregorianCalendar` behaviour: day 0 is the day before day 1
payDate = payDate.withDayOfMonth(1).minusDays(1);
} else {
payDate = payDate.withDayOfMonth(payDay);
}
System.out.println(payDate);
When I ran the snippet just now, the output was the date you already mentioned:
2019-12-31
If we wanted it shorter, we could use payDate.withDayOfMonth(1).minusDays(1).plusDays(payDay) or the trick from Andreas’ answer, and we would not need the if statement. I would not, though. (1) It’s harder to read. (2) It doesn’t give the validation of payDay that comes for free in the snippet above.
The confusing behaviour of Calendar comes from not range checking the argument to set(). So day 0 of the month is the day before day 1 of the month. Day -1 would be the day before that, and so forth. It’s in this snippet from the documentation (or was supposed to be, at least):
When a Calendar is in lenient mode, it accepts a wider range of
calendar field values than it produces. When a Calendar recomputes
calendar field values for return by get(), all of the calendar
fields are normalized. For example, a lenient GregorianCalendar
interprets MONTH == JANUARY, DAY_OF_MONTH == 32 as February 1.
You may read it with this snippet from the documentation of the setLenient method:
The default is lenient.
Links
Documentation of Calendar
Documentation of Calendar.setLenient()

You're not going to be able to just invoke one method to achieve the same results. If you're sure that setting DAY_OF_MONTH to 0 should cause it to roll back one month (this is the type of thing I'd run past a business analyst or product owner for a sanity check) then you're going to have to do something like this:
int payDay = 0;
LocalDate payDate = LocalDate.of(2020, Month.JANUARY, 29);
if(payDay == 0) {
payDate = payDate.minusMonths(1);
payDay = payDate.lengthOfMonth();
}
payDate = payDate.withDayOfMonth(payDay);
Another approach:
int payDay = 0;
LocalDate payDate = LocalDate.of(2020, Month.JANUARY, 29);
if(payDay == 0) {
payDate = payDate.withDayOfMonth(1).minusDays(1);
} else {
payDate = payDate.withDayOfMonth(payDay);
}

Related

Subtracting one day off Calendar doesn't work

I am trying to build a simple program with java.util.Calendar. When trying to get the weekday before, my output always stays the same. Code and what I tried below:
Calendar c = Calendar.getInstance();
c.set(Calendar.MONTH, month);
c.set(Calendar.DAY_OF_MONTH, day);
c.set(Calendar.YEAR, year);
int date = c.get(Calendar.DAY_OF_WEEK);
return new SimpleDateFormat("EEEE").format(date).toUpperCase();
This was my code at the start. I have tried subtracting day by one:
day = day-1;
I have tried adding minus one to both the Day of Month and Day of week field:
c.add(Calendar.DAY_OF_WEEK, -1);
c.add(Calendar.DAY_OF_MONTH, -1);
I think that it has something to do with my SimpleDateFormat, but I am not sure.
I am trying to build a simple program with java.util.Calendar.
That is impossible. Specifically, the 'simple program' part. Nothing that uses Calendar is simple.
The calendar API is horrible; it makes no sense (the first month of the year is... 0, to change values, you have to use int constants, which isn't idiomatic java), and is confused about what it is trying to represent (it's not a calendar, it's a date/time value, or, is it? Is it solarflares time, appointment time, or alarmclock time? It's confused and doesn't know). That's why there is a new API: java.time. Java does not remove stuff even if it is obsolete, because that would break old code. So, the fact that Calendar is still around doesn't mean much.
Use java.time.
I think that it has something to do with my SimpleDateFormat, but I am not sure.
It doesn't. But let's forget about this silly API and use java time instead!
// note that in calendar, january is 0, and that is insane.
// in localdate, it is 1, which is sane.
// thus, assuming you have `month = month - 1;` someplace in your code...
// remove that.
LocalDate date = LocalDate.of(year, month, day);
DayOfWeek day = date.getDayOfWeek();
System.out.println(day);
Wanna go back a day? Okay.
DayOfWeek day = date.minusDays(1).getDayOfWeek();
System.out.println(day);
The reason this fails is that you aren't invoking the method you think you are. There is no SimpleDateFormat#format(int). But SimpleDateFormat extends Format, which declares Format#format(Object). Your int is boxed to Integer and then the relevant code in Format#format(Object) is
if (obj instanceof Date)
return format( (Date)obj, toAppendTo, fieldPosition );
else if (obj instanceof Number)
return format( new Date(((Number)obj).longValue()),
toAppendTo, fieldPosition );
else
throw new IllegalArgumentException("Cannot format given Object as a Date");
At this point, since the value you passed is boxed to Integer which extends Number, the second branch of the if-else is taken and your day-of-week value is converted to a date via the constructor. The number is interpreted as a millisecond value and when you subtracted 1 you changed the time by 1 millisecond, not 1 day.
All that said, you should NOT be using Calendar, use the new date/time API in the java.time package.

Date.getYear() vs. Calendar.get(Calendar.YEAR) - change of implementation. Will it work?

I am asking this question cause actually I have absolutely no way to test this case, and maybe someone could explain it to me :)
I have been working on a piece of code that was written by a person who is very new to programming. This code looks like this:
List<Date> dateList = infoFacade.getDateFrom(documentId);
for(Date from : dateList) {
LocalDate now1 = LocalDate.now();
int year = now1.getYear();
int previousyear = now1.getYear()-1;
int yearfrom = from.getYear()+1900;
if((yearfrom == year )|| (yearfrom == previousyear )){
idoc.setBauinfoArzvon(from);
}
}
I have rewritten it a little bit, so we stop using a deprecated method. It looks like this:
for (Date from : infoFacade.getDateFrom(documentId))
{
cal.setTime(from);
int yearfrom = cal.get(Calendar.YEAR);
if ((yearfrom == LocalDate.now().getYear())
|| (yearfrom == (LocalDate.now().getYear() - 1)))
{
idoc.setDateFrom(from);
}
}
I am worried about all that +1900 or -1900 thing. Should I add or substract something from the yearfrom variable to get the same results as in the code before refactoring?
Assuming you cannot change the return type of infoFacade.getDateFrom() my suggestion would be:
ZoneId zone = ZoneId.systemDefault();
LocalDate now1 = LocalDate.now(zone);
int year = now1.getYear();
int previousYear = year - 1;
List<Date> dateList = infoFacade.getDateFrom(documentId);
for (Date from : dateList) {
int yearfrom = from.toInstant().atZone(zone).getYear();
if (yearfrom == year || yearfrom == previousYear) {
idoc.setBauinfoArzvon(from);
}
}
Both versions of your code implicitly rely on the JVM’s time zone (which is fragile). I have made this dependency explicit. I am reading the default time zone and the current date only once to ensure consistent results. And by converting the Date first to an Instant and then to ZonedDateTime I am avoiding both the deprecated method and the old and outdated Calendar class. And any considerations about whether to add or subtract 1900 or not, which gives clearer code and fewer doubts on the part of the reader.
To answer your question more directly too: No, in your rewritten version of the code you should neither add nor subtract 1900 (or any other number). The code does give the same result. This is because Date uses a “1900-based year” (where 2018 is given as 118, for example), while the also outdated Calendar class numbers the years the same way humans do. My worry is different: If either the default time zone changes while the code is running or (unlikely, but possible) New Year passes, LocalDate.now() will not give the same result each time, so your results will be inconsistent. The JVM’s default time zone can be changed at any time from another part of your program or another program running in the same JVM.
I have written a simple test:
public static void main(String[] args) {
Date date = new GregorianCalendar().getTime();
Calendar cal = new GregorianCalendar();
cal.setTime(date);
System.out.println(cal.get(Calendar.YEAR));
System.out.println((LocalDate.now().getYear() - 1));
System.out.println(LocalDate.now().getYear());
LocalDate now1 = LocalDate.now();
int year = now1.getYear();
int previousyear = now1.getYear()-1;
int yearfrom = date.getYear()+1900;
System.out.println(year);
System.out.println(previousyear);
System.out.println(yearfrom);
}
The output of this test is:
2018
2017
2018
2018
2017
2018
So both code samples are giving the same result.
BUT i will try to use the #Ole V.V. answer tomorrow to see what will happen.

Java: Customize adding 1 month to the current date

I've read around and basically I've figured out that the Calendar object is capable of adding 1 month to a date specified by using something like:
Calendar cal = Calendar.getInstance();
cal.add(Calendar.MONTH, 1);
Although I don't like its behavior whenever the date is on either the 30 or 31. If ever I add 1 month to 01/31/2012, the output becomes 02/29/2012. When I add 1 more month, it becomes 03/29/2012.
Is there anyway I can force 02/29/2012 to become 03/01/2012 automatically?
Basically this is what I want to happen:
Default date: 01/31/2012
Add 1 month: 03/01/2012
Add 1 more month: 03/31/2012
What you are asking for is some implicit knowledge that if the starting date is the last day of the month, and you add 1 month, the result should be the last day of the following month. I.e. the property "last-day-of-month" should be sticky.
This is not directly available in Java's Calendar, but one possible solution is to use Calendar.getActualMaximum(Calendar.DAY_OF_MONTH) to reset the day after incrementing the month.
Calendar cal = ...;
cal.add(Calendar.MONTH,1);
cal.set(Calendar.DAY_OF_MONTH,cal.getActualMaximum(Calendar.DAY_OF_MONTH));
You could even subclass GregorianCalendar and add a method
public Calendar endOfNextMonth() { ... }
to encapsulate the operation.
Well for add 30 days you can do something like this:
public static java.sql.Date sumarFechasDias(java.sql.Date fch, int days) {
Calendar cal = new GregorianCalendar();
cal.setTimeInMillis(fch.getTime());
cal.add(Calendar.DATE, days);
return new java.sql.Date(cal.getTimeInMillis());
}
if days=30, it will return your date with 30 days added.
It looks like you want the calendar to roll up to the beginning of the next month if the date of the next month is smaller than the date of the month before it. Here's how we'd do that:
Calendar cal = Calendar.getInstance();
int oldDay = cal.get(DAY_OF_MONTH);
cal.add(Calendar.MONTH, 1);
// If the old DAY_OF_MONTH was larger than our new one, then
// roll over to the beginning of the next month.
if(oldDay > cal.get(DAY_OF_MONTH){
cal.add(Calendar.MONTH, 1);
cal.set(Calendar.DAY, 1);
}

Is there a good way to get the date of the coming Wednesday?

Is there a good way to get the date of the coming Wednesday?
That is, if today is Tuesday, I want to get the date of Wednesday in this week; if today is Wednesday, I want to get the date of next Wednesday; if today is Thursday, I want to get the date of Wednesday in the following week.
Thanks.
The basic algorithm is the following:
Get the current date
Get its day of week
Find its difference with Wednesday
If the difference is not positive, add 7 (i.e. insist on next coming/future date)
Add the difference
Here's a snippet to show how to do this with java.util.Calendar:
import java.util.Calendar;
public class NextWednesday {
public static Calendar nextDayOfWeek(int dow) {
Calendar date = Calendar.getInstance();
int diff = dow - date.get(Calendar.DAY_OF_WEEK);
if (diff <= 0) {
diff += 7;
}
date.add(Calendar.DAY_OF_MONTH, diff);
return date;
}
public static void main(String[] args) {
System.out.printf(
"%ta, %<tb %<te, %<tY",
nextDayOfWeek(Calendar.WEDNESDAY)
);
}
}
Relative to my here and now, the output of the above snippet is "Wed, Aug 18, 2010".
API links
java.util.Calendar
java.util.Formatter - for the formatting string syntax
tl;dr
LocalDate // Represent a date-only value, without time-of-day and without time zone.
.now() // Capture the current date as seen in the wall-clock time used by the people of a specific region (a time zone). The JVM’s current default time zone is used here. Better to specify explicitly your desired/expected time zone by passing a `ZoneId` argument. Returns a `LocalDate` object.
.with( // Generate a new `LocalDate` object based on values of the original but with some adjustment.
TemporalAdjusters // A class that provides some handy pre-defined implementations of `TemporalAdjuster` (note the singular) interface.
.next( // An implementation of `TemporalAdjuster` that jumps to another date on the specified day-of-week.
DayOfWeek.WEDNESDAY // Pass one of the seven predefined enum objects, Monday-Sunday.
) // Returns an object implementing `TemporalAdjuster` interface.
) // Returns a `LocalDate` object.
Details
Using Java8 Date time API you can easily find the coming Wednesday.
LocalDate nextWed = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.WEDNESDAY));
next(DayOfWeek dayOfWeek) - Returns the next day-of-week adjuster, which adjusts the date
to the first occurrence of the specified day-of-week after the date
being adjusted.
Suppose If you want to get previous Wednesday then,
LocalDate prevWed = LocalDate.now().with(TemporalAdjusters.previous(DayOfWeek.WEDNESDAY));
previous(DayOfWeek dayOfWeek) - Returns the previous day-of-week adjuster, which adjusts
the date to the first occurrence of the specified day-of-week before
the date being adjusted.
Suppose If you want to get next or current Wednesday then
LocalDate nextOrSameWed = LocalDate.now().with(TemporalAdjusters.nextOrSame(DayOfWeek.WEDNESDAY));
nextOrSame(DayOfWeek dayOfWeek) - Returns the next-or-same day-of-week
adjuster, which adjusts the date to the first occurrence of the
specified day-of-week after the date being adjusted unless it is
already on that day in which case the same object is returned.
Edit:
You can also pass ZoneId to get the current date from the system clock in the specified time-zone.
ZoneId zoneId = ZoneId.of("UTC");
LocalDate nextWed = LocalDate.now(zoneId).with(TemporalAdjusters.next(DayOfWeek.WEDNESDAY));
For more information refer TemporalAdjusters
Using JodaTime:
LocalDate date = new LocalDate(System.currentTimeMillis());
Period period = Period.fieldDifference(date, date.withDayOfWeek(DateTimeConstants.WEDNESDAY));
int days = period.getDays();
if (days < 1) {
days = days + 7;
}
System.out.println(date.plusDays(days));
Calendar c= Calendar.getInstance();
c.set(Calendar.DAY_OF_WEEK, Calendar.WEDNESDAY);
c.add(Calendar.DAY_OF_MONTH, 7);
c.getTime();
Use java.util.Calendar. You get the current date/time like this:
Calendar date = Calendar.getInstance();
From there, get date.get(Calendar.DAY_OF_WEEK) to get the current day of week and get the difference to Calendar.WEDNESDAY and add it.
public static void nextWednesday() throws ParseException
{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
int weekday = calendar.get(Calendar.DAY_OF_WEEK);
int days = Calendar.WEDNESDAY - weekday;
if (days < 0)
{
days += 7;
}
calendar.add(Calendar.DAY_OF_YEAR, days);
System.out.println(sdf.format(calendar.getTime()));
}

Getting Day from a Date Object

I wanna see if someDate has any day in it. Am I checking it right?
Calendar cal = Calendar.getInstance();
cal.setTime(someDate); // someDate is a Date
int day = cal.get(Calendar.DAY_OF_MONTH);
if(day == 0){
// code //
}
I'm not sure what you mean by "has any day in it" - all Dates will have a day in them... :-)
Other than that, you probably want the following:
Calendar cal = Calendar.getInstance();
cal.setTime(someDate); // someDate is a Date
int day = cal.get(Calendar.DAY_OF_WEEK);
if(day == Calendar.SUNDAY){
// code //
}
The big change is that you want to get the DAY_OF_WEEK field; what your example does is gets the day within the month (e.g. September 15th would return "15"). Secondly, comparing with Calendar.SUNDAY (or equivalent) is clearer and less error-prone that directly comparing with e.g. 0, even if the code is equivalent.
Every date object will have a day. The day of the month is never going to be 0 though, it will be in the range 1-31. Meaning that your check will always fail.
If I understand correctly you want Calendar.DAY_OF_WEEK.
The answer by Andrzej Doyle is correct.
Just for the heck of it, here is that same kind of code but using the Joda-Time 2.3 library and Java 7.
Unlike java.util.Calendar, Joda-Time sensibly uses 1-based counting. So the days of the week are numbered 1 to 7. Furthermore, Joda-Time uses the standard (ISO 8601) approach where Monday is the first day of the week (1).
// © 2013 Basil Bourque. This source code may be used freely forever by anyone taking full responsibility for doing so.
// import org.joda.time.*;
// import org.joda.time.format.*;
// Specify time zone rather than rely on default.
// Time Zone list… http://joda-time.sourceforge.net/timezones.html (not quite up-to-date, read page for details)
DateTimeZone timeZone = DateTimeZone.forID( "Europe/Paris" );
DateTime now = new DateTime( timeZone );
if( now.dayOfWeek().get() == DateTimeConstants.MONDAY ) {
System.out.println( "Today is a Monday." );
} else {
System.out.println( "Nope, today is some other day of week." );
}

Categories

Resources