Skip to content

Commit

Permalink
Dates, java.util.Dates, and Timestamps now written out in String form…
Browse files Browse the repository at this point in the history
… by default.
  • Loading branch information
jdereg committed Feb 1, 2025
1 parent 16ca31a commit 9b17652
Show file tree
Hide file tree
Showing 12 changed files with 743 additions and 262 deletions.
76 changes: 54 additions & 22 deletions src/main/java/com/cedarsoftware/util/DateUtilities.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
Expand Down Expand Up @@ -152,16 +153,26 @@ public final class DateUtilities {
Pattern.CASE_INSENSITIVE);

private static final Pattern unixDateTimePattern = Pattern.compile(
"\\b(" + days + ")\\b" + ws + "\\b(" + mos + ")\\b" + ws + "(" + d1or2 + ")" + ws + "(" + d2 + ":" + d2 + ":" + d2 + ")" + wsOp + "(" + tzUnix + ")?" + wsOp + "(" + yr + ")",
"(?:\\b(" + days + ")\\b" + ws + ")?"
+ "\\b(" + mos + ")\\b" + ws
+ "(" + d1or2 + ")" + ws
+ "(" + d2 + ":" + d2 + ":" + d2 + ")" + wsOp
+ "(" + tzUnix + ")?"
+ wsOp
+ "(" + yr + ")",
Pattern.CASE_INSENSITIVE);

private static final Pattern timePattern = Pattern.compile(
"(" + d2 + "):(" + d2 + ")(?::(" + d2 + ")(" + nano + ")?)?(" + tz_Hh_MM_SS + "|" + tz_Hh_MM + "|" + tz_HHMM + "|" + tz_Hh + "|Z)?(" + tzNamed + ")?",
Pattern.CASE_INSENSITIVE);

private static final Pattern zonePattern = Pattern.compile(
"(" + tz_Hh_MM_SS + "|" + tz_Hh_MM + "|" + tz_HHMM + "|" + tz_Hh + "|Z|" + tzNamed + ")",
Pattern.CASE_INSENSITIVE);

private static final Pattern dayPattern = Pattern.compile("\\b(" + days + ")\\b", Pattern.CASE_INSENSITIVE);
private static final Map<String, Integer> months = new ConcurrentHashMap<>();
private static final Map<String, String> ABBREVIATION_TO_TIMEZONE = new ConcurrentHashMap<>();
public static final Map<String, String> ABBREVIATION_TO_TIMEZONE = new HashMap<>();

static {
// Month name to number map
Expand Down Expand Up @@ -388,6 +399,18 @@ public static ZonedDateTime parseDate(String dateStr, ZoneId defaultZoneId, bool
day = matcher.group(7);
}
remains = remnant;
// Do we have a Date with a TimeZone after it, but no time?
if (remnant.startsWith("T")) {
matcher = zonePattern.matcher(remnant.substring(1));
if (matcher.matches()) {
throw new IllegalArgumentException("Time zone information without time is invalid: " + dateStr);
}
} else {
matcher = zonePattern.matcher(remnant);
if (matcher.matches()) {
throw new IllegalArgumentException("Time zone information without time is invalid: " + dateStr);
}
}
} else {
// 2) Try alphaMonthPattern
matcher = alphaMonthPattern.matcher(dateStr);
Expand Down Expand Up @@ -523,27 +546,36 @@ private static long convertFractionToNanos(String fracSec) {
}

private static ZoneId getTimeZone(String tz) {
if (tz != null) {
if (tz.startsWith("-") || tz.startsWith("+")) {
ZoneOffset offset = ZoneOffset.of(tz);
return ZoneId.ofOffset("GMT", offset);
} else {
try {
return ZoneId.of(tz);
} catch (Exception e) {
TimeZone timeZone = TimeZone.getTimeZone(tz);
if (timeZone.getRawOffset() == 0) {
String zoneName = ABBREVIATION_TO_TIMEZONE.get(tz);
if (zoneName != null) {
return ZoneId.of(zoneName);
}
throw e;
}
return timeZone.toZoneId();
}
if (tz == null || tz.isEmpty()) {
return ZoneId.systemDefault();
}

// 1) If tz starts with +/- => offset
if (tz.startsWith("-") || tz.startsWith("+")) {
ZoneOffset offset = ZoneOffset.of(tz);
return ZoneId.ofOffset("GMT", offset);
}

// 2) Check custom abbreviation map first
String mappedZone = ABBREVIATION_TO_TIMEZONE.get(tz.toUpperCase());
if (mappedZone != null) {
// e.g. "EST" => "America/New_York"
return ZoneId.of(mappedZone);
}

// 3) Try ZoneId.of(tz) for full region IDs like "Europe/Paris"
try {
return ZoneId.of(tz);
} catch (Exception zoneIdEx) {
// 4) Fallback to TimeZone for weird short IDs or older JDK
TimeZone timeZone = TimeZone.getTimeZone(tz);
if (timeZone.getID().equals("GMT") && !tz.toUpperCase().equals("GMT")) {
// Means the JDK didn't recognize 'tz' (it fell back to "GMT")
throw zoneIdEx; // rethrow original
}
// Otherwise, we accept whatever the JDK returned
return timeZone.toZoneId();
}
return ZoneId.systemDefault();
}

private static void verifyNoGarbageLeft(String remnant) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicLong;

/**
Expand Down Expand Up @@ -111,7 +112,22 @@ static Calendar create(long epochMilli, Converter converter) {

static String toString(Object from, Converter converter) {
Calendar cal = (Calendar) from;
return DateConversions.toString(cal.getTime(), converter);
ZonedDateTime zdt = cal.toInstant().atZone(cal.getTimeZone().toZoneId());
TimeZone tz = cal.getTimeZone();

String pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS";

// Only use named zones for IANA timezone IDs
String id = tz.getID();
if (id.contains("/")) { // IANA timezones contain "/"
pattern += "['['VV']']";
} else if ("GMT".equals(id) || "UTC".equals(id)) {
pattern += "X"; // Z for UTC/GMT
} else {
pattern += "xxx"; // Offsets for everything else (EST, GMT+02:00, etc)
}

return zdt.format(DateTimeFormatter.ofPattern(pattern));
}

static OffsetDateTime toOffsetDateTime(Object from, Converter converter) {
Expand All @@ -121,23 +137,8 @@ static OffsetDateTime toOffsetDateTime(Object from, Converter converter) {
}

static Map<String, Object> toMap(Object from, Converter converter) {
Calendar cal = (Calendar) from;
ZonedDateTime zdt = cal.toInstant().atZone(cal.getTimeZone().toZoneId());

// Format with timezone keeping DST information
String formatted;
int ms = zdt.getNano() / 1_000_000; // Convert nanos to millis
if (ms == 0) {
// No fractional seconds
formatted = zdt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'['VV']'"));
} else {
// Millisecond precision
formatted = zdt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"))
+ String.format(".%03d[%s]", ms, zdt.getZone());
}

Map<String, Object> target = new LinkedHashMap<>();
target.put(MapConversions.CALENDAR, formatted);
target.put(MapConversions.CALENDAR, toString(from, converter));
return target;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@ private static void buildFactoryConversions() {
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::toString);
CONVERSION_DB.put(pair(java.sql.Date.class, String.class), DateConversions::sqlDateToString);
CONVERSION_DB.put(pair(java.sql.Date.class, String.class), DateConversions::toSqlDateString);
CONVERSION_DB.put(pair(Timestamp.class, String.class), TimestampConversions::toString);
CONVERSION_DB.put(pair(LocalDate.class, String.class), LocalDateConversions::toString);
CONVERSION_DB.put(pair(LocalTime.class, String.class), LocalTimeConversions::toString);
Expand Down
52 changes: 13 additions & 39 deletions src/main/java/com/cedarsoftware/util/convert/DateConversions.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
Expand Down Expand Up @@ -37,6 +36,9 @@
* limitations under the License.
*/
final class DateConversions {
static final DateTimeFormatter MILLIS_FMT = new DateTimeFormatterBuilder()
.appendInstant(3) // Force exactly 3 decimal places
.toFormatter();

private DateConversions() {}

Expand Down Expand Up @@ -118,55 +120,27 @@ static AtomicLong toAtomicLong(Object from, Converter converter) {
return new AtomicLong(toLong(from, converter));
}

static String sqlDateToString(Object from, Converter converter) {
java.sql.Date sqlDate = (java.sql.Date) from;
return toString(new Date(sqlDate.getTime()), converter);
}

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();

return zonedDateTime.format(formatter);
Instant instant = date.toInstant(); // Convert legacy Date to Instant
return MILLIS_FMT.format(instant);
}

static String toSqlDateString(Object from, Converter converter) {
java.sql.Date sqlDate = (java.sql.Date) from;
// java.sql.Date.toString() returns the date in "yyyy-MM-dd" format.
return sqlDate.toString();
}

static Map<String, Object> toMap(Object from, Converter converter) {
Date date = (Date) from;
String formatted;
Map<String, Object> map = new LinkedHashMap<>();

if (date instanceof java.sql.Date) {
// Convert millis to Instant then LocalDate in UTC
LocalDate localDate = Instant.ofEpochMilli(date.getTime())
.atZone(ZoneOffset.UTC)
.toLocalDate();

// Place that LocalDate at midnight in UTC, then format
ZonedDateTime zdt = localDate.atStartOfDay(ZoneOffset.UTC);
formatted = zdt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));

map.put(MapConversions.SQL_DATE, formatted);
map.put(MapConversions.SQL_DATE, toSqlDateString(date, converter));
} else {
// Regular util.Date - format with time
ZonedDateTime zdt = date.toInstant().atZone(ZoneOffset.UTC);
int ms = zdt.getNano() / 1_000_000; // Convert nanos to millis
if (ms == 0) {
// No fractional seconds
formatted = zdt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));
} else {
// Millisecond precision
formatted = zdt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"))
+ String.format(".%03dZ", ms);
}
map.put(MapConversions.DATE, formatted);
map.put(MapConversions.DATE, toString(from, converter));
}

return map;
Expand Down
Loading

0 comments on commit 9b17652

Please sign in to comment.