In the JSR-310 java.time API in JDK 8, what are the rules for calculating the result of adding a month to a date. In particular, what happens when you add 1 month to a date like January 31st?
LocalDate initial = LocalDate.of(2012, 1, 31); // 31st January 2012
LocalDate result = initial.plusMonths(1);
// what is the result?
Short answer:
In the example, the result will be the last day of February, 2012-02-29.
Explanation:
The question, "what date do you get if you add a month", is one which could be open to interpretation. To avoid this, the java.time API has a clear rule. The result will have the same day-of-month as the input, unless that would be an invalid date, in which case the result is the last day of the month.
Thus, the 31st January plus one month would result in the 31st February, but since that is an invalid date, the result is the last valid date in February, which is 28th or 29th February depending on whether it is a leap year:
// normal case
2011-01-15 plus 1 month = 2011-02-15 // 15 Jan -> 15 Feb
// special rule choosing the last valid day-of-month
2011-01-31 plus 1 month = 2011-02-28 // 31 Jan -> 28 Feb (2011 is normal year)
2012-01-31 plus 1 month = 2012-02-29 // 31 Jan -> 29 Feb (2012 is leap year)
// same rule applies for months other than February
2013-03-31 plus 1 month = 2013-04-30 // 31 Mar -> 30 Apr (only 30 days in April)
The same rule applies whether adding one month or many months and is always based on the resulting month. ie. the month is added first (adjusting the year if necessary), and only then is the day-of-month considered. The same rule also applies when subtracting.
// multiple months works on the month of the result
2013-10-31 plus 4 months = 2014-02-28 // last day of February
2013-10-31 minus 4 months = 2013-06-30 // last day of June
The same rules also apply when adding/subtracting years to/from a date - the years are added, and only then is the day-of-month checked for validity within the month.
// years use the same rule
2012-02-29 plus 1 year = 2013-02-28 // 29th February invalid so adjusted to 28th
If your business logic needs a different rule for month addition, the best approach is to write a TemporalAdjuster or TemporalAmount that packages up your special logic.
Related
I am looking to build up a function that return an array with all week numbers of the previous months in a year that are the same week number of one particular month.
I am using as first day of week Monday and I am taking as first week of month week with the first Monday of current month.
Input: week of year and year. For example, 27 and 2019. The first week of July (7).
Output: array of week of months. For example, [2, 6, 10, 14, 19, 23, 27].
What I try:
private void getResult(int weekYear)
{
LocalDate date = LocalDate.now();
final int weekNumber = 27;
LocalDate newDate = date.with(IsoFields.WEEK_OF_WEEK_BASED_YEAR, weekNumber);
int month = newDate.getMonthValue();;
int weekMonth = LocalDate.from(newDate).get(WeekFields.ISO.weekOfMonth());
System.out.println(newDate);
System.out.println(month);
System.out.println(weekMonth);
ArrayList<Integer> list = new ArrayList<Integer>();
for (int i = 1; i <= month; i++)
{
LocalDate tempDate = date.withYear(2019).withMonth(i).with(WeekFields.ISO.weekOfMonth(), weekMonth);
int tempYear = LocalDate.from(tempDate).get(WeekFields.ISO.weekOfWeekBasedYear());
list.add(tempYear);
}
list.forEach((e) -> System.out.print(e + " "));
}
int weekYear = 27;
getResult(weekYear);
What I get: [1 6 10 14 18 23 27].
What I am looking for: I have two question:
First one: the results obtained are different from those expected. I think the problem is due to the fact that I didn't specify how to calculate the first week of the month (first Monday of the month). Is it right? How can I solve that?
Second one: What is a better solution?
The key here is understanding a few points:
You are numbering weeks in two different ways. For the week of year you are using ISO numbering: the first week of the year is the one that includes at least 4 days of the new year. For week of month you are counting the Mondays (you may say that the first week of the month is the one that includes seven days of the month, not four).
The week number may not always exist. If your starting point is in 0th or the 5th week of the month, a preceding month may not have that week in it.
the results obtained are different from those expected. I think the
problem is due to the fact that I didn't specify how to calculate the
first week of the month (first Monday of the month). Is it right? How
can I solve that?
You are correct. To count the Mondays of the month you may use:
LocalDate tempDate = date.withYear(2019)
.withMonth(i)
.with(ChronoField.DAY_OF_WEEK, DayOfWeek.MONDAY.getValue())
.with(ChronoField.ALIGNED_WEEK_OF_MONTH, weekMonth);
(DayOfWeek.MONDAY.getValue() is just of wordy way of saying 1, of course, but conveys the intention better, so I prefer it.)
With this change to your code the output is the expected:
2 6 10 14 19 23 27
The key is ChronoField.ALIGNED_WEEK_OF_MONTH. The aligned weeks of a month start from the 1st of the month and are always 7 days regardless of the days of the week. The first aligned week is from the 1st through the 7th of the month, the 2nd aligned week if from 8th through 14th, etc. Since we have set the day of week to Monday, setting the aligned week to 1 gives us the 1st Monday of the month, etc.
We’re not done yet. If I set weekNumber to 40, I get:
2 6 10 14 14 23 27 27 36 41
I had expected 40 to be the last number in the list, but it is not there. Week 40 of 2019 is from Monday September 30 through October 6, so if I understand correctly you want the 5th week of those months that have a 5th week. This brings us back to the issue of not all month having a week 5 (because they don’t have 5 Mondays). What happened was that since I ran your code on a Tuesday, it took Tuesday in week 40, which is October 1, as a starting point, and therefore gave me the 1st rather than the 5th week of every month.
are there better solutions? Can you suggest one?
I can’t really. What you’ve got is fine.
Only you’re not using the int weekYear parameter. You may want to use it in place of your weekNumber local variable. In any case you should delete one of them and use the other.
And this unrelated tip: Your use of LocalDate.from(someLocalDate) is redundant since it just gives you the same LocalDate again (either the same object or an equal one, I don’t know or care). Just use someLocalDate in those situations.
I'm trying to set minimal value of WEEK_OF_MONTH field as follows:
calendar.set(WEEK_OF_MONTH, calendar.getActualMinimum(WEEK_OF_MONTH));
The call to
calendar.getActualMinimum(WEEK_OF_MONTH)
returns 0
But at calculation during get* operations this field becomes 5.
Moreover, without leniency mode, I get
java.lang.IllegalArgumentException: WEEK_OF_MONTH: 0 -> 5 // or MONTH: 9 -> 8
at java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2829)
at java.util.Calendar.updateTime(Calendar.java:3393)
at java.util.Calendar.complete(Calendar.java:2265)
at java.util.Calendar.get(Calendar.java:1826)
at Main.main(Main.java:19)
If I set WEEK_OF_MONTH = 1, then I get it correctly as 1.
Check out an example
Can anyone clarify such a behavior? Thanks in advance.
java.time
Locale russia = Locale.forLanguageTag("ru-RU");
WeekFields wf = WeekFields.of(russia);
LocalDate date = LocalDate.now(ZoneId.of("Europe/Moscow"));
int minimumWeekOfMonth = date.with(TemporalAdjusters.firstDayOfMonth()).get(wf.weekOfMonth());
System.out.println("Minimum week of month: " + minimumWeekOfMonth);
LocalDate dateInFirstWeekOfMonth = date.with(wf.weekOfMonth(), minimumWeekOfMonth);
System.out.println("Date in first week of month: " + dateInFirstWeekOfMonth);
When running this snippet just now I got the following output:
Minimum week of month: 1
Date in first week of month: 2018-10-05
I have assumed that you are in Russian locale. Russia uses the international week numbering where Monday is the first day of the week and week one of a year or month is the first week that contains at least 4 days of the year or month. So week 1 of October 2018 was from Monday October 1 through Sunday October 7. This in turn means that the minumum week in this month is 1. Starting out from today (a Friday) and setting the week of month to 1 gives Friday in week 1, that is, Friday October 5.
If I start out from Wednesday September 12 instead I get:
Minimum week of month: 0
Date in first week of month: 2018-08-29
Week 1 of September was from Monday September 3 through September 9. This means that September 1 and 2 were in week 0, so 0 is the minimum week of month for September. And when starting from a Wednesday I set week number to 0, I get the Wednesday of that week, which happens to lie in August: August 29. If we ask for the week of month of that date, do we get 0?
System.out.println("Week of month: " + dateInFirstWeekOfMonth.get(wf.weekOfMonth()));
Output:
Week of month: 5
Since the date is in August, we now get which week of August the date is in, which happens to be week 5.
What happened in your code?
It seems to me that GregorianCalendar.getActualMinimum(Calendar.WEEK_OF_MONTH) always returns 0. I cannot make sense of this observation. Since Russia uses the Gregorian calendar, an instance of GregorianCalendar is what you really get from Calendar.getInstance.
I wouldn’t want to bother. As I said in a comment already, the Calendar class is long outdated and has a range of design problems with it, so I recommend you don’t use it. I’d certainly prefer java.time, the modern Java date and time API, any time.
Link
Oracle tutorial: Date Time explaining how to use java.time.
I expect these two formatters to be equivalent:
DateTimeFormatter fromBuilder = new DateTimeFormatterBuilder()
.appendValue(IsoFields.WEEK_BASED_YEAR, 4)
.appendLiteral('-')
.appendValue(IsoFields.WEEK_OF_WEEK_BASED_YEAR, 2)
.toFormatter();
DateTimeFormatter fromPattern = DateTimeFormatter.ofPattern("YYYY-ww");
But they do not give the same result:
LocalDate date = LocalDate.of(2017, 1, 1);
System.out.printf("from builder: %s%n", fromBuilder.format(date)); // prints 'from builder: 2016-52'
System.out.printf("from pattern: %s%n", fromPattern.format(date)); // prints 'from pattern: 2017-01'
What am I missing?
The Y and w patterns correspond to a localized version of week-fields, using the JVM's default locale (java.util.Locale). The second formatter is equivalent to:
// localized week fields (using default Locale)
WeekFields weekFields = WeekFields.of(Locale.getDefault());
// equivalent to YYYY-ww
DateTimeFormatter fmt = new DateTimeFormatterBuilder()
.appendValue(weekFields.weekBasedYear(), 4)
.appendLiteral('-')
.appendValue(weekFields.weekOfWeekBasedYear(), 2)
.toFormatter();
As this is locale dependent, it can or can't work like IsoFields. The WeekFields created above will have a different behaviour depending on the JVM's default locale.
IsoFields, on the other hand, follows ISO-8601 definition to define the week-based fields, as described in the javadoc:
The first week of a week-based-year is the first Monday-based week of the standard ISO year that has at least 4 days in the new year.
If January 1st is Monday then week 1 starts on January 1st
If January 1st is Tuesday then week 1 starts on December 31st of the previous standard year
If January 1st is Wednesday then week 1 starts on December 30th of the previous standard year
If January 1st is Thursday then week 1 starts on December 29th of the previous standard year
If January 1st is Friday then week 1 starts on January 4th
If January 1st is Saturday then week 1 starts on January 3rd
If January 1st is Sunday then week 1 starts on January 2nd
As 2017-01-01 is a Sunday, it corresponds to the last line above: week 1 starts on January 2nd 2017, so January 1st 2017 is still in the last week of 2016.
You can check how your WeekFields instance differs from IsoFields by calling the methods getFirstDayOfWeek() and getMinimalDaysInFirstWeek() - which are used to calculate the values of the respecitive week-based fields:
A week is defined by:
The first day-of-week. For example, the ISO-8601 standard considers Monday to be the first day-of-week.
The minimal number of days in the first week. For example, the ISO-8601 standard counts the first week as needing at least 4 days.
Together these two values allow a year or month to be divided into weeks.
In the JVM I'm using, the default locale is pt_BR, and the WeekFields created has the first day-of-week as Sunday, and minimal days in first week as 1. Check yours and you'll see that it also differs from IsoFields.
You can check ISO's definition by using the constant WeekFields.ISO: getFirstDayOfWeek() returns Monday and getMinimalDaysInFirstWeek() returns 4.
Also, remind that there's a small difference between IsoFields and WeekFields.ISO. Quoting JodaStephen's comment in this thread:
The only observable difference was that WeekFields operates on all calendar systems (by converting to ISO) whereas IsoFields only operates on ISO (and rejects other calendar systems)
I need to get the date by passing these parameters
year
week number (in a month) i.e. 1,2,3,4,5
day number (in a week) 0 (Sunday) to 6 (Saturday)
Month
I looked for a constructor in Calendar class but does not contain these parameters.
In spite of your tags I agree with Joe C’s comment, you should prefer the modern Java date and time API (AKA known as java.time or JSR-310) over the long outdated Calendar and friends. The modern classes are more programmer friendly and nicer to work with, they come with fewer surprises, lead to clearer and more natural code (at least when you get used to the fluent style) and are easier to debug should you make a bug.
I don’t know how you count your weeks of the month. I have assumed that the first week starts on the first of the month and lasts 7 days (so the second week starts on the 8th and the 5th week on the 29th day of the month). If you count differently, please see Hugo’s answer.
I suggest this method:
public static LocalDate weekNoAndDayToDate(Year year, Month month, int weekOfMonth, int dayOfWeek) {
// find day of week: convert from 0 (Sunday) to 6 (Saturday)
// to DayOfWeek, from 1 (Monday) to 7 (Sunday)
DayOfWeek dow;
if (dayOfWeek == 0) { // Sunday
dow = DayOfWeek.SUNDAY;
} else {
dow = DayOfWeek.of(dayOfWeek);
}
return year.atMonth(month)
.atDay(1)
.with(TemporalAdjusters.nextOrSame(dow))
.with(ChronoField.ALIGNED_WEEK_OF_MONTH, weekOfMonth);
}
With this we may do for example
LocalDate today = weekNoAndDayToDate(Year.of(2017), Month.JULY, 1, 1);
This yields
2017-07-03
If you need either a Date or a GregorianCalendar, for example for an old API that you cannot change, do one of the following:
Date oldfashiondDateObject
= Date.from(today.atStartOfDay(ZoneId.systemDefault()).toInstant());
GregorianCalendar oldfashionedCalendarObject
= GregorianCalendar.from(today.atStartOfDay(ZoneId.systemDefault()));
In both cases the result will be different in different time zones (one of the inherent troubles of the old classes). On my computer the first yields a Date of
Mon Jul 03 00:00:00 CEST 2017
The second yields a GregorianCalendar equal to the same point in time.
To create date from year, week number and week's day use java.util.Calendar instance:
Calendar cal = Calendar.getInstance();
cal.set(Calendar.YEAR, 2017);
cal.set(Calendar.WEEK_OF_YEAR, 26);
cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
To convert from Calendar to java.util.Date :
Date date = cal.getTime();
To convert Date into java.time.LocalDateTime use :
LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
As #Ole V.V. explained, you need to define in what day the week starts, and how many days it must have to be considered the first week.
For example, the ISO-8601 standard considers:
Monday to be the first day-of-week.
The minimal number of days in the first week: the standard counts the first week as needing at least 4 days
The month is divided into periods where each period starts on the defined first day-of-week. The earliest period in the same month is referred to as week 0 if it has less than the minimal number of days and week 1 if it has at least the minimal number of days.
Depending on how you define those, you can have different results. Consider the calendar for July 2017:
July 2017
Su Mo Tu We Th Fr Sa
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
If we consider ISO's definition, we have:
week zero - 2017-07-01 to 2017-07-02
week 1: from 2017-07-03 to 2017-07-09
week 2: from 2017-07-10 to 2017-07-16
week 3: from 2017-07-17 to 2017-07-22
week 4: from 2017-07-23 to 2017-07-30
week 5: 2017-07-31
If we consider first day of week as Sunday and minimal number of days in the first week as 1, we have:
week 1: 2017-07-01
week 2: from 2017-07-02 to 2017-07-08
week 3: from 2017-07-09 to 2017-07-15
week 4: from 2017-07-16 to 2017-07-21
week 5: from 2017-07-22 to 2017-07-29
week 6: from 2017-07-30 to 2017-07-31
As a solution with Calendar was already posted, I'm gonna write one using the new API. If you're using Java 8, consider using the new java.time API. It's easier, less bugged and less error-prone than the old APIs.
If you're using Java <= 7, you can use the ThreeTen Backport, a great backport for Java 8's new date/time classes. And for Android, there's the ThreeTenABP (more on how to use it here).
The code below works for both.
The only difference is the package names (in Java 8 is java.time and in ThreeTen Backport (or Android's ThreeTenABP) is org.threeten.bp), but the classes and methods names are the same.
I'm using a DateTimeFormatter because it takes all the fields (month, year, day of week and week of month) and resolves the day accordingly, creating a LocalDate (which has the day/month/year fields). I'm also using the WeekFields class, which can be configured to use different week definitions (first day and minimal number of days in first week)
There's also a little adjustment to consider Sunday as zero:
public LocalDate getDate(int weekOfMonth, int dayOfWeek, int month, int year) {
// you can customize your week definition (first day of week and mininum days in first week)
WeekFields wf = WeekFields.of(DayOfWeek.SUNDAY, 2);
DateTimeFormatter fmt = new DateTimeFormatterBuilder()
// week of month, using week definition from WeekFields
.appendValue(wf.weekOfMonth()).appendPattern(" ")
// day of week
.appendValue(ChronoField.DAY_OF_WEEK)
// month and year
.appendPattern(" M/yyyy")
// create formatter
.toFormatter();
return LocalDate.parse(weekOfMonth + " " +
// Sunday is 0, adjusting value
(dayOfWeek == 0 ? 7 : dayOfWeek) + " " + month + "/" + year, fmt);
}
Using this code (week starts on Sunday, and 2 days are required to be considered the first week - otherwise week will be zero as in the first example above):
LocalDate d = getDate(1, 6, 7, 2017);
d will be 2017-07-08 (Saturday in the week 1 of July 2017).
If you want to use ISO 8601 definition, use the constant WeekFields.ISO instead of using WeekFields.of() method.
As #Ole V.V. suggested in the comments, it can also be done without creating a formatter: get the first dayOfWeek of the month and adjust it to the desired weekOfMonth:
public LocalDate getDate(int weekOfMonth, int dayOfWeek, int month, int year) {
// you can customize your week definition (first day of week and mininum days in first week)
WeekFields wf = WeekFields.of(DayOfWeek.SUNDAY, 2);
// Sunday is 0, adjusting value
DayOfWeek dow = DayOfWeek.of(dayOfWeek == 0 ? 7 : dayOfWeek);
// get the first weekday of the month
LocalDate first = LocalDate.of(year, month, 1).with(TemporalAdjusters.nextOrSame(dow));
// check in which week this date is
int week = first.get(wf.weekOfMonth());
// adjust to weekOfMonth
return first.plusWeeks(weekOfMonth - week);
}
This works exactly the same way, but without the need of a formatter - I've tested with dates from year 1600 to 2100 and it gets the same results.
PS: Calendar also has a similar configuration via the methods setFirstDayOfWeek() and setMinimalDaysInFirstWeek(). You can check the default configuration calling the respective get methods.
I have an application that asks a user to input a start date. Then the user will go through a series of 5 pages and input data to be exported to an excel sheet with page 1 being on the first block with the date day 2 being on the second block with a incremented date etc etc. Is there a any type of function available to accomplish this or am I doomed to the use of if statements to increment the date correctly. I tried to increment the date by simply returning the date in a series of variables
int day = getIntent().getExtras().getInt("day");
int year = getIntent().getExtras().getInt("year");
String month = getIntent().getStringExtra("month");
String dayofweek = getIntent().getStringExtra("dayofweek");
And the I would output those variables in the appropriate cells for the excel sheet. Monday would be just the variables since thats the start date. Tuesday would be the day + 1. Wednesday would be the day + 1 to continuously increment it to day 5. The problem I ran into is it the start day is selected as the last day of the month it would continue to increment the day ie Jan 31 Jan 32 Jan 33 Jan 34 etc etc. So I have to take the end of the month, end of the year, and leap year in 2016 and 2020 into account. On top of weekends. For example if Tuesday is selected I want the start date to show up on tuesday the fourth date would show up on friday and then the next correct date on monday again.
So I guess the short of it all would be Is there a better way to implement this?
You have to make a date or calendar object to increment....
1 for month is February. The 30th of February is changed to 1st of March. You should set 0 for month. The best is to use the constant defined in Calendar:
c1.set(2000, Calendar.JANUARY, 30);
Then you can increment it
c1.add(Calendar.DATE, 1);
Date newDate = c1.getTime();
You will get new date object of the next day :)