SimpleDateFormat Week Calculations - java

I'm getting some puzzling results with SimpleDateFormat and am hoping that someone can shed some light on the issue. The output:
Time = Mon Dec 27 00:00:00 PST 2010
2010-01 <--- THIS IS WHAT I DON'T UNDERSTAND
Start of week = Sun Dec 26 00:00:00 PST 2010
2010-01
End of Week = Sat Jan 01 23:59:59 PST 2011
2011-01
Should I be treating the last "week" of the year that extends to the next year as a special case? Or is this the correct way to interpret this? Obviously when attempting to organize week sequentially, the order is incorrect. Adjusting the initial values, Dec 25, 2005 is considered the 53rd week. I haven't looked at Joda yet to see if Joda produces similar results.
The relevant code:
private static Date getStartOfWeek( Date d ) {
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.setTime( d );
calendar.set( Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek() );
return calendar.getTime();
}
private static Date getEndOfWeek( Date d ) {
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.setTime( d );
calendar.add( Calendar.WEEK_OF_YEAR, 1 );
calendar.set( Calendar.DAY_OF_WEEK, calendar.getFirstDayOfWeek() );
calendar.add( Calendar.MILLISECOND, -1 );
return calendar.getTime();
}
Calendar calendar = Calendar.getInstance();
calendar.clear();
calendar.set( 2010, Calendar.DECEMBER, 27 );
Date d = calendar.getTime();
Date start = getStartOfWeek( d );
Date end = getEndOfWeek( d );
SimpleDateFormat fmt = new SimpleDateFormat( "yyyy-ww" );
System.out.println( "Time = " + d );
System.out.println( fmt.format( d ) );
System.out.println( "Start of week = " + start );
System.out.println( fmt.format( start ) );
System.out.println( "End of Week = " + end );
System.out.println( fmt.format( end ) );
Background: I found this when using the crosstab (date grouped into week) in JasperReports.
EDIT: I am using JDK 1.6.0_25
EDIT: It seems that I will have to use Joda to get the correct result. TO get the week start/end, I ended up using: LocalDate.withDayOfWeek. To retrieve the year and week number, I used DateTime.getWeekyear and DateTime.getWeekOfWeekyear.

The bug is in your formatting code, not Java.
The surprising behavior is due to an esoteric rule in date notation. Note that ISO 8601 (rather confusingly) specifies different rules for year boundaries when using week numbers. In particular, 2010-12-27 is considered part of 2011 when using week numbers.
As a result, you should be using the "week year" YYYY rather than the usual year yyyy. (See http://download.oracle.com/javase/7/docs/api/java/util/GregorianCalendar.html#week_year and the last example in http://download.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html.)
Also, the standard notation for dates uses an explicit 'W', so you should use new SimpleDateFormat( "YYYY-'W'ww" ) instead.
Edit: There's another problem. Java seems to default to the non-standard calendar.getMinimalDaysInFirstWeek() == 1, so you have to set
calendar.setMinimalDaysInFirstWeek( 4 );
in order to get the correct year.
Edit: From reading the Calendar javadocs, you might also need to set the starting day to Monday. Further, the YYYY format specifier seems to be new in Java 1.7. In light of this, unless you're willing to upgrade to a pre-release Java version, I recommend just using Joda Time.

From ISO Week
The first week of a year is the week that contains the first Thursday
of the year.
So the behavior has nothing to do with Java or Joda. This is how the "week of the year" is implemented worldwide (if they follow the ISO standard)

the standadr java Date & Time classes are not well designed and sometimes don't work as expected. use instead Joda Time

Yes, this looks to be a bug in Java, and not in the formatting code. If you print:
System.out.println("Week = " + calendar.get(Calendar.WEEK_OF_YEAR));
It also shows 1.
If you change your date to something not so close to the end of the year, eg:
calendar.set(2010, Calendar.NOVEMBER, 27);
Then the output looks right. BTW, I tested using Sun 1.6.0_25 64bit VM.

Related

Java: Easiest Way to Subtract Dates

I have created a class with two fields that need to be dates, start_date and date_passed. I have been researching the best way in java to have dates in a YYYY MM DD format that allows for easy date subtraction, and the ability to "make-up" a date, say in the future for example.
Example of what I'd like it to do...
library.circulate_book("Chemistry", **start date here**) //actual date or random date
library.pass_book("Chemistry", **Date Passed here**) //random date such as 5 days after start date
int days_had = Date_Passed - start_date
So far, I've found plenty of ways to format dates using Calendars and Date classes, but have yet to find one that looks like it would work considering most dates end up as Strings. Any suggestions/small examples are greatly appreciated! Also, any links to examples would be awesome!
tl;dr
To move from one date to another by adding/subtracting a number of days.
LocalDate.now(
ZoneId.of( "Pacific/Auckland" )
)
.minusDays( 5 )
To calculate the number of days, months, and years elapsed between two dates.
ChronoUnit.DAYS.between( start , stop )
Parsing
First you must parse your string inputs into date-time objects. Then you work on preforming your business logic with those objects.
Stop thinking of date-time values as strings, that will drive you nuts. We work with date-time objects in our code; we exchange data with users or other apps using a String representation of that date-time object.
In Java 8 and later, use the java.time framework. See Tutorial.
You want only a date, without time-of-day, so we can use the LocalDate class.
That funky double-colon syntax is a method reference, a way to say what method should be called by other code.
String input = "2015 01 02";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern ( "yyyy MM dd" );
LocalDate localDate = formatter.parse ( input , LocalDate :: from );
Current date
Determining today’s date requires a time zone. For any given moment, the date varies around the globe by zone.
ZoneId z = ZoneId.of( "Africa/Tunis" ) ;
LocalDate todayTunis = LocalDate.now( z ) ;
If you want the JVM’s current default time zone, call ZoneId.systemDefault.
Subtracting Dates
This has been addressed many times before on StackOveflow.com. For example, How to subtract X days from a date using Java calendar?. For details, see other Answers such as this one by me and this one by me for more details. Tip: "elapsed" is a key search word.
Use ChronoUnit.DAYS enum to calculate count of days elapsed.
LocalDate weekLater = localDate.plusDays ( 7 );
long daysElapsed = java.time.temporal.ChronoUnit.DAYS.between( todayTunis , weekLater ) ;
Dump to console.
System.out.println ( "localDate: " + localDate + " to " + weekLater + " in days: " + daysElapsed );
localDate: 2015-01-02 to 2015-01-09 in days: 7
The best way to do this in Java-8 is not the flawed answer of Basil Bourque but this approach:
String startDate = "2016 01 02";
String passedDate = "2016 02 29";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd");
LocalDate date1 = LocalDate.parse(startDate, formatter);
LocalDate date2 = LocalDate.parse(passedDate, formatter);
long elapsedDays = ChronoUnit.DAYS.between(date1, date2);
System.out.println(elapsedDays); // 58 (correct)
The suggested use of java.time.Period.getDays() is dangerous and often wrong as soon as the elapsed duration exceeds one month. The whole Period is P1M27D so this code effectively only queries the partial amount of elapsed days (there is also an elapsed month):
System.out.println(Period.between(date1, date2).getDays()); // 27 (WRONG!!!)
A satisfying solution using the classes java.util.Date, GregorianCalendar etc. is hard to find. You can use the answer of Tacktheritrix but have to be aware of the fact that the calculated count of elapsed days might differ due to the sub-day-parts of java.util.Date and is also not reliable because of ignoring day-light-saving switches (where the clock jumps by one hour in many parts of the world).
Side note: At least 8 external libraries offer good answers to your problem, too. But I think, your simple use-case does not justify the embedding of an extra library unless you are not yet on Java-8. Any alternative solution how to count the elapsed days between two dates would not be easier than in Java-8 - only similar. And since you accepted the Java-8-related answer of Basil Bourque, I assume that you are indeed on Java-8 so I leave out the answer how to solve your problem with other libraries.
Use the Java 8 Date API or Joda, no need for new inventions.
You can find some examples here: http://examples.javacodegeeks.com/core-java/java-8-datetime-api-tutorial/
Here's an answer using Calendar:
Calendar cal = Calendar.getInstance();
Calendar cal2 = Calendar.getInstance();
cal2.setTime(cal.getTime());
cal2.add(Calendar.DAY_OF_YEAR, 5);
System.out.println((cal2.getTimeInMillis() - cal.getTimeInMillis()) / (1000d * 60 * 60 * 24));
LocalDate startDate = LocalDate.of(2009, 9, 1);
LocalDate today = LocalDate.now();
Period p = Period.between(startDate, today);
System.out.println("Years " + p.getYears());
System.out.println("Months " + p.getMonths());
System.out.println("Days " + p.getDays());
If you are stuck withan older java you can use SimpleDateFormat.
//For substraction
long differenceInMillis = date1.getTime() - date2.getTime();
//Date Format
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy MM dd");
String dateAsString = dateFormat.format(date1); //To get a text representation
Date dateParsed = dateFormat.parse(dateAsString); //To get it back as date

Why is my printed date wrong?

I have verified that the date is read correctly from a file, but once I use SimpleDateFormat.format with the pattern "dd/MM/yy" it suddenly adds a month. This leads me to believe lenient mode is calculating the wrong value. But I have no idea what would make it add a full month.
Some example dates I read:
16/09/2013
23/09/2013
30/09/2013
07/10/2013
14/10/2013
21/10/2013
The code used to parse the date (it's a wrapper around Calendar I made):
public static SimpleDateTime parseDate(String date)
{
String[] dateParts = date.split("[-\\.:/]");
int day = Integer.parseInt(dateParts[0]);
int month = Integer.parseInt(dateParts[1]);
int year = Integer.parseInt(dateParts[2]);
return new SimpleDateTime(dag, maand, jaar);
}
The constructor used here:
public SimpleDateTime(int day, int month, int year)
{
date = Calendar.getInstance();
date.setLenient(true);
setDay(day);
setMonth(month);
setYear(year);
}
The setters for day, month and year:
public void setYear(int year)
{
date.set(Calendar.YEAR, year);
}
public void setMonth(int month)
{
date.set(Calendar.MONTH, month);
}
public void setDay(int day)
{
date.set(Calendar.DAY_OF_MONTH, day);
}
And the code used to format the date:
public String toString(String pattern)
{
String output = new SimpleDateFormat(pattern, Locale.getDefault()).format(date.getTime());
return output;
}
where the pattern passed is:
"dd/MM/yy"
Intended to print a date as:
16/09/13
23/09/13
Instead I get:
16/10/13
23/10/13
January is 0 in Java; February is 1 and so on.
See Calendar.JANUARY, Calendar.FEBRUARY.
So when you're reading 1 from the file
you think you read JAN but you read FEB.
You should do: date.set(Calendar.MONTH, month-1); to fix this.
Months are indexed from 0 not 1 so 10 is November and 11 will be December.
Calendar.MONTH
From documentation:
Field number for get and set indicating the month. This is a calendar-specific value. The first month of the year is JANUARY; the last depends on the number of months in a year.
So if you check JANUARY you see it starts in zero.
Make sure your month is in the interval 0-11. Possibly it is in 1-12.
The reason for this is that the counting starts at 0.
January == 0
February == 1
and so on. See the documentation.
THe problem is that you pass 9 to SimpleDateFormat and since month are indexed from 0 to 11 it will parse month '9' as the 10th month.
You need to subtract 1 from the month :)
Calendar class in Java holds months starting from 0, hence when you set the month as 0, it would consider it as January. SimpleDateFormat provides for a way to correctly display the value as 01.
Calendar cal = Calendar.getInstance();
cal.set(Calendar.MONTH, 0);
System.out.println(new SimpleDateFormat("dd/MM/yy").format(cal.getTime()));
Output:
29/01/14
The workaround for you to align you file that Calendar can work with (since December - or 12 would trickle over to the next year) or modify your logic to pick Constants like:
cal.set(Calendar.MONTH, Calendar.JANUARY);
The answer by peter.petrov is almost correct, except for one major problem. Like your question, it neglects to account for time zone.
For your information, this kind of work is much easier in Joda-Time (or new java.time.* classes in Java 8). Joda-Time is so much cleaner you won't even feel the need to create a wrapper class.
// Specify the time zone for which the incoming date is intended.
DateTimeZone timeZone = DateTimeZone.forID( "Europe/Brussels" );
String input = "16/09/2013";
DateTimeFormatter formatter = DateTimeFormat.forPattern("dd/MM/yyyy").withZone( timeZone );
DateTime dateTime = formatter.parseDateTime( input );
String output = formatter.print( dateTime );
Dump to console…
System.out.println( "dateTime: " + dateTime );
System.out.println( "output: " + output );
System.out.println( "millis since Unix epoch: " + dateTime.getMillis() );
When run…
dateTime: 2013-09-16T00:00:00.000+02:00
output: 16/09/2013
millis since Unix epoch: 1379282400000

Getting the right day of the week (Java Calendar)

I manage some dates in my own date classes. I want to create a method like this in my date:
int getWeekdayIndex()
right now it lookes something like this:
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.set(year, month+1, day); // data of this date
int weekdayIndex = cal.get(Calendar.DAY_OF_WEEK);
return weekdayIndex;
I know DAY_OF_WEEK returns 1(Sunday) to 7(Saturday), but if I test this method with
2014-01-06 I get index 5 (suppost to be 2)
2014-01-07 I get index 6 (suppost to be 3)
(I live in Berlin UTC+01:00 - if it matters)
You should use:
cal.set(year, month-1, day); // data of this date
Do note the month-1 part instead of month+1.
Assuming that month's value is:
1 for Jan
2 for Feb
3 for Mar
...
What is month in your scenario? If it's 0 for "January", you are adding 1 when calling the set method. But months in Java are zero-based, so you're setting "February", and February 6th, 2014 is a Thursday (5), and February 7th, 2014 is a Friday (6).
month - the value used to set the MONTH calendar field. Month value is 0-based. e.g., 0 for January.
Try changing
cal.set(year, month+1, day);
to
cal.set(year, month, day);
FYI, Joda-Time 2.3 has convenient methods for such work.
The DateTime class offers a dayOfWeek method. From that you can derive either the localized name of the day, and to your needs, the number. Joda-Time counts days using the international standard ISO 8601, where Monday is 1 and Sunday is 7.
Example code…
// © 2013 Basil Bourque. This source code may be used freely forever by anyone taking full responsibility for doing so.
// import org.joda.time.*;
DateTimeZone berlinTimeZone = DateTimeZone.forID( "Europe/Berlin" );
DateTime dateTime = new DateTime( 2013, DateTimeConstants.DECEMBER, 13, 14, 15, 16, berlinTimeZone );
String dayOfWeekAsText = dateTime.dayOfWeek().getAsText( Locale.GERMANY );
int dayOfWeekAsNumber = dateTime.dayOfWeek().get();
System.out.println( "dayOfWeekAsText: " + dayOfWeekAsText );
System.out.println( "dayOfWeekAsNumber: " + dayOfWeekAsNumber );
When run…
dayOfWeekAsText: Freitag
dayOfWeekAsNumber: 5
Easy to jump to another month.
DateTime previousMonthDateTime = dateTime.minusMonths( 1 );
DateTime nextMonthDateTime = dateTime.plusMonths( 1 );

Get the week start and end date given a current date and week start

If possible I would prefer a joda or non-joda solution for the scenario below
Lets say if my week starts on 02/05/2012 and the given current date is 02/22/2011. I need to calculate the week start and end date for the given current date. So my solution should have the week start as 02/19 and week ends at 02/25.
For simplicity, I have set my week start here as 02/05/2011 but it could be any day potentially and my week always has 7 days.
My existing code is below but doesnt seem to work as expected.
public Interval getWeekInterval(Date calendarStartDate, Date date)
{
Calendar sDate = Calendar.getInstance();
sDate.setTime(getMidnightDate(calendarStartDate));
Calendar eDate = Calendar.getInstance();
eDate.setTime(date);
Calendar weekStartDate = (Calendar) sDate.clone();
logger.debug("Date:" + sDate.getTime());
while (sDate.before(eDate)) {
weekStartDate = sDate;
sDate.add(Calendar.DAY_OF_WEEK_IN_MONTH, 1);
}
return new Interval(weekStartDate.getTime(), sDate.getTime());
}
Defining A Week
If you are using date-time objects, you should define a week as up to but not including the first moment of the day after the end of week. As seen in this diagram.
This approach is known as Half-Open. This approach is commonly used for working with spans of time.
The reason is because, logically, that last moment of the day before the new day is infinitely divisible as a fraction of a second. You may think that using ".999" would handle that for milliseconds, but then you'd mistaken when writing for the new java.time.* classes in Java 8 that have nanosecond resolution rather than millisecond. You may think think that using ".999999999" would handle that case, but then you’d be mistaken when handling date-time values from many databases such as Postgres that use microsecond resolution, ".999999".
In the third-party open-source Joda-Time library, this Half-Open logic is how its Interval class works. The beginning is inclusive and the ending is exclusive. This works out nicely. Similarly, calling plusWeeks(1) on a DateTime to add a week to the first moment of a day gives you the first moment of the 8th day later (see example below).
Time Zone
The question and other answers ignores the issue of time zone. If you do not specify, you'll be getting the default time zone. Usually better to specify a time zone, using a proper time zone name (not 3-letter code).
Joda-Time
Avoid the java.util.Date & Calendar classes bundled with Java. They are notoriously troublesome.
Here is some example code using Joda-Time 2.3.
CAVEAT: I have not tested of of the below code thoroughly. Just my first take, a rough draft. May well be flawed.
Standard Week (Monday-Sunday)
The Joda-Time library is built around the ISO 8601 standard. That standard defines the first day of the week as Monday, last day as Sunday.
If that meets your definition of a week, then getting the beginning and ending is easy.
UPDATE As an alternative to the discussion below, see this very clever and very simple one-liner solution by SpaceTrucker.
Simply forcing the day-of-week works because Joda-Time assumes you want:
Monday to be before (or same as) today.
Sunday to be after (or same as) today.
DateTimeZone timeZone = DateTimeZone.forID( "Europe/Paris" );
DateTime now = new DateTime( timeZone );
DateTime weekStart = now.withDayOfWeek( DateTimeConstants.MONDAY ).withTimeAtStartOfDay();
DateTime weekEnd = now.withDayOfWeek(DateTimeConstants.SUNDAY).plusDays( 1 ).withTimeAtStartOfDay();
Interval week = new Interval( weekStart, weekEnd );
Dump to console…
System.out.println( "now: " + now );
System.out.println( "weekStart: " + weekStart );
System.out.println( "weekEnd: " + weekEnd );
System.out.println( "week: " + week );
When run…
now: 2014-01-24T06:29:23.043+01:00
weekStart: 2014-01-20T00:00:00.000+01:00
weekEnd: 2014-01-27T00:00:00.000+01:00
week: 2014-01-20T00:00:00.000+01:00/2014-01-27T00:00:00.000+01:00
To see if a date-time lands inside that interval, call the contains method.
boolean weekContainsDate = week.contains( now );
Non-Standard Week
If that does not meet your definition of a week, you a twist on that code.
DateTimeZone timeZone = DateTimeZone.forID( "America/New_York" );
DateTime now = new DateTime( timeZone );
DateTime weekStart = now.withDayOfWeek( DateTimeConstants.SUNDAY ).withTimeAtStartOfDay();
if ( now.isBefore( weekStart )) {
// If we got next Sunday, go back one week to last Sunday.
weekStart = weekStart.minusWeeks( 1 );
}
DateTime weekEnd = weekStart.plusWeeks( 1 );
Interval week = new Interval( weekStart, weekEnd );
Dump to console…
System.out.println( "now: " + now );
System.out.println( "weekStart: " + weekStart );
System.out.println( "weekEnd: " + weekEnd );
System.out.println( "week: " + week );
When run…
now: 2014-01-24T00:54:27.092-05:00
weekStart: 2014-01-19T00:00:00.000-05:00
weekEnd: 2014-01-26T00:00:00.000-05:00
week: 2014-01-19T00:00:00.000-05:00/2014-01-26T00:00:00.000-05:00
First day of week depends on the country.
What makes the calculation fragile, is that one may break the year boundary, and the week number (Calendar.WEEK_OF_YEAR). The following would do:
Calendar currentDate = Calendar.getInstance(Locale.US);
int firstDayOfWeek = currentDate.getFirstDayOfWeek();
Calendar startDate = Calendar.getInstance(Locale.US);
startDate.setTime(currentDate.getTime());
//while (startDate.get(Calendar.DAY_OF_WEEK) != firstDayOfWeek) {
// startDate.add(Calendar.DATE, -1);
//}
int days = (startDate.get(Calendar.DAY_OF_WEEK) + 7 - firstDayOfWeek) % 7;
startDate.add(Calendar.DATE, -days);
Calendar endDate = Calendar.getInstance(Locale.US);
endDate.setTime(startDate.getTime());
endDate.add(Calendar.DATE, 6);
One bug in Calendar breaks your code, clone, seems to simply give the identical object, hence at the end you have identical dates. (Java 7 at least).
DateTime sDateTime = new DateTime(startDate); // My calendar start date
DateTime eDateTime = new DateTime(date); // the date for the week to be determined
Interval interval = new Interval(sDateTime, sDateTime.plusWeeks(1));
while(!interval.contains(eDateTime))
{
interval = new Interval(interval.getEnd(), interval.getEnd().plusWeeks(1));
}
return interval;
Try this (pseudo-code):
// How many days gone after reference date (a known week-start date)
daysGone = today - referenceDate;
// A new week starts after each 7 days
dayOfWeek = daysGone % 7;
// Now, we know today is which day of the week.
// We can find start & end days of this week with ease
weekStart = today - dayOfWeek;
weekEnd = weekStart + 6;
Now, we can shorten all of this to two lines:
weekStart = today - ((today - referenceDate) % 7);
weekEnd = weekStart + 6;
Note that we subtracted date values like integers to show algorithm. You have to write your java code properly.

Java DateFormat and SimpleDateFormat returning a date that is incorrect

Today is Tuesday, February 9, 2010 and when I print the date I get the wrong date:
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
Date today = formatter.parse(String.format("%04d-%02d-%02d",
Calendar.getInstance().get(Calendar.YEAR),
Calendar.getInstance().get(Calendar.MONTH),
Calendar.getInstance().get(Calendar.DAY_OF_MONTH)));
System.out.println("Today is " + today.toString());
The print line results in: "Today is Sat Jan 09 00:00:00 CST 2010"
It most certainly is not Saturday Jan 09, it's Tuesday Feb 09. I'm assuming I'm doing something wrong, so can anybody let me know what's wrong here? Do I have to manually set the day of week?
Update
Note: I don't want to initialize today with new Date() because I want the hours, minutes, seconds and milliseconds initialized to 0. This is necessary so I can compare a user input date with today: if the user inputs today's date and I use the formatter to construct a Date object, then if I initialize today with new Date() and I compare the two dates- today will be after the user selected date (which is incorrect). Thus I need to initialize today at the beginning of the day without the hr/min/sec/ms.
Confusingly, Calendar months count from 0 (January) to 11 (December), so you're passing "2010-01-09" to formatter.parse() when you extract the MONTH field from the Calendar.
There's a discussion of this in a related SO question.
If you don't want to use JodaTime you could use:
Calendar calendar = Calendar.getInstance();
calendar.set( Calendar.HOUR_OF_DAY, 0 );
calendar.set( Calendar.MINUTE, 0 );
calendar.set( Calendar.SECOND, 0 );
calendar.set( Calendar.MILLISECOND, 0 );
Date today = calendar.getTime();
This is much more efficient and less error-prone than your String formatting/parsing approach.
If you can use JodaTime this is a much preferred method:
LocalDate date = new DateTime().toLocaleDate();

Categories

Resources