Java date comparison off by a day - java

I have a Java method which compares two Dates and returns the number of days between them, but it's off by a day.
Even after I 0 out the hours, min, and sec the calculation is still off.
public long compareDates(Date exp, Date today){
TimeZone tzone = TimeZone.getTimeZone("America/New_York");
Calendar expDate = Calendar.getInstance();
Calendar todayDate = Calendar.getInstance();
expDate.setTime(exp);
todayDate.setTime(today);
expDate.set(Calendar.HOUR_OF_DAY, 0);
expDate.set(Calendar.MINUTE, 0);
expDate.set(Calendar.SECOND, 0);
todayDate.set(Calendar.HOUR_OF_DAY, 0);
todayDate.set(Calendar.MINUTE, 0);
todayDate.set(Calendar.SECOND, 0);
logger.info("Today = " + Long.toString(todayDate.getTimeInMillis()) + " Expiration = " + Long.toString(expDate.getTimeInMillis()));
expDate.setTimeZone(tzone);
todayDate.setTimeZone(tzone);
return (expDate.getTimeInMillis()-todayDate.getTimeInMillis())/86400000;
}
Output
Today = 1453939200030 Expiration = 1454544000000
There's 7 days between 1/28 and 2/4 but this returns 6.

Well, as you can see, you didn't clear the milliseconds, and 1454544000000 - 1453939200030 = 604799970 and dividing by 86400000 gets you 6.99999965277777..., which means 6 when truncated to int.
Now, if you clear the milliseconds too, today becomes 1453939200000, which will lead to you answer 7.
Note: This doesn't mean you're done, because of Daylight Savings Time. With DST, one of the timestamps may be ±1 hour from the other, so you may still get that truncation issue.
This was an answer to your particular issue. Try searching for how to correctly find days between dates in Java.

Today = 1453939200030
The times are given in milliseconds, and it looks like somehow your inputted Date has 30 extra milliseconds on it.
When I subtract the 30 milliseconds, then do the math on a calculator, I get 7 days. With your figures as is, I get 6.9999996527777777777777777777778, and in long math, the decimal figures get truncated to 6.
Zero out the milliseconds also.
expDate.set(Calendar.MILLISECOND, 0);
todayDate.set(Calendar.MILLISECOND, 0);

java.time
The Question and other Answers use outmoded classes. The old date-time classes such as java.util.Date/.Calendar bundled with the earliest versions of Java have proven to be quite troublesome. Those old classes have been supplanted by the java.time framework in Java 8 and later.
As the other Answers point out correctly, the issue is that the start long has 30 on the right side, precluding a whole-day calculation.
Count-Of-Days Definition
Furthermore you must define what you mean by a count-of-days. Do you mean a count by date, so any time on the 3rd of January to any time on the 4th is one day even if the times were a minute before and after midnight? Or do you mean a count of generic 24-hour blocks of time while ignoring the fact that particular days in particular time zones are not always 24-hours long because of Daylight Saving Time (DST) and other anomalies?
Count Days By Date
If you want the former, count by dates, then make use of the LocalDate class (a date-only without time-of-day nor time zone) and the Period class (a span of time defined as a count of years, months, days) found in java.time.
Define your inputs. Use long rather than int. These numbers apparently represent a count of milliseconds since the first moment of 1970 in UTC.
long startMilli = 1_453_939_200_030L;
long stopMilli = 1_454_544_000_000L;
Convert those long numbers into Instant objects, a moment on the timeline in UTC.
Instant startInstant = Instant.ofEpochMilli ( startMilli );
Instant stopInstant = Instant.ofEpochMilli ( stopMilli );
Define the time zone in which you want to consider the calendar dates. Note that time zone is crucial in defining dates. The date is not simultaneously the same around the globe. The date varies by time zone.
ZoneId zoneId = ZoneId.of ( "America/Montreal" );
Apply that time zone to each Instant to produce ZonedDateTime.
ZonedDateTime startZdt = ZonedDateTime.ofInstant ( startInstant , zoneId );
ZonedDateTime stopZdt = ZonedDateTime.ofInstant ( stopInstant , zoneId );
To get a Period, we need “local” dates. By “local” we mean any particular locality, a generic date value. The LocalDate class contains no time zone, but the time zone contained with in the ZonedDateTime is applied when determining a LocalDate.
LocalDate startLocalDate = startZdt.toLocalDate ();;
LocalDate stopLocalDate = stopZdt.toLocalDate ();
Define our span of time as a count of generic days, in Period.
Period period = Period.between ( startLocalDate , stopLocalDate );
Interrogate the Period to ask for the number of generic days contained within.
int days = period.getDays ();
Dump to console.
System.out.println ( "milli: " + startMilli + "/" + stopMilli + " | Instant: " + startInstant + "/" + stopInstant + " | ZonedDateTime: " + startZdt + "/" + stopZdt + " | LocalDate: " + startLocalDate + "/" + stopLocalDate + " | period: " + period + " | days: " + days );
milli: 1453939200030/1454544000000 | Instant: 2016-01-28T00:00:00.030Z/2016-02-04T00:00:00Z | ZonedDateTime: 2016-01-27T19:00:00.030-05:00[America/Montreal]/2016-02-03T19:00-05:00[America/Montreal] | LocalDate: 2016-01-27/2016-02-03 | period: P7D | days: 7
Count Of Whole Days
If you want a count of whole days, use the Days class from ThreeTen-Extra. Notice in the output below that we get a count of six (6) days rather than seven (7) as seen above.
ThreeTen-Extra
The ThreeTen-Extra project extends java.time. Run by the same folks who built java.time.
The behavior of the between method is not documented clearly. Experimenting shows that it seems to based on 24-hour chunks of time, not dates. Replace the 030 with 000, and also try replacing in the stopMilli the last 000 with 030, to see the behavior for yourself.
Days daysObject = Days.between ( startZdt , stopZdt );
int daysObjectCount = daysObject.getAmount ();
Dump to console. The P6D string you see in the output was generated according to the formats defined in the ISO 8601 standard. This standard is used by default in java.time for all parsing and generating of textual representations of date-time values. These standard formats are quite sensible and useful so do glance at that linked Wikipedia page.
System.out.println ( "daysObject: " + daysObject + " | daysObjectCount: " + daysObjectCount );
daysObject: P6D | daysObjectCount: 6

To fix my problems, I have zeroed out the milliseconds as mentioned, as well as casted the longs to doubles in order to maintain accuracy and round when necessary.
expDate.setTime(exp);
todayDate.setTime(today);
expDate.setTimeZone(tzone);
todayDate.setTimeZone(tzone);
expDate.set(Calendar.HOUR_OF_DAY, 0);
expDate.set(Calendar.MINUTE, 0);
expDate.set(Calendar.SECOND, 0);
expDate.set(Calendar.MILLISECOND, 0);
todayDate.set(Calendar.HOUR_OF_DAY, 0);
todayDate.set(Calendar.MINUTE, 0);
todayDate.set(Calendar.SECOND, 0);
todayDate.set(Calendar.MILLISECOND, 0);
double diff = ((double)expDate.getTimeInMillis()-(double)todayDate.getTimeInMillis())/86400000;
return Math.round(diff);

Related

What is a best way to find the number of days and months between two java.time.Instant objects?

NOTE: search Google before marking this question as duplicate. I did search and browse this question and all answers that I found were either for LocalDate, Joda or legacy Java Date.
It took me quite some time to investigate this so I've decided to share this as an answer.
I'd like a way to calculate the (approximate) number of months and days between two Java Instants (objects of java.time.Instant)?
First, what you are asking is not well-defined. For example between the instants 2020-03-01T06:00:00Z and 2020-03-31T05:00:00Z could be:
29 days 23 hours in Australia/Melbourne time zone;
30 days in Europe/Paris time zone;
1 month 1 day in America/Los_Angeles time zone.
Accurate result in a given time zone
ZoneId zone = ZoneId.of("America/Los_Angeles");
Instant start = Instant.parse("2020-03-01T06:00:00Z");
Instant end = Instant.parse("2020-03-31T05:00:00Z");
ZonedDateTime startZdt = start.atZone(zone);
LocalDate startDate = startZdt.toLocalDate();
ZonedDateTime endZdt = end.atZone(zone);
LocalDate endDate = endZdt.toLocalDate();
Period p = Period.between(startDate, endDate);
if (startZdt.plus(p).isAfter(endZdt)) {
// The time of day on the end date is earlier, so don’t count a full date
endDate = endDate.minusDays(1);
p = Period.between(startDate, endDate);
}
System.out.println(p);
Output:
P1M1D
Read as a period of 1 month 1 day.
Approximate result independent of time zone
Prefer to leave as much of the calculation to java.time as possible. This includes the estimate of the length of a month.
Duration diff = Duration.between(start, end);
Duration durationOfAMonth = ChronoUnit.MONTHS.getDuration();
long months = diff.dividedBy(durationOfAMonth);
diff = diff.minus(durationOfAMonth.multipliedBy(months));
long days = diff.toDays();
System.out.println("" + months + " months " + days + " days");
0 months 29 days
I've opted out to approximate solution (it assumes all months have 30.44 days). I've opted out to use something like this:
Duration duration = Duration.between(instant1, instant2).abs(); /* if want negative values remove .abs() */
long hours = duration.toHours();
double daysAndMonthsInDays = hours / 24.0;
int months = daysAndMonthsInDays / 30.44; //average number of days per month
int days = daysAndMonthsInDays - months * 30.44;
Please post another answer if there is a better solution using Duration class or something else. I've decided not to convert Instant to LocalDate and to perform the conversion on that level. That would not use an approximation of 30.44 days in a month, but rather the actual number.

Calculating years between from a leap year

When calculating years between two dates, where the second date is calculated from the first one (this is a simplified example of what I'm working on), LocalDate and Period seem to calculate a year slightly differently.
For example,
LocalDate date = LocalDate.of(1996, 2, 29);
LocalDate plusYear = date.plusYears(1);
System.out.println(Period.between(date, plusYear).getYears());
while
LocalDate date = LocalDate.of(1996, 3, 29);
LocalDate plusYear = date.plusYears(1);
System.out.println(Period.between(date, plusYear).getYears());
Despite having explicitly added a year, first Period return the years as 0, while the second case returns 1.
Is there a neat way around this?
This question has a philosophical nature and spans few problems like time measurements, and date format conventions.
LocalDate is an implementation of ISO 8601 date exchange standard.
Java Doc states explicitly that this class does not represent time but provides only standard date notation.
The API provides only simple operations on the notation itself and all calculations are done by incrementing the Year, or Month, or Day of a given date.
In other words, when calling LocalDate.plusYears() you are adding conceptual years of 365 days each, rather than the exact amount of time within a year.
This makes Day the lowest unit of time which one can add to a date expressed by LocalDate.
In human understanding, date is not a moment in time, but it is a period.
It starts with 00h 00m 00s (...) and finishes with 23h 59m 59s (...).
LocalDate however avoids problems of time measurement and vagueness of human time units (hour, day, month, and a year can all have different length) and models date notation simply as a tuple of:
(years, months within a year, days within a month )
calculated since the beginning of the era.
In this interpretation, it makes sense that Day is the smallest unit affecting the date.
As an example following:
LocalDate date = LocalDate.of(1996, 2, 29);
LocalDate plusSecond = date.plus(1, ChronoUnit.SECONDS);
returns
java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds
... which shows, that using LocalDate and adding the number of seconds (or smaller units to drive the precision), you could not overcome the limitation listed in your question.
Looking at the implementation you find that LocalDate.plusYears() after adding the years, calls resolvePreviousValid(). This method then checks for leap year and modifies the day field in the following manner:
day = Math.min(day, IsoChronology.INSTANCE.isLeapYear((long)year)?29:28);
In other words it corrects it by effectively deducting 1 day.
You could use Year.length() which returns the number of days for given year and will return 366 for leap years. So you could do:
LocalDate plusYear = date.plus(Year.of(date.getYear()).length(), ChronoUnit.DAYS);
You will still run into following oddities (call to Year.length() replaced with the day counts for brevity):
LocalDate date = LocalDate.of(1996, 2, 29);
LocalDate plusYear = date.plus(365, ChronoUnit.DAYS);
System.out.println(plusYear);
Period between = Period.between(date, plusYear);
System.out.println( between.getYears() + "y " +
between.getMonths() + "m " +
between.getDays() + "d");
returns
1997-02-28
0y 11m 30d
then
LocalDate date = LocalDate.of(1996, 3, 29);
LocalDate plusYear = date.plus(365, ChronoUnit.DAYS);
System.out.println(plusYear);
Period between = Period.between(date, plusYear);
System.out.println( between.getYears() + "y " +
between.getMonths() + "m " +
between.getDays() + "d");
returns
1997-03-29
1y 0m 0d
and finally:
LocalDate date = LocalDate.of(1996, 2, 29);
LocalDate plusYear = date.plus(366, ChronoUnit.DAYS);
System.out.println(plusYear);
Period between = Period.between(date, plusYear);
System.out.println( between.getYears() + "y " +
between.getMonths() + "m " +
between.getDays() + "d");
returns:
1997-03-01
1y 0m 1d
Please note that moving the date by 366 instead of 365 days increased the period from 11 months and 30 days to 1 year and 1 day (2 days increase!).

Convert milliseconds to TDateTime in Java

I'm in an Android project that requires saving a file with TDateTime type (Delphi). I have my date in milliseconds, but I don't know how to convert milliseconds to TDateTime.
I have something like this:
Date dateInMillis = new Date(System.currentTimeMillis());
double dateInDouble = ???;
I'll be glad for any tips that can help me to resolve this.
Delphi's TDateTime measures time in days. Java follows the Unix standard and measures in milliseconds. To convert between the two you need to scale by the number of milliseconds in a day, 86400000.
The other difference is that the two systems use a different epoch. The Unix epoch, as used by Java, is 00:00, 1 Jan 1970. The Delphi epoch is 00:00, 30 December 1899. The Unix epoch, represented as a Delphi TDateTime is 25569.
So, to convert from milliseconds from the Unix epoch, to days from the Delphi epoch you perform the following calculation:
double delphiDateTime = unixMillis/86400000.0 + 25569.0;
More recently post Java 8 with the new date time classes, the following should work:
LocalDateTime localDateTime = LocalDateTime.of(1899, 12, 30, 0, 0);//Delphi date EPOCH time start
double fraction = val % 1;
long intPart = (long) (val - fraction);
localDateTime = localDateTime.plus(intPart, ChronoUnit.DAYS);
localDateTime = localDateTime.plus((long) (24*60*60*1000 * fraction), ChronoUnit.MILLIS); //fraction is a fraction of the time of day
tl;dr
Use modern java.time classes that years ago supplanted Date class.
Duration duration =
Duration.between(
LocalDate.of( 1899 , Month.DECEMBER , 30 ).atStartOfDay( ZoneOffset.UTC ).toInstant() , // Epoch reference moment used by Delphi.
Instant.now() // Current moment as seen in UTC.
);
double temp =
// Get a whole number of days (integer ) + a decimal fraction of partial day = a `double` number = the `TDateTime` type in Delphi.
duration.toDays() +
(
( double ) duration.minusDays( duration.toDays() ).toMillis() // Milliseconds in our partial day.
/
( double ) Duration.ofDays( 1 ).toMillis() // Milliseconds in a full day, a generic 24-hour day.
);
double r = Math.floor( temp * 1000 ) / 1000; // Truncate to third decimal place to represent a resolution of milliseconds, the limit of `TDateTime` type in Delphi.
java.time
Use only the modern java.time classes in Java, never the legacy classes such as java.util.Date or java.sql.Date.
Capture the current moment as seen in UTC.
Instant now = Instant.now() ;
Because of some twisted history, as its epoch reference for its TDateTime class Delphi uses the first moment of 1899-12-30 presumably in UTC. For the date portion, use LocalDate.
LocalDate delphiEpochDate = LocalDate.of( 1899 , Month.DECEMBER , 30 ); // No, not the 31st, the 30th.
epochDate.toString(): 1899-12-30
As a habit, let java.time determine the first moment of the day, as it is not always 00:00 on all dates in all zones.
Instant delphiEpochMoment = delphiEpochDate.atStartOfDay( ZoneOffset.UTC ).toInstant();
odt.toString(): 1899-12-30T00:00Z
Delphi uses a terrible method of tracking time as inherited from spreadsheets: Using a double floating-point fractional number.
The integer portion represents full days, generic 24-hour long days that ignore the anomalies of political time. For such elapsed time, we use Duration.
Duration d = Duration.between( delphiEpochMoment , now ) ;
long days = d.toDays() ; // Generic 24-hour long days.
Next, get the fractional part that represents a portion of a 24-hour day. First we subtract the amount of the full days, to leave us with a partial day.
Duration partialDay = d.minusDays( days ) ;
Then divide the partial amount by the length of a full day. We will use a resolution of milliseconds rather than the nanoseconds capability of Duration, as it seems Delphi is limited to milliseconds.
double millisOfPartialDay = partialDay.toMillis() ;
double millisOfFullDay = Duration.ofDays( 1 ).toMillis() ;
double tempResult = ( millisOfPartialDay / millisOfFullDay ) ;
We should truncate results to milliseconds. For truncating a double, see this Answer. And we should add our whole number of days.
double tempResult = days + ( millisOfPartialDay / millisOfFullDay ) ;
double result = Math.floor( tempResult * 1000 ) / 1000 ;
Putting that all together.
Instant now = Instant.now();
LocalDate delphiEpochDate = LocalDate.of( 1899 , Month.DECEMBER , 30 ); // No, not the 31st, the 30th.
Instant delphiEpochMoment = delphiEpochDate.atStartOfDay( ZoneOffset.UTC ).toInstant();
Duration d = Duration.between( delphiEpochMoment , now );
long days = d.toDays(); // Generic 24-hour long days.
Duration partialDay = d.minusDays( days );
double millisOfPartialDay = partialDay.toMillis();
double millisOfFullDay = Duration.ofDays( 1 ).toMillis();
double tempResult = days + ( millisOfPartialDay / millisOfFullDay );
double result = Math.floor( tempResult * 1000 ) / 1000; // Truncate to third decimal place to represent a resolution of milliseconds.
System.out.println( "delphiEpochMoment = " + delphiEpochMoment );
System.out.println( "d = " + d );
System.out.println( "tempResult = " + tempResult );
System.out.println( "result = " + result );
delphiEpochMoment = 1899-12-30T00:00:00Z
d = PT1056815H56M10.613011S
tempResult = 44033.99734505787
result = 44033.997
Caveat: I have not tested this code. And I do not use Delphi. So buyer beware: this code is worth every penny you paid for it.
Avoid LocalDateTime
Beware: Do not use LocalDateTime to represent a moment. This class represents a date with time-of-day, but lacks the context of a time zone or offset-from-UTC. For example, take the value of Noon on January 23rd of 2021. Would that be noon in Tokyo Japan? Or noon in Toulouse France? Or noon in Toledo Ohio US? Those would be three very different moments, several hours apart. We do not know which is intended if the time zone is lacking.
You could get away with LocalDateTime in this specific problem of Delphi time-tracking because Delphi (presumably) uses UTC, and UTC uses generic 24-hour-long days as does LocalDateTime. But using LocalDateTime is conceptually not fit for this problem, as we need moments for here.

Android/Java - Test if date was last month

I am working on an app where I store some information between each use, this data essentially boils down to counting the number of times an event has happened today, this week, this month and in the apps lifetime. I store this data in 4 distinct counters I can load/save using SharedPreferences.
Alongside the data I store the "last run time" of the app as a date, my plan was that during load time I will load in the counters then test the stored date against today's date to determine which counters need to be cleared.
Sounds simple right!
After pulling my hair out for a while and going backward and forwards through the Calendar documentation I think I understand them enough to come up with the following:
Calendar last = Calendar.getInstance();
last.setTimeInMillis(lastDate);
Calendar today = Calendar.getInstance();
today.add(Calendar.DATE, -1);
if ( !last.after(today) )
{
today = 0;
}
today.add(Calendar.WEEK_OF_MONTH, -1);
today.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY);
if ( !last.after(today) )
{
today = 0;
week = 0;
}
today = Calendar.getInstance();
today.add(Calendar.MONTH, -1);
today.set(Calendar.DATE, today.getActualMaximum(Calendar.DATE));
if ( !last.after(today) )
{
today = 0;
week = 0;
month = 0;
}
I think this should be fine, however the issue I have is testing, testing today is easy, however testing the month logic would require either waiting a month, or writing a test case which uses the Calendar API to simulate an old date, however I can't write the test case if my assumptions on how the API works was wrong in the first place!
Therefore, after a large wall of text my question is... does the above block of code look sane, or have I completely mis-understood working with dates in Java?
Thanks!
Edit:
Second pass at the code:
Does this look any more sensible? If I am understanding things correctly I am now attempting to compare the end of the date that was last saved with the very start of today, this week and this month.
Calendar last = Calendar.getInstance();
last.setTimeInMillis(lastDate);
last.set(Calendar.HOUR_OF_DAY, last.getActualMaximum(Calendar.HOUR_OF_DAY));
last.set(Calendar.MINUTE, last.getActualMaximum(Calendar.MINUTE));
last.set(Calendar.SECOND, last.getActualMaximum(Calendar.SECOND));
last.set(Calendar.MILLISECOND, last.getActualMaximum(Calendar.MILLISECOND));
Calendar todayStart = Calendar.getInstance();
todayStart.set(Calendar.HOUR_OF_DAY, todayStart.getActualMinimum(Calendar.HOUR_OF_DAY));
todayStart.set(Calendar.MINUTE, todayStart.getActualMinimum(Calendar.MINUTE));
todayStart.set(Calendar.SECOND, todayStart.getActualMinimum(Calendar.SECOND));
todayStart.set(Calendar.MILLISECOND, todayStart.getActualMinimum(Calendar.MILLISECOND));
// If the last recorded date was before the absolute minimum of today
if ( last.before(todayStart) )
{
todayCount = 0;
}
Calendar thisWeekStart = Calendar.getInstance();
thisWeekStart.set(Calendar.HOUR_OF_DAY, thisWeekStart.getActualMinimum(Calendar.HOUR_OF_DAY));
thisWeekStart.set(Calendar.MINUTE, thisWeekStart.getActualMinimum(Calendar.MINUTE));
thisWeekStart.set(Calendar.SECOND, thisWeekStart.getActualMinimum(Calendar.SECOND));
thisWeekStart.set(Calendar.DAY_OF_WEEK, thisWeekStart.getFirstDayOfWeek());
thisWeekStart.set(Calendar.MILLISECOND, thisWeekStart.getActualMinimum(Calendar.MILLISECOND));
// If the last date was before the absolute minimum of this week then clear
// this week (and today, just to be on the safe side)
if ( last.before(thisWeekStart) )
{
todayCount = 0;
weekCount = 0;
}
Calendar thisMonthStart = Calendar.getInstance();
thisMonthStart.set(Calendar.HOUR_OF_DAY, thisMonthStart.getActualMinimum(Calendar.HOUR_OF_DAY));
thisMonthStart.set(Calendar.MINUTE, thisMonthStart.getActualMinimum(Calendar.MINUTE));
thisMonthStart.set(Calendar.SECOND, thisMonthStart.getActualMinimum(Calendar.SECOND));
thisMonthStart.set(Calendar.DAY_OF_MONTH, thisMonthStart.getActualMinimum(Calendar.MONTH));
thisMonthStart.set(Calendar.MILLISECOND, thisMonthStart.getActualMinimum(Calendar.MILLISECOND));
// If the last date was before the absolute minimum of this month then clear month...
if ( !last.after(thisMonthStart) )
{
todayCount = 0;
weekCount = 0;
monthCount = 0;
}
Other than the readability challenges of using a variable called "today" and setting it to all manner of things that aren't "Today", you're not handling the time.
If it's now 3:20, and something happened at 5:00pm on Jan 31st, we probably want to still count that as happening in January? You should max out the time related fields to the end of the day as well.
For the week thing, that can be a real mess if someone executes in a locale where Sunday is considered the first day of the week. You may want to consider using the system's first day of week, rather than Sunday.
Also it is probably worth noting that this depends explicitly on the use of Calendar.add() to work properly. cal.set(Calendar.MONTH, cal.get(Calendar.MONTH) -1); is NOT the same thing and would be broken.
You should just use Joda-Time. If you do your code becomes:
DateTime oneMonthAgo = new DateTime().minusMonths(1);
DateTime oneWeekAgo = new DateTime().minusWeeks(1);
And so on... It requires no further dependencies than the JDK itself and works on Android. Hope that helps.
Yes, you can use Joda-Time on Android. (From what I've read; I don't use Android)
Yes, you should be using Joda-Time. Far more advanced and useful that the notoriously troublesome java.util.Date and .Calendar classes bundled with Java.
Both your question and the other answers are ignoring the crucial issue of time zone. The time zone defines the meaning of "today" and the beginning/ending of other days.
You should define in plain declarative sentences exactly what you mean by "today", "this week", and "this month". For example, "today"…
Do you mean the last 24 hours?
Do you mean from 00:00:00 and up to but not including 00:00:00 tomorrow, in the UTC/GMT time zone (that is, no time zone offset)?
Do mean from the first moment of today in a given time zone (some offset from UTC) up to but not including the first moment of tomorrow in the same time zone? This may not be 24 hours because of Daylight Saving Time (DST) or other anomalies.
I'm too tired to parse your code. And I shouldn't have to. Before writing such date-time code, you should spell out in plain English what your goal is. Date-time work is surprisingly tricky, so you must be clear on your goals.
Here's some example code in Joda-Time 2.3.
Joda-Time uses the ISO 8601 standard for most defaults. This includes the definition of a week. Monday is first day, numbered 1, and Sunday is last day, numbered 7.
When focusing on a "day" with date-time objects, you may want to start with the first moment of the day. If so, call the withTimeAtStartOfDay method. To get end-of-day, don't. Use the Half-Open approach where you compare up to but not including the first moment of the next day. Explanation is found in other answers on StackOverflow.
Joda-Time offers 3 classes to handle spans of time: Period, Duration, and Interval. Check them all out. When doing comparisons, Joda-Time uses the "Half-Open" approach where the beginning is inclusive and the ending is exclusive. This makes sense when you ponder it. Search StackOverflow for more discussion.
Here's a bit of example code to get you going. I take a set of arbitrary date-time values. Then I define some spans of time as a day, week ago, and month ago. Then I count how many of the values fall into those spans.
String input = "2014-01-02T03:04:05Z";
DateTimeZone timeZone = DateTimeZone.forID( "America/Montreal" );
java.util.List<DateTime> dateTimes = new java.util.ArrayList<DateTime>();
DateTime dateTime1 = new DateTime( input, timeZone ); // Parse the string as being in Zulu time zone (UTC). Then adjust to Montréal time.
dateTimes.add( dateTime1 );
dateTimes.add( dateTime1.plusDays( 3 ) );
dateTimes.add( dateTime1.plusWeeks( 1 ) );
dateTimes.add( dateTime1.plusMonths( 1 ) );
DateTime now = new DateTime( timeZone );
dateTimes.add( now );
dateTimes.add( now.minusDays( 1 ) );
dateTimes.add( now.minusDays( 10 ) );
// Spans of time
Interval today = new Interval( now.withTimeAtStartOfDay(), now.plusDays( 1 ).withTimeAtStartOfDay() );
Interval pastWeek = new Interval( now.minusWeeks( 1 ).withTimeAtStartOfDay(), now.plusDays( 1 ).withTimeAtStartOfDay() );
Interval pastMonth = new Interval( now.minusMonths( 1 ).withTimeAtStartOfDay(), now.plusDays( 1 ).withTimeAtStartOfDay() );
int countTotal = dateTimes.size();
int countDay = 0;
int countWeek = 0;
int countMonth = 0;
for ( DateTime dateTime : dateTimes ) {
if ( today.contains( dateTime ) ) {
countDay++;
}
if ( pastWeek.contains( dateTime ) ) {
countWeek++;
}
if ( pastMonth.contains( dateTime ) ) {
countMonth++;
}
}
Dump to console…
System.out.println( "dateTimes: " + dateTimes );
System.out.println( "today: " + today );
System.out.println( "pastWeek: " + pastWeek );
System.out.println( "pastMonth: " + pastMonth );
System.out.println( "countTotal: " + countTotal );
System.out.println( "countDay: " + countDay );
System.out.println( "countWeek: " + countWeek );
System.out.println( "countMonth: " + countMonth );
When run…
dateTimes: [2014-01-01T22:04:05.000-05:00, 2014-01-04T22:04:05.000-05:00, 2014-01-08T22:04:05.000-05:00, 2014-02-01T22:04:05.000-05:00, 2014-03-05T07:40:25.508-05:00, 2014-03-04T07:40:25.508-05:00, 2014-02-23T07:40:25.508-05:00]
today: 2014-03-05T00:00:00.000-05:00/2014-03-06T00:00:00.000-05:00
pastWeek: 2014-02-26T00:00:00.000-05:00/2014-03-06T00:00:00.000-05:00
pastMonth: 2014-02-05T00:00:00.000-05:00/2014-03-06T00:00:00.000-05:00
countTotal: 7
countDay: 1
countWeek: 2
countMonth: 3

Java TimeZone head scratching

I would like to save some user timezone in a Daylight saving proof format.
My goal is to get the correct GMT offset whenever the code gets executed.
In order to figure out my best option, I wrote the following:
ArrayList<String> list = new ArrayList<String>();
list.add( "EST");
list.add( "EDT");
list.add( "America/New_York");
long now = System.currentTimeMillis();
for( String tzID: list) {
TimeZone tz = TimeZone.getTimeZone( tzID);
System.out.println( tzID + " now=" + tz.getOffset( now) / 3600000 + " / +182=" + tz.getOffset( now + ( 182 * 86400000)) / 3600000);
}
For short, give me the offset now and in 182 days
Executed September 3rd, the output is
EST now=-5 / +182=-5
EDT now=0 / +182=0
America/New_York now=-4 / +182=-4
This is unexpected for several reasons
1) Why is America/New_York not giving -4/-5 ?, Isn't it supposed to be date sensitive?
2) Why does EDT == UTC?
java.time
The question and the accepted answer use the java.util date-time API which was the right thing to do in 2012. In March 2014, the modern Date-Time API was released as part of the Java 8 standard library which supplanted the legacy date-time API and since then it is strongly recommended to switch to java.time, the modern date-time API.
Solution using java.time
You can use ZonedDateTime which automatically adjusts the time zone offset for a given ZoneId.
Demo:
import java.time.ZoneId;
import java.time.ZonedDateTime;
class Main {
public static void main(String[] args) {
ZoneId zone = ZoneId.of("America/New_York");
ZonedDateTime now = ZonedDateTime.now(zone);
ZonedDateTime after182Days = now.plusDays(182);
System.out.println(zone + " now=" + now.getOffset() + " / +182=" + after182Days.getOffset());
}
}
Output as of now:
America/New_York now=-05:00 / +182=-04:00
ONLINE DEMO
Learn more about the modern Date-Time API from Trail: Date Time.
Do not use three-letter timezone ID: Note from the Java 7 Timezone documentation:
Three-letter time zone IDs
For compatibility with JDK 1.1.x, some other three-letter time zone IDs (such as "PST", "CTT", "AST") are
also supported. However, their use is deprecated because the same
abbreviation is often used for multiple time zones (for example, "CST"
could be U.S. "Central Standard Time" and "China Standard Time"), and
the Java platform can then only recognize one of them.
One problem you have is that 182 * 86400000 overflows. If you use
long now = System.currentTimeMillis();
for( String tzID: "EST,EDT,America/New_York".split(",")) {
TimeZone tz = TimeZone.getTimeZone( tzID);
System.out.println( tz.getDisplayName() + " now=" + tz.getOffset( now) / 36e5
+ " / +182=" + tz.getOffset( now + 182 * 86400000L) / 36e5);
}
prints
Eastern Standard Time now=-5.0 / +182=-5.0
Greenwich Mean Time now=0.0 / +182=0.0
Eastern Standard Time now=-4.0 / +182=-5.0
If you look at the javadoc and source for getTimeZone you can see
* #return the specified <code>TimeZone</code>, or the GMT zone if the given ID
* cannot be understood.
public static synchronized TimeZone getTimeZone(String ID) {
return getTimeZone(ID, true);
}
private static TimeZone getTimeZone(String ID, boolean fallback) {
TimeZone tz = ZoneInfo.getTimeZone(ID);
if (tz == null) {
tz = parseCustomTimeZone(ID);
if (tz == null && fallback) {
tz = new ZoneInfo(GMT_ID, 0);
}
}
return tz;
}
In short, EDT is not recognised so it becomes GMT.
I suspect this is the problem:
now + ( 182 * 86400000)
The parenthesized arithmetic expression overflows 32 bits. You probably want:
now + ( 182 * 86400000L)
However, that still assumes that any daylight saving time will be applied for roughly six months, which is certainly not the case in the real world. For example, looking at the Sao Paolo time zone, it switches in October and February - so if you ran your code in September, you'd end up seeing -3 / -3. Even for time zones where DST switches on/off roughly every six months, you're very likely to find 182 consecutive days each year without a switchover (almost by definition, given that that's slightly less than half a year).
It's not clear exactly what you're trying to do, but I suspect you should really just be saving the time zone ID, e.g. "America/New_York". Almost anything else is asking for trouble.

Categories