Skip to content

Commit

Permalink
Calendar, Date, and Timestamp use the same conversion to String, whic…
Browse files Browse the repository at this point in the history
…h outputs the date-time in local-time zone, which they are used to, but includes milliseconds and the offset from GMT, or Z if GMT. This way, the user can always tell what this time is in terms of UTC/GMT.
  • Loading branch information
jdereg committed Mar 3, 2024
1 parent 044dfe6 commit 54b72be
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 47 deletions.
2 changes: 1 addition & 1 deletion src/main/java/com/cedarsoftware/util/Convention.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static void throwIfNull(Object value, String message) {
* @throws IllegalArgumentException if the string passed in is null or empty
*/
public static void throwIfNullOrEmpty(String value, String message) {
if (value == null || value.isEmpty()) {
if (StringUtilities.isEmpty(value)) {
throw new IllegalArgumentException(message);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
Expand Down Expand Up @@ -109,9 +108,8 @@ static Calendar create(long epochMilli, Converter converter) {
}

static String toString(Object from, Converter converter) {
ZonedDateTime zdt = toZonedDateTime(from, converter);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS]XXX", converter.getOptions().getLocale());
return formatter.format(zdt);
Calendar cal = (Calendar) from;
return DateConversions.toString(cal.getTime(), converter);
}

static Map<String, Object> toMap(Object from, Converter converter) {
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/cedarsoftware/util/convert/Converter.java
Original file line number Diff line number Diff line change
Expand Up @@ -680,9 +680,9 @@ private static void buildFactoryConversions() {
CONVERSION_DB.put(pair(ByteBuffer.class, String.class), ByteBufferConversions::toString);
CONVERSION_DB.put(pair(CharBuffer.class, String.class), CharBufferConversions::toString);
CONVERSION_DB.put(pair(Class.class, String.class), ClassConversions::toString);
CONVERSION_DB.put(pair(Date.class, String.class), DateConversions::dateToString);
CONVERSION_DB.put(pair(Date.class, String.class), DateConversions::toString);
CONVERSION_DB.put(pair(java.sql.Date.class, String.class), DateConversions::sqlDateToString);
CONVERSION_DB.put(pair(Timestamp.class, String.class), DateConversions::timestampToString);
CONVERSION_DB.put(pair(Timestamp.class, String.class), DateConversions::toString);
CONVERSION_DB.put(pair(LocalDate.class, String.class), LocalDateConversions::toString);
CONVERSION_DB.put(pair(LocalTime.class, String.class), LocalTimeConversions::toString);
CONVERSION_DB.put(pair(LocalDateTime.class, String.class), LocalDateTimeConversions::toString);
Expand Down
40 changes: 26 additions & 14 deletions src/main/java/com/cedarsoftware/util/convert/DateConversions.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import java.math.BigInteger;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.atomic.AtomicLong;
Expand Down Expand Up @@ -102,21 +103,32 @@ static AtomicLong toAtomicLong(Object from, Converter converter) {
return new AtomicLong(toLong(from, converter));
}

static String dateToString(Object from, Converter converter) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
simpleDateFormat.setTimeZone(converter.getOptions().getTimeZone());
return simpleDateFormat.format(((Date) from));
}

static String sqlDateToString(Object from, Converter converter) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
simpleDateFormat.setTimeZone(converter.getOptions().getTimeZone());
return simpleDateFormat.format(((Date) from));
java.sql.Date sqlDate = (java.sql.Date) from;
return toString(new Date(sqlDate.getTime()), converter);
}

static String timestampToString(Object from, Converter converter) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
simpleDateFormat.setTimeZone(converter.getOptions().getTimeZone());
return simpleDateFormat.format(((Date) from));
static String toString(Object from, Converter converter) {
Date date = (Date) from;

// Convert Date to ZonedDateTime
ZonedDateTime zonedDateTime = date.toInstant().atZone(converter.getOptions().getZoneId());

// Build a formatter with optional milliseconds and always show timezone offset
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")
.appendOffset("+HH:MM", "Z") // Timezone offset
.toFormatter();

// Build a formatter with optional milliseconds and always show the timezone name
// DateTimeFormatter formatter = new DateTimeFormatterBuilder()
// .appendPattern("yyyy-MM-dd'T'HH:mm:ss")
// .appendFraction(ChronoField.MILLI_OF_SECOND, 0, 3, true) // Optional milliseconds
// .appendLiteral('[') // Space separator
// .appendZoneId()
// .appendLiteral(']')
// .toFormatter();

return zonedDateTime.format(formatter);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,9 @@ static Period toPeriod(Object from, Converter converter) {
}

static Date toDate(Object from, Converter converter) {
Instant instant = toInstant(from, converter);
return instant == null ? null : Date.from(instant);
String strDate = (String) from;
ZonedDateTime zdt = DateUtilities.parseDate(strDate, converter.getOptions().getZoneId(), true);
return Date.from(zdt.toInstant());
}

static java.sql.Date toSqlDate(Object from, Converter converter) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,19 +381,19 @@ private static void loadStringTests() {
{Date.class, "java.util.Date", true}
});
TEST_DB.put(pair(Date.class, String.class), new Object[][]{
{new Date(1), toGmtString(new Date(1))},
{new Date(Integer.MAX_VALUE), toGmtString(new Date(Integer.MAX_VALUE))},
{new Date(Long.MAX_VALUE), toGmtString(new Date(Long.MAX_VALUE))}
{new Date(-1), "1970-01-01T08:59:59.999+09:00", true}, // Tokyo (set in options - defaults to system when not set explicitly)
{new Date(0), "1970-01-01T09:00:00.000+09:00", true},
{new Date(1), "1970-01-01T09:00:00.001+09:00", true},
});
TEST_DB.put(pair(java.sql.Date.class, String.class), new Object[][]{
{new java.sql.Date(1), toGmtString(new java.sql.Date(1))},
{new java.sql.Date(Integer.MAX_VALUE), toGmtString(new java.sql.Date(Integer.MAX_VALUE))},
{new java.sql.Date(Long.MAX_VALUE), toGmtString(new java.sql.Date(Long.MAX_VALUE))}
{new java.sql.Date(-1), "1970-01-01T08:59:59.999+09:00", true}, // Tokyo (set in options - defaults to system when not set explicitly)
{new java.sql.Date(0), "1970-01-01T09:00:00.000+09:00", true},
{new java.sql.Date(1), "1970-01-01T09:00:00.001+09:00", true},
});
TEST_DB.put(pair(Timestamp.class, String.class), new Object[][]{
{new Timestamp(1), toGmtString(new Timestamp(1))},
{new Timestamp(Integer.MAX_VALUE), toGmtString(new Timestamp(Integer.MAX_VALUE))},
{new Timestamp(Long.MAX_VALUE), toGmtString(new Timestamp(Long.MAX_VALUE))},
{new Timestamp(-1), "1970-01-01T08:59:59.999+09:00", true}, // Tokyo (set in options - defaults to system when not set explicitly)
{new Timestamp(0), "1970-01-01T09:00:00.000+09:00", true},
{new Timestamp(1), "1970-01-01T09:00:00.001+09:00", true},
});
TEST_DB.put(pair(LocalDate.class, String.class), new Object[][]{
{LocalDate.parse("1965-12-31"), "1965-12-31"},
Expand All @@ -418,10 +418,19 @@ private static void loadStringTests() {
TEST_DB.put(pair(Calendar.class, String.class), new Object[][]{
{(Supplier<Calendar>) () -> {
Calendar cal = Calendar.getInstance(TOKYO_TZ);
cal.set(2024, Calendar.FEBRUARY, 5, 22, 31, 17);
cal.set(Calendar.MILLISECOND, 409);
cal.setTimeInMillis(-1);
return cal;
}, "1970-01-01T08:59:59.999+09:00", true},
{(Supplier<Calendar>) () -> {
Calendar cal = Calendar.getInstance(TOKYO_TZ);
cal.setTimeInMillis(0);
return cal;
}, "1970-01-01T09:00:00.000+09:00", true},
{(Supplier<Calendar>) () -> {
Calendar cal = Calendar.getInstance(TOKYO_TZ);
cal.setTimeInMillis(1);
return cal;
}, "2024-02-05T22:31:17.409+09:00"}
}, "1970-01-01T09:00:00.001+09:00", true},
});
TEST_DB.put(pair(Number.class, String.class), new Object[][]{
{(byte) 1, "1"},
Expand Down
23 changes: 14 additions & 9 deletions src/test/java/com/cedarsoftware/util/convert/ConverterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1794,18 +1794,20 @@ void testString_fromDate()
Date date = cal.getTime();

String converted = this.converter.convert(date, String.class);
assertThat(converted).isEqualTo("2015-01-17T08:34:49");
assertThat(converted).startsWith("2015-01-17T08:34:49");
}

@Test
void testString_fromCalendar()
{
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
cal.clear();
cal.set(2015, 0, 17, 8, 34, 49);
// TODO: Gets fixed when Date.class ==> String.class is tested/added
// assertEquals("2015-01-17T08:34:49", this.converter.convert(cal.getTime(), String.class));
assertEquals("2015-01-17T08:34:49.000Z", this.converter.convert(cal, String.class));
cal.setTimeInMillis(1421483689000L);

Converter converter1 = new Converter(new ConverterOptions() {
public ZoneId getZoneId() { return ZoneId.of("GMT"); }
});
assertEquals("2015-01-17T08:34:49.000Z", converter1.convert(cal.getTime(), String.class));
assertEquals("2015-01-17T08:34:49.000Z", converter1.convert(cal, String.class));
}

@Test
Expand Down Expand Up @@ -1943,7 +1945,6 @@ void testAtomicInteger(Object value, int expectedResult)
@Test
void testAtomicInteger_withEmptyString() {
AtomicInteger converted = this.converter.convert("", AtomicInteger.class);
//TODO: Do we want nullable types to default to zero
assertThat(converted.get()).isEqualTo(0);
}

Expand Down Expand Up @@ -2503,7 +2504,9 @@ private static Stream<Arguments> unparseableDates() {
@MethodSource("unparseableDates")
void testUnparseableDates_Date(String date)
{
assertNull(this.converter.convert(date, Date.class));
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> this.converter.convert(date, Date.class))
.withMessageContaining("'dateStr' must not be null or empty String");
}

@ParameterizedTest
Expand Down Expand Up @@ -2923,7 +2926,9 @@ void testMapToDate() {

map.clear();
map.put("value", "");
assert null == this.converter.convert(map, Date.class);
assertThatThrownBy(() -> converter.convert(map, Date.class))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("'dateStr' must not be null or empty String");

map.clear();
map.put("value", null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.stream.Stream;

import com.cedarsoftware.util.ClassUtilities;
Expand Down Expand Up @@ -248,9 +247,7 @@ private static Stream<Arguments> classesThatReturnNull_whenTrimmedToEmpty() {
Arguments.of(Year.class),
Arguments.of(Timestamp.class),
Arguments.of(java.sql.Date.class),
Arguments.of(Date.class),
Arguments.of(Instant.class),
Arguments.of(Date.class),
Arguments.of(java.sql.Date.class),
Arguments.of(Timestamp.class),
Arguments.of(ZonedDateTime.class),
Expand Down

0 comments on commit 54b72be

Please sign in to comment.