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

Reply via email to