This is an automated email from the ASF dual-hosted git repository.
pvillard pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new bc68763b06 NIFI-14620 Corrected toDate EL Function handling of time
zone
bc68763b06 is described below
commit bc68763b063bbf683510abe15750b314618e6482
Author: exceptionfactory <[email protected]>
AuthorDate: Wed Jun 11 15:24:09 2025 -0500
NIFI-14620 Corrected toDate EL Function handling of time zone
- Refactored DateTimeFormatter parsed result handling to incorporate Time
Zone when provided
Signed-off-by: Pierre Villard <[email protected]>
This closes #10011.
---
.../attribute/expression/language/TestQuery.java | 25 +++++++
.../java/org/apache/nifi/util/FormatUtils.java | 87 ++++++++++++++++------
2 files changed, 89 insertions(+), 23 deletions(-)
diff --git
a/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java
b/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java
index b840feb36e..2e26b8acc7 100644
---
a/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java
+++
b/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java
@@ -1011,6 +1011,29 @@ public class TestQuery {
verifyEquals("${date:toDate():toNumber():toDate():toNumber():toDate():toNumber():format('yyyy')}",
attributes, "2014");
}
+ @Test
+ public void testToDateAdjustedWithTimeZone() {
+ final String dateFormat = "yyyy-MM-dd";
+ final String created = "2025-06-01";
+ final String timeZoneId = "UTC";
+ final TimeZone timeZone = TimeZone.getTimeZone(timeZoneId);
+
+ // Build Calendar for java.util.Date to match expected result of
toDate() function
+ final Calendar calendar = Calendar.getInstance();
+ calendar.clear();
+ calendar.set(Calendar.YEAR, 2025);
+ calendar.set(Calendar.MONTH, Calendar.JUNE);
+ calendar.set(Calendar.DAY_OF_MONTH, 1);
+ calendar.setTimeZone(timeZone);
+
+ // Expected Date with System Default Time Zone and hours adjusted
based on Time Zone specified
+ final Date expected = calendar.getTime();
+
+ final String expression = "${created:toDate('%s',
'%s')}".formatted(dateFormat, timeZoneId);
+ final Map<String, String> attributes = Map.of("created", created);
+ verifyEquals(expression, attributes, expected);
+ }
+
@Test
public void testInstantConversion() {
final Map<String, String> attributes = new HashMap<>();
@@ -2591,6 +2614,8 @@ public class TestQuery {
}
} else if (expectedResult instanceof Boolean) {
assertEquals(ResultType.BOOLEAN, result.getResultType());
+ } else if (expectedResult instanceof Date) {
+ assertEquals(ResultType.DATE, result.getResultType());
} else {
assertEquals(ResultType.STRING, result.getResultType());
}
diff --git
a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/FormatUtils.java
b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/FormatUtils.java
index 7808558c16..7210932692 100644
---
a/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/FormatUtils.java
+++
b/nifi-commons/nifi-utils/src/main/java/org/apache/nifi/util/FormatUtils.java
@@ -21,15 +21,15 @@ import org.apache.nifi.time.DurationFormat;
import java.text.NumberFormat;
import java.time.Instant;
-import java.time.LocalDate;
import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.Year;
-import java.time.YearMonth;
+import java.time.OffsetDateTime;
import java.time.ZoneId;
+import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
+import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
+import java.time.temporal.TemporalQueries;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@@ -39,9 +39,6 @@ public class FormatUtils {
// 'public static final' members defined for backward compatibility, since
they were moved to TimeFormat.
public static final String TIME_DURATION_REGEX =
DurationFormat.TIME_DURATION_REGEX;
public static final Pattern TIME_DURATION_PATTERN =
DurationFormat.TIME_DURATION_PATTERN;
-
- private static final LocalDate EPOCH_INITIAL_DATE = LocalDate.of(1970, 1,
1);
-
/**
* Formats the specified count by adding commas.
*
@@ -234,29 +231,73 @@ public class FormatUtils {
}
/**
- * Parse text to Instant - support different formats like: zoned date
time, date time, date, time
- * @param formatter configured formatter
+ * Parse text and build Instant based on chronological fields found
+ *
+ * @param formatter DateTimeFormatter responsible for parsing
* @param text text which will be parsed
* @return parsed Instant
*/
- public static Instant parseToInstant(DateTimeFormatter formatter, String
text) {
+ public static Instant parseToInstant(final DateTimeFormatter formatter,
final String text) {
if (text == null) {
throw new IllegalArgumentException("Text cannot be null");
}
- TemporalAccessor parsed = formatter.parseBest(text, Instant::from,
LocalDateTime::from, LocalDate::from, YearMonth::from, Year::from,
LocalTime::from);
- return switch (parsed) {
- case Instant instant -> instant;
- case LocalDateTime localDateTime ->
toInstantInSystemDefaultTimeZone(localDateTime);
- case LocalDate localDate ->
toInstantInSystemDefaultTimeZone(localDate.atTime(0, 0));
- case YearMonth yearMonth ->
toInstantInSystemDefaultTimeZone(yearMonth.atDay(1).atTime(0, 0));
- case Year year ->
toInstantInSystemDefaultTimeZone(year.atDay(1).atTime(0, 0));
- case null, default ->
toInstantInSystemDefaultTimeZone(((LocalTime)
parsed).atDate(EPOCH_INITIAL_DATE));
- };
- }
+ final TemporalAccessor parsed = formatter.parse(text);
- private static Instant toInstantInSystemDefaultTimeZone(LocalDateTime
dateTime) {
- return dateTime.atZone(ZoneId.systemDefault()).toInstant();
- }
+ // Default to 1970 as start of epoch
+ int year = 1970;
+ if (parsed.isSupported(ChronoField.YEAR)) {
+ year = parsed.get(ChronoField.YEAR);
+ }
+
+ int month = 1;
+ if (parsed.isSupported(ChronoField.MONTH_OF_YEAR)) {
+ month = parsed.get(ChronoField.MONTH_OF_YEAR);
+ }
+
+ int day = 1;
+ if (parsed.isSupported(ChronoField.DAY_OF_MONTH)) {
+ day = parsed.get(ChronoField.DAY_OF_MONTH);
+ }
+
+ int hour = 0;
+ if (parsed.isSupported(ChronoField.HOUR_OF_DAY)) {
+ hour = parsed.get(ChronoField.HOUR_OF_DAY);
+ }
+ int minute = 0;
+ if (parsed.isSupported(ChronoField.MINUTE_OF_HOUR)) {
+ minute = parsed.get(ChronoField.MINUTE_OF_HOUR);
+ }
+
+ int second = 0;
+ if (parsed.isSupported(ChronoField.SECOND_OF_MINUTE)) {
+ second = parsed.get(ChronoField.SECOND_OF_MINUTE);
+ }
+
+ int nano = 0;
+ if (parsed.isSupported(ChronoField.MILLI_OF_SECOND)) {
+ // Get nanoseconds for maximum resolution
+ nano = parsed.get(ChronoField.NANO_OF_SECOND);
+ }
+
+ final LocalDateTime localDateTime = LocalDateTime.of(year, month, day,
hour, minute, second, nano);
+
+ ZoneId zoneId = parsed.query(TemporalQueries.zoneId());
+ if (zoneId == null) {
+ zoneId = ZoneId.systemDefault();
+ }
+
+ final ZoneOffset zoneOffset;
+ if (parsed.isSupported(ChronoField.OFFSET_SECONDS)) {
+ final int offsetSeconds = parsed.get(ChronoField.OFFSET_SECONDS);
+ zoneOffset = ZoneOffset.ofTotalSeconds(offsetSeconds);
+ } else {
+ // Get Zone Offset from provided Zone ID and parsed Local Date Time
+ zoneOffset = zoneId.getRules().getOffset(localDateTime);
+ }
+
+ final OffsetDateTime offsetDateTime = OffsetDateTime.of(localDateTime,
zoneOffset);
+ return offsetDateTime.toInstant();
+ }
}