We have a Postgres table that has two TIMESTAMP WITHOUT TIME ZONE columns, prc_sta_dt and prc_end_dt. We check to see whether a java.util.Date falls in between the start and end dates.
Here is some of the Java code, which is simplified but gets the point across.
// This format expects a String such as 2018-12-03T10:00:00
// With a date and a time, but no time zone
String timestamp = "2018-12-03T10:00:00";
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
Date searchDate = formatter.parse(timestamp);
// Here's the Postgres query
String query = "select promotion_cd from promotions " +
"where prc_sta_dt <= :srch_dt and prc_end_dt >= :srch_dt";
Map<String, Object> map = new HashMap<String, Object>();
map.put("srch_dt", searchDate);
List<Promotion> promotions = jdbcTemplate.query(query, map, promotionMapper);
In our Postgres table, we have promotions that start at 9am on 12/3/2018 and end at 3pm on the same day. The prc_sta_dt and prc_end_dt colums in our database for these rows are 2018-12-03 09:00:00.0 and 2018-12-03 15:00:00.0
Question: When JDBC/Postgres accepts our searchDate and compares it to these timestamps, will it accept the given search date of 10am (2018-12-03T10:00:00) or will it treat this time as being under the time zone that the server is running on, and then convert that to UTC?
For example, if the server is running in Chicago, then will it interpret 10 am as 10am CST and then convert that to 4pm UTC before doing the comparison in the database? If so then we're out of luck!
I doubt this would happen, but I just want to make sure so there are no surprises.
Wrong data type, Date is not a date
A java.util.Date object represents a moment in UTC, a specific point on the timeline. So it represents the combination of a date, a time-of-day, and an offset-from-UTC of zero (for UTC itself). Among the many poor design choices in this terrible class is its misleading name that has confused countless Java programmers.
TIMESTAMP WITHOUT TIME ZONE
If you care about moments, then your database column should not be of type TIMESTAMP WITHOUT TIME ZONE. That data type represents a date and a time-of-day without any concept of time zone or offset-from-UTC. As such, by definition, that type cannot represent a moment, is not a point on the timeline. This type should only be used when you mean a date-with-time anywhere or everywhere.
Examples:
“Christmas starts after stroke of midnight at beginning of December 25, 2018” where Christmas in Kiribati comes first, India later, and Africa even later.
“Company-wide memo: Each of our factories in Delhi, Düsseldorf, and Detroit will close one hour early at 16:00 on January 21st” where 4 PM at each factory is three different moments, each several hours apart.
TIMESTAMP WITH TIME ZONE
When tracking specific a specific moment, a single point on the timeline, use a column of type TIMESTAMP WITH TIME ZONE. In Postgres, such values are stored in UTC. Any time zone or offset info submitted with an input is used to adjust into UTC, then the zone/offset info is discarded.
BEWARE: Some tools may have the well-intentioned but unfortunate anti-feature of injecting a time zone after retrieving the value in UTC, thereby misrepresenting what was actually stored.
Comparing a moment to values of TIMESTAMP WITHOUT TIME ZONE
As for comparing a moment to values in your column of type TIMESTAMP WITHOUT TIME ZONE, doing so would generally not make sense.
But if you are clear-headed and educated about date-time handling, and making this comparison is sensible in your business logic, let's forge on.
Wrong classes
You are using lousy, terrible, awful date-time classes (Date, SimpleDateFormat, etc.) that were supplanted years ago by the java.time classes. Do yourself a favor: Stop using the legacy date-time classes. Use only java.time.
If given a moment as a java.util.Date, use the new methods added to the old classes to convert. In particular, java.util.Date is replaced by Instant.
Instant instant = myJavaUtilDate.toInstant() ; // Convert from legacy class to modern class.
Specify the time zone in which you want to adjust your Instant moment in UTC for comparison. For example, if your database was built by someone who did not understand proper date-time handling, and has been using the TIMESTAMP WITHOUT TIME ZONE column to store date-with-time values that were taken from the wall-clock time of Québec, then use the time zone America/Montreal.
Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 2-4 letter abbreviation such as EST or IST as they are not true time zones, not standardized, and not even unique(!).
ZoneId z = ZoneId.of( "America/Montreal" ) ;
Apply that zone to our Instant to get a ZonedDateTime object.
ZonedDateTime zdt = instant.atZone( z ) ;
Our resulting ZonedDateTime object represents the same moment as the Instant object, same point on the timeline, but viewed with a different wall-clock time.
To hammer a square-peg into a round-hole, let's convert that ZonedDateTime object to a LocalDateTime object, thereby stripping away the time zone information and leaving only a date-with-time-of-day value.
LocalDateTime ldt = zdt.toLocalDateTime() ;
Half-Open
where prc_sta_dt <= :srch_dt and prc_end_dt >= :srch_dt
That logic is prone to failure. Generally, the best practice in date-time handling when defining a span-of-time to use Half-Open, where the beginning is inclusive while the ending is exclusive.
So use this:
WHERE instant >= start_col AND instant < stop_col ;
For a PreparedStatement, we would have placeholders.
WHERE ? >= start_col AND ? < stop_col ;
On the Java side, as of JDBC 4.2 we can directly exchange java.time objects with the database via getObject and setObject methods.
You might be able to pass an Instant depending on your JDBC driver. Support for Instant is not required by the JDBC spec. So try it, or read the doc for your driver.
myPreparedStatement.setObject( 1 , instant ) ;
myPreparedStatement.setObject( 2 , instant ) ;
If Instant is not supported, convert from Instant to an OffsetDateTime set to UTC. Support for OffsetDateTime is required by the spec.
myPreparedStatement.setObject( 1 , instant.atOffset( ZoneOffset.UTC ) ) ;
myPreparedStatement.setObject( 2 , instant.atOffset( ZoneOffset.UTC ) ) ;
Retrieval.
OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
Always specify time zone
For example, if the server is running in Chicago, then will it interpret 10 am as 10am CST and then convert that to 4pm UTC before doing the comparison in the database?
A programmer should never depend on the time zone (or locale, by the way) currently set as the default on the host OS or JVM. Both are out of your control. And both can change at any moment during runtime!
Always specify the time zone by passing the optional argument to various date-time methods. Making those optional was a design flaw in java.time in my opinion, as programmers all too often ignore the issue of time zone, at their own peril. But that is one of very few design flaws in an amazingly useful and elegant framework.
Notice in our code above we specified the desired/expected time zone. The current default time zone of our host OS, our Postgres database connection, and our JVM will not alter the behavior of our code.
Current moment
If you want the current moment use any of these:
Instant.now()Always in UTC, by definition.
OffsetDateTime.now( someZoneOffset )Current moment as seen in the wall-clock time of a particular offset-from-UTC.
ZonedDateTime.now( someZoneId )Current moment as seen in the wall-clock time used by the people living in a particular region.
Java 7 and ThreeTen-Backport
If you are using Java 7, then you have no java.time classes built-in. Fortunately, the inventor of JSR 310 and java.time, Stephen Colebourne, also led the ThreeTen-Backport project to produce a library providing most of the java.time functionality to Java 6 & 7.
Here is a complete example app in a single .java file showing the use of back-port in Java 7 with the H2 Database Engine.
In Java 7, JDBC 4.2 is not available, so we cannot directly use the modern classes. We fall back to using java.sql.Timestamp which actually represents a moment in UTC, but which H2 stores into a column of TIMESTAMP WITHOUT TIME ZONE taking the date and the time-of-day as-is (using the wall-clock time of UTC) while ignoring the UTC aspect. I have not tried this in Postgres, but I expect you will see the same behavior.
package com.basilbourque.example;
import java.sql.*;
import org.threeten.bp.*;
public class App {
static final public String databaseConnectionString = "jdbc:h2:mem:localdatetime_example;DB_CLOSE_DELAY=-1"; // The `DB_CLOSE_DELAY=-1` keeps the in-memory database around for multiple connections.
public static void main ( String[] args ) {
App app = new App();
app.doIt();
}
private void doIt () {
System.out.println( "Bonjour tout le monde!" );
// java.sql.Timestamp ts = DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 1 , 23 , 12 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
// System.out.println( ts );
this.makeDatabase();
java.util.Date d = new java.util.Date(); // Capture the current moment using terrible old date-time class that is now legacy, supplanted years ago by the class `java.time.Instant`.
this.fetchRowsContainingMoment( d );
}
private void makeDatabase () {
try {
Class.forName( "org.h2.Driver" );
} catch ( ClassNotFoundException e ) {
e.printStackTrace();
}
try (
Connection conn = DriverManager.getConnection( databaseConnectionString ) ; // The `mem` means “In-Memory”, as in “Not persisted to disk”, good for a demo.
Statement stmt = conn.createStatement() ;
) {
String sql = "CREATE TABLE event_ ( \n" +
" pkey_ IDENTITY NOT NULL PRIMARY KEY , \n" +
" name_ VARCHAR NOT NULL , \n" +
" start_ TIMESTAMP WITHOUT TIME ZONE NOT NULL , \n" +
" stop_ TIMESTAMP WITHOUT TIME ZONE NOT NULL \n" +
");";
stmt.execute( sql );
// Insert row.
sql = "INSERT INTO event_ ( name_ , start_ , stop_ ) VALUES ( ? , ? , ? ) ;";
try (
PreparedStatement preparedStatement = conn.prepareStatement( sql ) ;
) {
preparedStatement.setObject( 1 , "Alpha" );
// We have to “fake it until we make it”, using a `java.sql.Timestamp` with its value in UTC while pretending it is not in a zone or offset.
// The legacy date-time classes lack a way to represent a date with time-of-day without any time zone or offset-from-UTC.
// The legacy classes have no counterpart to `TIMESTAMP WITHOUT TIME ZONE` in SQL, and have no counterpart to `java.time.LocalDateTime` in Java.
preparedStatement.setObject( 2 , DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 1 , 23 , 12 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() ) );
preparedStatement.setObject( 3 , DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 2 , 23 , 12 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() ) );
preparedStatement.executeUpdate();
preparedStatement.setString( 1 , "Beta" );
preparedStatement.setObject( 2 , DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 4 , 23 , 14 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() ) );
preparedStatement.setObject( 3 , DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 5 , 23 , 14 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() ) );
preparedStatement.executeUpdate();
preparedStatement.setString( 1 , "Gamma" );
preparedStatement.setObject( 2 , DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 11 , 23 , 16 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() ) );
preparedStatement.setObject( 3 , DateTimeUtils.toSqlTimestamp( ZonedDateTime.of( 2018 , 12 , 23 , 16 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() ) );
preparedStatement.executeUpdate();
}
} catch ( SQLException e ) {
e.printStackTrace();
}
}
private void fetchRowsContainingMoment ( java.util.Date moment ) {
// Immediately convert the legacy class `java.util.Date` to a modern `java.time.Instant`.
Instant instant = DateTimeUtils.toInstant( moment );
System.out.println( "instant.toString(): " + instant );
String sql = "SELECT * FROM event_ WHERE ? >= start_ AND ? < stop_ ORDER BY start_ ;";
try (
Connection conn = DriverManager.getConnection( databaseConnectionString ) ;
PreparedStatement pstmt = conn.prepareStatement( sql ) ;
) {
java.sql.Timestamp ts = DateTimeUtils.toSqlTimestamp( instant );
pstmt.setTimestamp( 1 , ts );
pstmt.setTimestamp( 2 , ts );
try ( ResultSet rs = pstmt.executeQuery() ; ) {
while ( rs.next() ) {
//Retrieve by column name
Integer pkey = rs.getInt( "pkey_" );
String name = rs.getString( "name_" );
java.sql.Timestamp start = rs.getTimestamp( "start_" );
java.sql.Timestamp stop = rs.getTimestamp( "stop_" );
// Instantiate a `Course` object for this data.
System.out.println( "Event pkey: " + pkey + " | name: " + name + " | start: " + start + " | stop: " + stop );
}
}
} catch ( SQLException e ) {
e.printStackTrace();
}
}
}
When run.
instant.toString(): 2018-12-04T05:06:02.573Z
Event pkey: 3 | name: Gamma | start: 2018-11-23 16:30:00.0 | stop: 2018-12-23 16:30:00.0
Java 8 without ThreeTen-Backport
And here is that same example, conceptually, but in Java 8 or later where we can use the java.time classes built-in without the ThreeTen-Backport library.
package com.basilbourque.example;
import java.sql.*;
import java.time.*;
public class App {
static final public String databaseConnectionString = "jdbc:h2:mem:localdatetime_example;DB_CLOSE_DELAY=-1"; // The `DB_CLOSE_DELAY=-1` keeps the in-memory database around for multiple connections.
public static void main ( String[] args ) {
App app = new App();
app.doIt();
}
private void doIt ( ) {
System.out.println( "Bonjour tout le monde!" );
this.makeDatabase();
java.util.Date d = new java.util.Date(); // Capture the current moment using terrible old date-time class that is now legacy, supplanted years ago by the class `java.time.Instant`.
this.fetchRowsContainingMoment( d );
}
private void makeDatabase ( ) {
try {
Class.forName( "org.h2.Driver" );
} catch ( ClassNotFoundException e ) {
e.printStackTrace();
}
try (
Connection conn = DriverManager.getConnection( databaseConnectionString ) ; // The `mem` means “In-Memory”, as in “Not persisted to disk”, good for a demo.
Statement stmt = conn.createStatement() ;
) {
String sql = "CREATE TABLE event_ ( \n" +
" pkey_ IDENTITY NOT NULL PRIMARY KEY , \n" +
" name_ VARCHAR NOT NULL , \n" +
" start_ TIMESTAMP WITHOUT TIME ZONE NOT NULL , \n" +
" stop_ TIMESTAMP WITHOUT TIME ZONE NOT NULL \n" +
");";
stmt.execute( sql );
// Insert row.
sql = "INSERT INTO event_ ( name_ , start_ , stop_ ) VALUES ( ? , ? , ? ) ;";
try (
PreparedStatement preparedStatement = conn.prepareStatement( sql ) ;
) {
preparedStatement.setObject( 1 , "Alpha" );
// We have to “fake it until we make it”, using a `java.sql.Timestamp` with its value in UTC while pretending it is not in a zone or offset.
// The legacy date-time classes lack a way to represent a date with time-of-day without any time zone or offset-from-UTC.
// The legacy classes have no counterpart to `TIMESTAMP WITHOUT TIME ZONE` in SQL, and have no counterpart to `java.time.LocalDateTime` in Java.
preparedStatement.setObject( 2 , ZonedDateTime.of( 2018 , 1 , 23 , 12 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
;
preparedStatement.setObject( 3 , ZonedDateTime.of( 2018 , 2 , 23 , 12 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
preparedStatement.executeUpdate();
preparedStatement.setString( 1 , "Beta" );
preparedStatement.setObject( 2 , ZonedDateTime.of( 2018 , 4 , 23 , 14 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
preparedStatement.setObject( 3 , ZonedDateTime.of( 2018 , 5 , 23 , 14 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
preparedStatement.executeUpdate();
preparedStatement.setString( 1 , "Gamma" );
preparedStatement.setObject( 2 , ZonedDateTime.of( 2018 , 11 , 23 , 16 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
preparedStatement.setObject( 3 , ZonedDateTime.of( 2018 , 12 , 23 , 16 , 30 , 0 , 0 , ZoneId.of( "America/Montreal" ) ).toLocalDateTime() );
preparedStatement.executeUpdate();
}
} catch ( SQLException e ) {
e.printStackTrace();
}
}
private void fetchRowsContainingMoment ( java.util.Date moment ) {
// Immediately convert the legacy class `java.util.Date` to a modern `java.time.Instant`.
Instant instant = moment.toInstant();
System.out.println( "instant.toString(): " + instant );
String sql = "SELECT * FROM event_ WHERE ? >= start_ AND ? < stop_ ORDER BY start_ ;";
try (
Connection conn = DriverManager.getConnection( databaseConnectionString ) ;
PreparedStatement pstmt = conn.prepareStatement( sql ) ;
) {
pstmt.setObject( 1 , instant );
pstmt.setObject( 2 , instant );
try ( ResultSet rs = pstmt.executeQuery() ; ) {
while ( rs.next() ) {
//Retrieve by column name
Integer pkey = rs.getInt( "pkey_" );
String name = rs.getString( "name_" );
Instant start = rs.getObject( "start_" , OffsetDateTime.class ).toInstant();
Instant stop = rs.getObject( "stop_" , OffsetDateTime.class ).toInstant();
// Instantiate a `Course` object for this data.
System.out.println( "Event pkey: " + pkey + " | name: " + name + " | start: " + start + " | stop: " + stop );
}
}
} catch ( SQLException e ) {
e.printStackTrace();
}
}
}
When run.
instant.toString(): 2018-12-04T05:10:54.635Z
Event pkey: 3 | name: Gamma | start: 2018-11-24T00:30:00Z | stop: 2018-12-24T00:30:00Z
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.
Where to obtain the java.time classes?
Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
Java 9 adds some minor features and fixes.
Java SE 6 and Java SE 7
Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
Android
Later versions of Android bundle implementations of the java.time classes.
For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.
I was writing a program to convert string to java.util.Date object and then store the same in the database, the program is running correctly with no errors. But in the MySql the date is not stored correctly, java is storing date which is a yesterday date with respect to date written in the string.
Example- If I write "2018-11-10", it stores "2018-11-09".
My code-
import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class Demo {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Connection con;
Class.forName("com.mysql.cj.jdbc.Driver");
Date date = Date.valueOf("2018-11-20");
con = DriverManager.getConnection("jdbc:mysql://localhost:9090/ProjectScheduler", "root", "tiger");
PreparedStatement prepare = con.prepareStatement("insert into SchedulerDB(Date) values(?)");
prepare.setDate(1, date);
prepare.executeUpdate();
}
}
In MySql workbench, I created table as
create table SchedulerDB(Date date)
when I run select query to see data in table using
select * from SchedulerDB
Then under the column "Date" in the table- "SchedulerDB", I got 2018-11-19. But I want 2018-11-20 as this is what I wrote in java code. Please help me in solving this problem.
tl;dr
myPreparedStatement.setObject(
… ,
LocalDate.parse( "2018-11-20")
) ;
Details
You are using the terribly designed java.sql.Date class. This class unfortunately is a subclass of java.util.Date though the documentation strangely tells us to ignore that fact of inheritance. So while java.sql.Date pretends to represent a date only, it is in fact a moment, a date with a time-of-day set to midnight in some zone and then adjusted to UTC. This is a wretched design, and is one of many reasons to never use the legacy date-time classes.
For a DATE column in SQL, use the modern LocalDate class instead.
LocalDate
The LocalDate class truly represents a date-only value, without time-of-day and without time zone or offset-from-UTC.
For database column of a type akin to the SQL-standard DATE type, use the LocalDate class in Java.
LocalDate ld = LocalDate.parse( "2018-11-20" );
JDBC 4.2
As of JDBC 4.2, we can directly exchange java.time objects with the database.
myPreparedStatement.setObject( … , ld ) ;
Retrieval:
LocalDate ld = myResultSet.getObject( … , LocalDate.class ) ;
With no time zone involved, your retrieved value will be the same 2018-11-20 that you stored.
Example app
Here is a complete example app using the H2 Database Engine. As a demo, I have it set to write to an in-memory database rather than persisting to actual storage.
We create a database with a single table, with two columns:
a sequential number primary key named pkey_
a DATE column named when_
We insert a couple LocalDate objects, and then retrieve the rows to dump to console.
package com.basilbourque.example;
import java.sql.*;
import java.time.LocalDate;
import java.util.List;
public class EventDate {
public static void main ( String[] args ) {
EventDate app = new EventDate();
app.doIt();
}
private void doIt ( ) {
final String driverName = "org.h2.Driver";
final String catalogName = "event_demo_db";
final String jdbcPath = "jdbc:h2:mem:" + catalogName + ";DB_CLOSE_DELAY=-1"; // Set delay to keep in-memory database even after last connection closed.
// Verify JDBC driver.
try {
Class.forName( driverName );
} catch ( ClassNotFoundException e ) {
e.printStackTrace();
}
// Connect, and create database.
try (
Connection conn = DriverManager.getConnection( jdbcPath ) ;
) {
String sql = null;
// Create table.
try ( Statement stmt = conn.createStatement() ; ) {
sql = "CREATE TABLE " + "event_" + " ( \n" +
" pkey_ IDENTITY PRIMARY KEY , \n" +
" when_ DATE NOT NULL \n" +
") ; \n";
System.out.println( "TRACE - SQL:\n" + sql );
stmt.execute( sql );
}
System.out.println( "TRACE - Created table `event_`." );
// Add rows
sql = "INSERT INTO event_ ( when_ ) \n" +
"VALUES ( ? ) " +
"; ";
List < LocalDate > dates = List.of( LocalDate.parse( "2018-11-10" ) , LocalDate.parse( "2018-12-31" ) );
System.out.println( "Inserting list of LocalDate objects: " + dates );
try (
PreparedStatement ps = conn.prepareStatement( sql ) ;
) {
for ( LocalDate localDate : dates ) {
ps.setObject( 1 , localDate );
ps.executeUpdate();
}
}
// Retrieve rows
sql = "SELECT * FROM " + "event_" + " ;";
try (
Statement stmt = conn.createStatement() ;
ResultSet rs = stmt.executeQuery( sql ) ;
) {
while ( rs.next() ) {
int pkey = rs.getInt( "pkey_" );
LocalDate when = rs.getObject( "when_" , LocalDate.class );
System.out.println( "Row pkey_: " + pkey + " when_: " + when );
}
}
} catch ( SQLException e ) {
e.printStackTrace();
}
}
}
When run.
Inserting list of LocalDate objects: [2018-11-10, 2018-12-31]
Row pkey_: 1 when_: 2018-11-10
Row pkey_: 2 when_: 2018-12-31
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.
Where to obtain the java.time classes?
Java SE 8, Java SE 9, Java SE 10, Java SE 11, and later - Part of the standard Java API with a bundled implementation.
Java 9 adds some minor features and fixes.
Java SE 6 and Java SE 7
Most of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
Android
Later versions of Android bundle implementations of the java.time classes.
For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.
I am pragmatically building a nested JSON from a table. Json looks something like this:
{"date":"03-16-2018 06:57:02",
"details":
[
{
"motorstate":0,
"startTime":"03-16-2018 20:41:57",
},
{
"motorstate":0,
"startTime":"03-16-2018 06:57:02",
}
]
},
{"date":"03-15-2018 08:08:48",
"details":
[
{
"motorstate":0,
"startTime":"03-16-2018 03:53:30",
}
]
}
If you look into the above example, the second record:
{"date":"03-15-2018 08:08:48",
"details":
[
{
"motorstate":0,
"startTime":"03-16-2018 03:53:30",
}
]
}
The dates are mismatching. This is because the date shown here is in IST but actually stored in UTC in my Google Datastore. In UTC, its still 03-15-2018 and not 03-16-2018.
My question is how can we perform a date difference in different timezones other than UTC in Java? The Date.getTime() method always give the difference in UTC and not in local Timezone.
tl;dr
Date difference in different timezones in java
Period.between(
LocalDateTime.parse(
"03-15-2018 08:08:48" ,
DateTimeFormatter.ofPattern( “MM-dd-uuuu HH:mm:ss” )
)
.atZone( ZoneId.of( ”Asia/Kolkata" ) )
.toLocalDate()
,
LocalDate.now( ZoneId.of( ”Asia/Kolkata" ) )
)
Details
The modern approach uses the java.time classes rather than the troublesome old me old legacy date-time classes such as Date and Calendar.
Tip: Avoid custom formatting patterns when serializing date-time values to text. Use standard ISO 8601 formats only.
Tip: when exchanging date-time values as text, always include an indicator of the offset-from-UTC and the time zone name.
First, parse your input strings as LocalDateTime because they lack any indication of offset or zone.
Define a formatting pattern to match input.
DateTimeFormatter f = DateTimeFormatter.ofPattern( “MM-dd-uuuu HH:mm:ss” ) ;
Parse.
String input = "03-15-2018 08:08:48" ;
LocalDateTime ldt = LocalDateTime.parse( input , f ) ;
You claim to know that these inputs were intended to represent a moment in India time. Apply a ZoneId to get a ZonedDateTime.
ZoneId z = ZoneId.of( ”Asia/Kolkata" ) ;
ZonedDateTime zdt = ldt.atZone( z ) ;
To get the date-only value, extract a LocalDate.
LocalDate ld = zdt.toLocalDate() ;
To represent the delta between dates as a number of years, months, and days unattached to the timeline, use Period.
Period p = Period.between( ldt , LocalDate.now( z ) ) ;
For a count of total days.
long days = ChronoUnit.DAYS.between( start , stop ) ;
About java.time
The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.
The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.
To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.
You may exchange java.time objects directly with your database. Use a JDBC driver compliant with JDBC 4.2 or later. No need for strings, no need for java.sql.* classes.
Where to obtain the java.time classes?
Java SE 8, Java SE 9, and later
Built-in.
Part of the standard Java API with a bundled implementation.
Java 9 adds some minor features and fixes.
Java SE 6 and Java SE 7
Much of the java.time functionality is back-ported to Java 6 & 7 in ThreeTen-Backport.
Android
Later versions of Android bundle implementations of the java.time classes.
For earlier Android (<26), the ThreeTenABP project adapts ThreeTen-Backport (mentioned above). See How to use ThreeTenABP….
The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.
public String getSummary(String deviceGuid)
{
Query<com.google.cloud.datastore.Entity> query
= Query.newEntityQueryBuilder()
.setKind("sensordata")
.setFilter(PropertyFilter.eq("deviceguid", deviceGuid))
.setOrderBy(OrderBy.desc("startTime"))
.build();
QueryResults<com.google.cloud.datastore.Entity> resultList =
datastore.run(query);
if(!resultList.hasNext())
{
log.warning("No record found..");
return "No record found";
}
com.google.cloud.datastore.Entity e =null;
com.google.cloud.datastore.Entity pe =null;
SensorDataOut sensorDataOut = new SensorDataOut();
SensorSummaryDataOut summary = new SensorSummaryDataOut();
TotalSummaryDataOut totalSummary = new TotalSummaryDataOut();
Calendar calendar = Calendar.getInstance();
calendar.setTimeZone(TimeZone.getTimeZone("IST"));
Calendar calendar1 = Calendar.getInstance();
calendar1.setTimeZone(TimeZone.getTimeZone("IST"));
SimpleDateFormat sdf = new SimpleDateFormat("MM-dd-yyyy HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("IST"));
SimpleDateFormat sdf1 = new SimpleDateFormat("MM-dd-yyyy");
//sdf.setTimeZone(TimeZone.getTimeZone("IST"));
long stopTime;
long startTime;
long pStartTime;
long diffTime;
long diffDay;
Date pDateWithoutTime;
Date eDateWithoutTime;
while(resultList.hasNext())
{
e = resultList.next();
startTime = (e.contains("startTime"))?e.getTimestamp("startTime").toSqlTimestamp().getTime():0;
stopTime = (e.contains("stopTime"))?e.getTimestamp("stopTime").toSqlTimestamp().getTime():0;
//log.info("Start Date : " + e.getTimestamp("startTime").toString() + " - " + String.valueOf(startTime));
//log.info("Stop Date : " + e.getTimestamp("stopTime").toString() + " - " + String.valueOf(stopTime));
//log.info("Usage Volume :" + String.valueOf(e.getLong("usageVolume")));
sensorDataOut = new SensorDataOut();
calendar.setTimeInMillis(stopTime);
sensorDataOut.stopTime = sdf.format(calendar.getTime());
calendar.setTimeInMillis(startTime);
sensorDataOut.startTime = sdf.format(calendar.getTime());
sensorDataOut.motorstate = (e.contains("motorstate"))?(int)e.getLong("motorstate"):-1;
sensorDataOut.startVolume = (e.contains("startVolume"))?(int)e.getLong("startVolume"):-1;
sensorDataOut.stopVolume = (e.contains("stopVolume"))?(int)e.getLong("stopVolume"):-1;
sensorDataOut.usageTime = (e.contains("usageTime"))?e.getLong("usageTime"):-1;
sensorDataOut.usageVolume = (e.contains("usageVolume"))?(int)e.getLong("usageVolume"):-1;
if(pe!=null)
{
//Get the date difference in terms of days. If it is same day then add the volume consumed
pStartTime= pe.getTimestamp("startTime").toSqlTimestamp().getTime();
try{
calendar.setTimeInMillis(pStartTime);
pDateWithoutTime = sdf1.parse(sdf1.format(calendar.getTime()));
calendar1.setTimeInMillis(e.getTimestamp("startTime").toSqlTimestamp().getTime());
eDateWithoutTime = sdf1.parse(sdf1.format(calendar1.getTime()));
}
catch(Exception ex){
log.info("Exception while parsing date");
continue;
}
diffTime = Math.abs(pDateWithoutTime.getTime() - eDateWithoutTime.getTime());
diffDay = TimeUnit.DAYS.convert(diffTime, TimeUnit.MILLISECONDS);
//log.info("pDateWithoutTime: " + pDateWithoutTime + ", eDateWithoutTime: " + eDateWithoutTime + ", consumedVolume: "
// + sensorDataOut.usageVolume;
if(diffDay!=0) //If not same day
{
totalSummary.totVolume = totalSummary.totVolume + summary.totVolume;
totalSummary.totDuration = totalSummary.totDuration + summary.totDuration;
totalSummary.details.add(summary);
summary = new SensorSummaryDataOut();
}
}
summary.date = sensorDataOut.startTime;
summary.totVolume = summary.totVolume + sensorDataOut.usageVolume;
summary.totDuration = summary.totDuration + sensorDataOut.usageTime;
summary.details.add(sensorDataOut);
pe = e;
}
if(summary.details.size()>0)
{
totalSummary.totVolume = totalSummary.totVolume + summary.totVolume;
totalSummary.totDuration = totalSummary.totDuration + summary.totDuration;
totalSummary.details.add(summary);
summary = new SensorSummaryDataOut();
}
totalSummary.avgVolume = totalSummary.totVolume/totalSummary.details.size();
totalSummary.deviceguid = deviceGuid;
String json = "";
Gson gson = new Gson();
json = gson.toJson(totalSummary);
return json;
} //End of Function
Below method will return you your UTC time to current Timezone and this worked for me
public static Date getLocalDateObjFromUTC(String date, String time) throws ParseException {
String dateAndTimeInUTC = date + " " + time;
SimpleDateFormat localDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
localDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
Date dateInLocalTimeZone = localDateFormat.parse(dateAndTimeInUTC);
return dateInLocalTimeZone;
}
Have a table setup in MySQL that holds data for an event (e.g. a music show or karaoke) at a particular place of business.
So my table looks like this:
CREATE TABLE `event_schedule` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`description` varchar(30) DEFAULT NULL,
`event_id` varchar(30) NOT NULL DEFAULT,
`event_date` datetime DEFAULT NULL,
`event_date_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00'
)
Database table contains rows like this:
| 1 | karoake | aab3223 | 2017-08-15 00:00:00 | 2017-08-15 16:00:00 |
| 2 | comedy | cce8465 | 2017-08-25 00:00:00 | 2017-08-25 19:00:00 |
The event_date is in UTC Time format as well is the MySQL server that it resides in.
select now()
Will give time in UTC...
Have logic setup which uses a Spring JDBC to check if a particular row exists based on curdate() like this:
#Repository
public class EventDao {
private static JdbcTemplate jdbcTemplate = null;
#Autowired
public EventDao(#Qualifier("aDataSource") DataSource dataSource) {
EventDao.jdbcTemplateObject = new JdbcTemplate(dataSource);
}
public static String getEventId() {
SqlRowSet eventRowSet = null;
String eventId = "";
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
dateInUTC = formatter.format(new Date());
eventRowSet = jdbcTemplate.queryForRowSet(
"select * from event_schedule where (event_date=CURDATE() or event_date=?)",
new Object[] { dateInUTC });
if (eventRowSet != null && eventRowSet.next()) {
eventId = rst.getString("event_id");
}
return eventId;
}
}
A class used to check if the event is under way:
public class EventProcessor {
public static String getEventStatus() {
String eventId = getEventId();
if (eventId != null) {
return "Event Started";
}
else {
return "Event not Started"
}
// TODO: Need to incorporate for two hours after its over.
// "Event Ended"
}
}
This system seemed to be working when the requirements dependent on just the event_date in UTC was simply:
2017-08-15 00:00:00
New Use Case(s) / Question(s):
If a client app connects to this web service and the situation is:
event starts 2017-08-15 19:00:00 (lets say this is 7:00 p.m PST) how can I set it up do this:
Have the service return "Event Not Started" from 2017-08-14 00:00 to 2017-08-15 18:59:59 ?
Meaning, from the midnight of the same date until one minute before it starts?
Have the service return "Event Ended" two hours after the event ends?
A lot of my client apps that connect to this web service might be in the West Coast (PST), Central (CST), or Eastern (EST).
What is the best way to do a time conversion for them (since I am using UTC Datetimes in the MySQL Database and the MySQL Database is in UTC itself)?
This has been addressed many times already from different angles on Stack Overflow. So search for more info.
Briefly…
I don't use Spring, so here is the basic JDBC to guide you.
Use only java.time classes. Avoid the troublesome old legacy date-time classes such as Date, Calendar, and the java.sql classes.
Store your date-times in UTC if they are past moments in history, or specific moments coming up soon. Query in UTC. Present in user’s expected time zone.
If scheduling more that several weeks out, you may want to use a date-time value without a zone or offset, as politicians frequently re-define time zones with little advance notice. I'll ignore that here.
Specify a proper time zone name in the format of continent/region, such as America/Montreal, Africa/Casablanca, or Pacific/Auckland. Never use the 3-4 letter abbreviation such as PST or IST as they are not true time zones, not standardized, and not even unique(!).
ZoneId z = ZoneId.of( "America/Los_Angeles" ) ;
LocalDate startDate = LocalDate.of( 2017 , 8 , 15 ) ;
LocalTime startTime = LocalTime.of( 19 , 0 ) ;
ZonedDateTime start = ZonedDateTime.of( startDate , startTime , z ) ;
Duration d = Duration.ofHours( 2 ) ;
ZonedDateTime stop = start.plus( d ) ;
If you want to store the Duration, call toString and store its generated String using standard ISO 8601 format.
Half-Open logic
Use Half-Open logic, where beginning is inclusive while the ending is exclusive. This entirely avoids your mess with “from the midnight of the same date until one minute before it starts?”.
String sql = "INSERT INTO event ( start , stop , duration , … ) VALUES ( ? , ? , ? , ...) ;" ;
… make prepared statement
myPreparedStatement.setObject( 1 , start.toInstant() ) ;
myPreparedStatement.setObject( 2 , stop.toInstant() ) ;
myPreparedStatement.setObject( 3 , duration.toString() ) ;
myPreparedStatement.setObject( … , … ) ;
Query is similar.
String sql = "SELECT start , stop , duration , pkey , … FROM event WHERE pkey = ? ;" ) ;
… execute query, get ResultSet
… loop the resulting rows
Instant start = myResultSet.get( 1 , Instant.class ) ;
Instant stop = myResultSet.get( 2 , Instant.class ) ;
Duration d = Duration.parse( myResultSet.getString( 3 ) ) ;
Compare to the current moment.
The Instant class represents a moment on the timeline in UTC with a resolution of nanoseconds (up to nine (9) digits of a decimal fraction).
Instant now = Instant.now() ;
Boolean eventScheduled = now.isBefore( start ) ;
Boolean eventOccurring = ( ! now.isBefore( start ) ) && now.isBefore( stop ) ;
Boolean eventExpired = ! now.isBefore( stop ) ; // "Not before stop" means either (a ) now is exactly the moment of expiration (using half-open logic we consider that past), or (b) now is later, after the moment of expiration.
If you want to query for events not yet started:
String sql = "SELECT start , stop , duration , pkey , … FROM event WHERE start > ? ; " ) ; // Find events starting after the passed moment.
…
myPreparedStatement.setObject( 1 , Instant.now() ) ;
The above code assumes your JDBC driver complies with JDBC 4.2 or later for supporting java.time types directly.