Skip to content

Commit c7b545c

Browse files
authored
Fix date conversion (spring-attic#2070)
* fix date conversion; fixes spring-attic#2067
1 parent 850766f commit c7b545c

File tree

6 files changed

+183
-50
lines changed

6 files changed

+183
-50
lines changed

docs/src/main/asciidoc/spanner.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,8 @@ Natively supported types:
441441
* `java.util.Date`
442442
* `java.util.Instant`
443443
* `java.sql.Date`
444+
* `java.time.LocalDate`
445+
* `java.time.LocalDateTime`
444446

445447

446448
==== Lists

spring-cloud-gcp-data-spanner/src/main/java/org/springframework/cloud/gcp/data/spanner/core/convert/SpannerConverters.java

+79-21
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import java.sql.Date;
2020
import java.time.Instant;
21+
import java.time.LocalDate;
22+
import java.time.LocalDateTime;
2123
import java.util.Arrays;
2224
import java.util.Calendar;
2325
import java.util.Collection;
@@ -80,38 +82,91 @@ public java.sql.Date convert(com.google.cloud.Date date) {
8082
};
8183

8284
/**
83-
* A converter from {@link java.util.Date} to the Spanner date type.
85+
* A converter from {@link LocalDate} to the Spanner date type.
8486
*/
8587
// @formatter:off
86-
public static final Converter<java.util.Date, com.google.cloud.Date>
87-
JAVA_TO_SPANNER_DATE_CONVERTER = new Converter<java.util.Date, com.google.cloud.Date>() {
88+
public static final Converter<LocalDate, com.google.cloud.Date>
89+
LOCAL_DATE_TIMESTAMP_CONVERTER = new Converter<LocalDate, com.google.cloud.Date>() {
8890
// @formatter:on
8991
@Nullable
9092
@Override
91-
public com.google.cloud.Date convert(java.util.Date date) {
92-
Calendar cal = Calendar.getInstance();
93-
cal.setTime(date);
94-
return com.google.cloud.Date.fromYearMonthDay(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1,
95-
cal.get(Calendar.DAY_OF_MONTH));
93+
public com.google.cloud.Date convert(LocalDate date) {
94+
return com.google.cloud.Date.fromYearMonthDay(date.getYear(), date.getMonthValue(), date.getDayOfMonth());
9695
}
9796
};
9897

9998
/**
100-
* A converter from the Spanner date type to {@link java.util.Date}.
99+
* A converter from the Spanner date type to {@link LocalDate}.
101100
*/
102101
// @formatter:off
103-
public static final Converter<com.google.cloud.Date, java.util.Date> SPANNER_TO_JAVA_DATE_CONVERTER =
104-
new Converter<com.google.cloud.Date, java.util.Date>() {
102+
public static final Converter<com.google.cloud.Date, LocalDate> TIMESTAMP_LOCAL_DATE_CONVERTER =
103+
new Converter<com.google.cloud.Date, LocalDate>() {
105104
// @formatter:on
106105
@Nullable
107106
@Override
108-
public java.util.Date convert(com.google.cloud.Date date) {
109-
Calendar cal = Calendar.getInstance();
110-
cal.set(date.getYear(), date.getMonth() - 1, date.getDayOfMonth());
111-
return cal.getTime();
107+
public LocalDate convert(com.google.cloud.Date date) {
108+
return LocalDate.of(date.getYear(), date.getMonth(), date.getDayOfMonth());
112109
}
113110
};
114111

112+
/**
113+
* A converter from {@link LocalDateTime} to the Spanner timestamp type.
114+
*/
115+
// @formatter:off
116+
public static final Converter<LocalDateTime, Timestamp>
117+
LOCAL_DATE_TIME_TIMESTAMP_CONVERTER = new Converter<LocalDateTime, Timestamp>() {
118+
// @formatter:on
119+
@Nullable
120+
@Override
121+
public Timestamp convert(LocalDateTime dateTime) {
122+
return JAVA_TO_SPANNER_TIMESTAMP_CONVERTER.convert(java.sql.Timestamp.valueOf(dateTime));
123+
}
124+
};
125+
126+
/**
127+
* A converter from the Spanner timestamp type to {@link LocalDateTime}.
128+
*/
129+
// @formatter:off
130+
public static final Converter<Timestamp, LocalDateTime> TIMESTAMP_LOCAL_DATE_TIME_CONVERTER =
131+
new Converter<Timestamp, LocalDateTime>() {
132+
// @formatter:on
133+
@Nullable
134+
@Override
135+
public LocalDateTime convert(Timestamp timestamp) {
136+
return SPANNER_TO_JAVA_TIMESTAMP_CONVERTER.convert(timestamp).toLocalDateTime();
137+
}
138+
};
139+
140+
/**
141+
* A converter from {@link java.util.Date} to the Spanner timestamp type.
142+
*/
143+
// @formatter:off
144+
public static final Converter<java.util.Date, Timestamp>
145+
DATE_TIMESTAMP_CONVERTER = new Converter<java.util.Date, Timestamp>() {
146+
// @formatter:on
147+
@Nullable
148+
@Override
149+
public Timestamp convert(java.util.Date date) {
150+
long time = date.getTime();
151+
long secs = Math.floorDiv(time, 1000L);
152+
int nanos = Math.toIntExact((time - secs * 1000L) * 1000000L);
153+
return Timestamp.ofTimeSecondsAndNanos(secs, nanos);
154+
}
155+
};
156+
157+
/**
158+
* A converter from the Spanner timestamp type to {@link java.util.Date}.
159+
*/
160+
// @formatter:off
161+
public static final Converter<Timestamp, java.util.Date> TIMESTAMP_DATE_CONVERTER =
162+
new Converter<Timestamp, java.util.Date>() {
163+
// @formatter:on
164+
@Nullable
165+
@Override
166+
public java.util.Date convert(Timestamp timestamp) {
167+
return timestamp.toDate();
168+
}
169+
};
115170
/**
116171
* A converter from {@link Instant} to the Spanner instantaneous time type.
117172
*/
@@ -151,8 +206,7 @@ public Instant convert(Timestamp timestamp) {
151206
@Override
152207
public Timestamp convert(java.sql.Timestamp timestamp) {
153208
long secs = Math.floorDiv(timestamp.getTime(), 1000L);
154-
int nanos = timestamp.getNanos();
155-
return Timestamp.ofTimeSecondsAndNanos(secs, nanos);
209+
return Timestamp.ofTimeSecondsAndNanos(secs, timestamp.getNanos());
156210
}
157211
};
158212

@@ -201,18 +255,22 @@ public byte[] convert(ByteArray bytes) {
201255
/** Converters from common types to those used by Spanner. */
202256
public static final Collection<Converter> DEFAULT_SPANNER_WRITE_CONVERTERS = Collections.unmodifiableCollection(
203257
Arrays.asList(
204-
JAVA_TO_SPANNER_DATE_CONVERTER,
258+
DATE_TIMESTAMP_CONVERTER,
205259
INSTANT_TIMESTAMP_CONVERTER,
206260
JAVA_TO_SPANNER_BYTE_ARRAY_CONVERTER,
207261
JAVA_TO_SPANNER_TIMESTAMP_CONVERTER,
208-
JAVA_SQL_TO_SPANNER_DATE_CONVERTER));
262+
JAVA_SQL_TO_SPANNER_DATE_CONVERTER,
263+
LOCAL_DATE_TIMESTAMP_CONVERTER,
264+
LOCAL_DATE_TIME_TIMESTAMP_CONVERTER));
209265

210266
/** Converters from common types to those used by Spanner. */
211267
public static final Collection<Converter> DEFAULT_SPANNER_READ_CONVERTERS = Collections.unmodifiableCollection(
212268
Arrays.asList(
213-
SPANNER_TO_JAVA_DATE_CONVERTER,
269+
TIMESTAMP_DATE_CONVERTER,
214270
TIMESTAMP_INSTANT_CONVERTER,
215271
SPANNER_TO_JAVA_BYTE_ARRAY_CONVERTER,
216272
SPANNER_TO_JAVA_TIMESTAMP_CONVERTER,
217-
SPANNER_TO_JAVA_SQL_DATE_CONVERTER));
273+
SPANNER_TO_JAVA_SQL_DATE_CONVERTER,
274+
TIMESTAMP_LOCAL_DATE_CONVERTER,
275+
TIMESTAMP_LOCAL_DATE_TIME_CONVERTER));
218276
}

spring-cloud-gcp-data-spanner/src/test/java/org/springframework/cloud/gcp/data/spanner/core/convert/ConverterAwareMappingSpannerEntityProcessorTests.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.cloud.gcp.data.spanner.core.convert;
1818

1919
import java.time.Instant;
20+
import java.time.LocalDate;
2021
import java.util.ArrayList;
2122
import java.util.Arrays;
2223
import java.util.List;
@@ -83,8 +84,9 @@ public void canConvertDefaultTypesNoCustomConverters() {
8384
ConverterAwareMappingSpannerEntityProcessor converter = new ConverterAwareMappingSpannerEntityProcessor(
8485
new SpannerMappingContext());
8586

86-
verifyCanConvert(converter, java.util.Date.class, Date.class);
87+
verifyCanConvert(converter, java.util.Date.class, Timestamp.class);
8788
verifyCanConvert(converter, Instant.class, Timestamp.class);
89+
verifyCanConvert(converter, LocalDate.class, Date.class);
8890
}
8991

9092
@Test
@@ -93,7 +95,8 @@ public void canConvertDefaultTypesCustomConverters() {
9395
new SpannerMappingContext(), Arrays.asList(JAVA_TO_SPANNER),
9496
Arrays.asList(SPANNER_TO_JAVA));
9597

96-
verifyCanConvert(converter, java.util.Date.class, Date.class);
98+
verifyCanConvert(converter, java.util.Date.class, Timestamp.class);
99+
verifyCanConvert(converter, LocalDate.class, Date.class);
97100
verifyCanConvert(converter, Instant.class, Timestamp.class);
98101
verifyCanConvert(converter, JavaType.class, SpannerType.class);
99102
}

spring-cloud-gcp-data-spanner/src/test/java/org/springframework/cloud/gcp/data/spanner/core/mapping/SpannerConvertersTest.java

+42-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
package org.springframework.cloud.gcp.data.spanner.core.mapping;
1818

19+
import java.time.Instant;
20+
import java.time.LocalDate;
21+
import java.time.LocalDateTime;
22+
1923
import com.google.cloud.ByteArray;
2024
import com.google.cloud.Date;
2125
import com.google.cloud.Timestamp;
@@ -32,11 +36,46 @@
3236
*/
3337
public class SpannerConvertersTest {
3438

39+
@Test
40+
public void localDateTimeConversionTest() {
41+
LocalDateTime dateTime = LocalDateTime.now();
42+
assertThat(SpannerConverters.TIMESTAMP_LOCAL_DATE_TIME_CONVERTER
43+
.convert(SpannerConverters.LOCAL_DATE_TIME_TIMESTAMP_CONVERTER.convert(dateTime))).isEqualTo(dateTime);
44+
}
45+
46+
@Test
47+
public void localDateTimeConversionPreEpochTest() {
48+
LocalDateTime dateTime = LocalDateTime.of(600, 12, 1, 2, 3, 4, 5);
49+
assertThat(SpannerConverters.TIMESTAMP_LOCAL_DATE_TIME_CONVERTER
50+
.convert(SpannerConverters.LOCAL_DATE_TIME_TIMESTAMP_CONVERTER.convert(dateTime))).isEqualTo(dateTime);
51+
}
52+
3553
@Test
3654
public void dateConversionTest() {
37-
Date date = Date.fromYearMonthDay(2018, 3, 29);
38-
assertThat(SpannerConverters.JAVA_TO_SPANNER_DATE_CONVERTER
39-
.convert(SpannerConverters.SPANNER_TO_JAVA_DATE_CONVERTER.convert(date))).isEqualTo(date);
55+
Timestamp timestamp = Timestamp.now();
56+
assertThat(SpannerConverters.DATE_TIMESTAMP_CONVERTER
57+
.convert(SpannerConverters.TIMESTAMP_DATE_CONVERTER.convert(timestamp))).isEqualTo(timestamp);
58+
}
59+
60+
@Test
61+
public void dateConversionPreEpochTest() {
62+
java.util.Date timestamp = java.util.Date.from(Instant.ofEpochSecond(-12345678, -123));
63+
assertThat(SpannerConverters.TIMESTAMP_DATE_CONVERTER
64+
.convert(SpannerConverters.DATE_TIMESTAMP_CONVERTER.convert(timestamp))).isEqualTo(timestamp);
65+
}
66+
67+
@Test
68+
public void localDateConversionTest() {
69+
LocalDate localDate = LocalDate.now();
70+
assertThat(SpannerConverters.TIMESTAMP_LOCAL_DATE_CONVERTER
71+
.convert(SpannerConverters.LOCAL_DATE_TIMESTAMP_CONVERTER.convert(localDate))).isEqualTo(localDate);
72+
}
73+
74+
@Test
75+
public void localDateConversionPreEpochTest() {
76+
LocalDate localDate = LocalDate.of(600, 12, 1);
77+
assertThat(SpannerConverters.TIMESTAMP_LOCAL_DATE_CONVERTER
78+
.convert(SpannerConverters.LOCAL_DATE_TIMESTAMP_CONVERTER.convert(localDate))).isEqualTo(localDate);
4079
}
4180

4281
@Test

spring-cloud-gcp-data-spanner/src/test/java/org/springframework/cloud/gcp/data/spanner/test/domain/Trade.java

+35-24
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
package org.springframework.cloud.gcp.data.spanner.test.domain;
1818

1919
import java.time.Instant;
20+
import java.time.LocalDate;
21+
import java.time.LocalDateTime;
2022
import java.util.ArrayList;
2123
import java.util.Date;
2224
import java.util.List;
2325
import java.util.Objects;
2426
import java.util.UUID;
2527

26-
import org.assertj.core.util.DateUtil;
27-
2828
import org.springframework.cloud.gcp.data.spanner.core.mapping.Column;
2929
import org.springframework.cloud.gcp.data.spanner.core.mapping.Embedded;
3030
import org.springframework.cloud.gcp.data.spanner.core.mapping.Interleaved;
@@ -48,6 +48,10 @@ public class Trade {
4848

4949
private Date tradeDate;
5050

51+
private LocalDate tradeLocalDate;
52+
53+
private LocalDateTime tradeLocalDateTime;
54+
5155
private String action;
5256

5357
private String symbol;
@@ -88,6 +92,9 @@ public static Trade aTrade() {
8892
t.traderId = traderId;
8993
t.tradeTime = Instant.ofEpochSecond(333);
9094
t.tradeDate = Date.from(t.tradeTime);
95+
t.tradeLocalDate = LocalDate.of(2015, 1, 1);
96+
t.tradeLocalDateTime = LocalDateTime.of(2015, 1, 1, 2, 3, 4, 5);
97+
t.subTrades = new ArrayList<>();
9198
t.tradeDetail.price = 100.0;
9299
t.tradeDetail.shares = 12345.6;
93100
for (int i = 1; i <= 5; i++) {
@@ -105,26 +112,23 @@ public boolean equals(Object o) {
105112
return false;
106113
}
107114
Trade trade = (Trade) o;
108-
return Objects.equals(this.tradeDetail.id, trade.tradeDetail.id)
109-
&& Objects.equals(this.age, trade.age)
110-
&& Objects.equals(this.action, trade.action)
111-
&& Objects.equals(this.tradeDetail.price, trade.tradeDetail.price)
112-
&& Objects.equals(this.tradeDetail.shares, trade.tradeDetail.shares)
113-
&& Objects.equals(this.symbol, trade.symbol)
114-
&& Objects.equals(this.tradeTime, trade.tradeTime)
115-
&& Objects.equals(this.traderId, trade.traderId)
116-
// java Date contains the time of day, but Cloud Spanner Date is only specific
117-
// to the day.
118-
&& Objects.equals(DateUtil.truncateTime(this.tradeDate),
119-
DateUtil.truncateTime(trade.tradeDate));
115+
return getAge() == trade.getAge() &&
116+
Objects.equals(getTradeTime(), trade.getTradeTime()) &&
117+
Objects.equals(getTradeDate(), trade.getTradeDate()) &&
118+
Objects.equals(this.tradeLocalDate, trade.tradeLocalDate) &&
119+
Objects.equals(this.tradeLocalDateTime, trade.tradeLocalDateTime) &&
120+
Objects.equals(getAction(), trade.getAction()) &&
121+
Objects.equals(getSymbol(), trade.getSymbol()) &&
122+
Objects.equals(getTradeDetail(), trade.getTradeDetail()) &&
123+
Objects.equals(getTraderId(), trade.getTraderId()) &&
124+
Objects.equals(getExecutionTimes(), trade.getExecutionTimes()) &&
125+
Objects.equals(getSubTrades(), trade.getSubTrades());
120126
}
121127

122128
@Override
123129
public int hashCode() {
124-
return Objects.hash(this.tradeDetail.id, this.age, this.action,
125-
this.tradeDetail.price, this.tradeDetail.shares, this.symbol,
126-
this.tradeTime, DateUtil.truncateTime(this.tradeDate),
127-
this.traderId, this.executionTimes);
130+
return Objects.hash(getAge(), getTradeTime(), getTradeDate(), this.tradeLocalDate, this.tradeLocalDateTime,
131+
getAction(), getSymbol(), getTradeDetail(), getTraderId(), getExecutionTimes(), getSubTrades());
128132
}
129133

130134
public String getId() {
@@ -209,12 +213,19 @@ public void setTradeDetail(TradeDetail tradeDetail) {
209213

210214
@Override
211215
public String toString() {
212-
return "Trade{" + "id='" + this.tradeDetail.id + '\'' + ", action='" + this.action
213-
+ '\'' + ", age=" + this.age + ", price=" + this.tradeDetail.price
214-
+ ", shares=" + this.tradeDetail.shares + ", symbol='"
215-
+ this.symbol + ", tradeTime="
216-
+ this.tradeTime + ", tradeDate='" + DateUtil.truncateTime(this.tradeDate)
217-
+ '\'' + ", traderId='" + this.traderId + '\'' + '}';
216+
return "Trade{" +
217+
"age=" + this.age +
218+
", tradeTime=" + this.tradeTime +
219+
", tradeDate=" + this.tradeDate +
220+
", tradeLocalDate=" + this.tradeLocalDate +
221+
", tradeLocalDateTime=" + this.tradeLocalDateTime +
222+
", action='" + this.action + '\'' +
223+
", symbol='" + this.symbol + '\'' +
224+
", tradeDetail=" + this.tradeDetail +
225+
", traderId='" + this.traderId + '\'' +
226+
", executionTimes=" + this.executionTimes +
227+
", subTrades=" + this.subTrades +
228+
'}';
218229
}
219230

220231
public Date getTradeDate() {

spring-cloud-gcp-data-spanner/src/test/java/org/springframework/cloud/gcp/data/spanner/test/domain/TradeDetail.java

+20
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.cloud.gcp.data.spanner.test.domain;
1818

19+
import java.util.Objects;
20+
1921
import org.springframework.cloud.gcp.data.spanner.core.mapping.PrimaryKey;
2022

2123
/**
@@ -56,4 +58,22 @@ public void setShares(Double shares) {
5658
this.shares = shares;
5759
}
5860

61+
@Override
62+
public boolean equals(Object o) {
63+
if (this == o) {
64+
return true;
65+
}
66+
if (o == null || getClass() != o.getClass()) {
67+
return false;
68+
}
69+
TradeDetail that = (TradeDetail) o;
70+
return Objects.equals(getId(), that.getId()) &&
71+
Objects.equals(getPrice(), that.getPrice()) &&
72+
Objects.equals(getShares(), that.getShares());
73+
}
74+
75+
@Override
76+
public int hashCode() {
77+
return Objects.hash(getId(), getPrice(), getShares());
78+
}
5979
}

0 commit comments

Comments
 (0)