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 0018851cb3 NIFI-15305 Fixed Date Time conversion for floating point as 
string
0018851cb3 is described below

commit 0018851cb3cb4ff5f8a9049b93a0dc1653930df1
Author: exceptionfactory <[email protected]>
AuthorDate: Tue Dec 23 22:25:43 2025 -0600

    NIFI-15305 Fixed Date Time conversion for floating point as string
    
    - Added check for integral numbers greater than expected number of seconds 
for year 10,000 and handled as milliseconds
    
    Signed-off-by: Pierre Villard <[email protected]>
    
    This closes #10697.
---
 .../field/ObjectLocalDateTimeFieldConverter.java   | 82 ++++++++++++++++------
 .../TestObjectLocalDateTimeFieldConverter.java     | 21 +++++-
 2 files changed, 78 insertions(+), 25 deletions(-)

diff --git 
a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectLocalDateTimeFieldConverter.java
 
b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectLocalDateTimeFieldConverter.java
index b5573cc9e7..3c84ad8d3a 100644
--- 
a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectLocalDateTimeFieldConverter.java
+++ 
b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/field/ObjectLocalDateTimeFieldConverter.java
@@ -25,6 +25,7 @@ import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoUnit;
 import java.time.temporal.TemporalAccessor;
 import java.time.temporal.TemporalQueries;
 import java.time.temporal.TemporalQuery;
@@ -36,6 +37,11 @@ import java.util.Optional;
  */
 class ObjectLocalDateTimeFieldConverter implements FieldConverter<Object, 
LocalDateTime> {
     private static final long YEAR_TEN_THOUSAND = 253_402_300_800_000L;
+    private static final long YEAR_TEN_THOUSAND_SECONDS = 253_402_300_800L;
+    private static final char PERIOD = '.';
+
+    private static final long MILLISECONDS_TO_MICROSECONDS = 1_000;
+    private static final long SECONDS_TO_MICROSECONDS = 1_000_000;
 
     private static final TemporalQuery<LocalDateTime> 
LOCAL_DATE_TIME_TEMPORAL_QUERY = new LocalDateTimeQuery();
 
@@ -66,9 +72,10 @@ class ObjectLocalDateTimeFieldConverter implements 
FieldConverter<Object, LocalD
                 return ofInstant(instant);
             }
             case final Number number -> {
-                // If value is a floating point number, we consider it as 
seconds since epoch plus a decimal part for fractions of a second.
+                // Handle floating point numbers with integral and fractional 
components
                 if (field instanceof Double || field instanceof Float) {
-                    return toLocalDateTime(number.doubleValue());
+                    final double floatingPointNumber = number.doubleValue();
+                    return convertDouble(floatingPointNumber);
                 }
 
                 return toLocalDateTime(number.longValue());
@@ -99,40 +106,71 @@ class ObjectLocalDateTimeFieldConverter implements 
FieldConverter<Object, LocalD
 
     private LocalDateTime tryParseAsNumber(final String value, final String 
fieldName) {
         try {
-            // If decimal, treat as a double and convert to seconds and 
nanoseconds.
-            if (value.contains(".")) {
-                final double number = Double.parseDouble(value);
-                return toLocalDateTime(number);
+            final LocalDateTime localDateTime;
+
+            final int periodIndex = value.indexOf(PERIOD);
+            if (periodIndex >= 0) {
+                // Parse Double to support both decimal notation and exponent 
notation
+                final double floatingPointNumber = Double.parseDouble(value);
+                localDateTime = convertDouble(floatingPointNumber);
+            } else {
+                final long number = Long.parseLong(value);
+                localDateTime = toLocalDateTime(number);
             }
 
-            // attempt to parse as a long value
-            final long number = Long.parseLong(value);
-            return toLocalDateTime(number);
+            return localDateTime;
         } catch (final NumberFormatException e) {
             throw new FieldConversionException(LocalDateTime.class, value, 
fieldName, e);
         }
     }
 
-    private LocalDateTime toLocalDateTime(final double secondsSinceEpoch) {
-        // Determine the number of micros past the second by subtracting the 
number of seconds from the decimal value and multiplying by 1 million.
-        final double micros = 1_000_000 * (secondsSinceEpoch - (long) 
secondsSinceEpoch);
-        // Convert micros to nanos. Note that we perform this as a separate 
operation, rather than multiplying by 1_000,000,000 in order to avoid
-        // issues that occur with rounding at high precision.
-        final long nanos = (long) micros * 1000L;
+    /**
+     * Convert double to LocalDateTime after evaluating integral and 
fractional components.
+     * Handles integral numbers greater than the year 10,000 in seconds as 
milliseconds, otherwise as seconds.
+     * Multiplies fractional number to microseconds based on size of integral 
number.
+     *
+     * @param number Number of milliseconds or seconds
+     * @return Local Date Time
+     */
+    private LocalDateTime convertDouble(final double number) {
+        // Cast to long for integral part of the number
+        final long integral = (long) number;
+
+        // Calculate fractional part of the number for subsequent precision 
evaluation
+        final double fractional = number - integral;
+        final Instant epoch;
+        final long fractionalMultiplier;
+
+        if (integral > YEAR_TEN_THOUSAND_SECONDS) {
+            // Handle large numbers as milliseconds instead of seconds
+            epoch = Instant.ofEpochMilli(integral);
+
+            // Convert fractional part from milliseconds to microseconds
+            fractionalMultiplier = MILLISECONDS_TO_MICROSECONDS;
+        } else {
+            // Handle smaller numbers as seconds
+            epoch = Instant.ofEpochSecond(integral);
+
+            // Convert fractional part from seconds to microseconds
+            fractionalMultiplier = SECONDS_TO_MICROSECONDS;
+        }
 
-        return toLocalDateTime((long) secondsSinceEpoch, nanos);
-    }
+        // Calculate microseconds according to multiplier for expected 
precision
+        final double fractionalMicroseconds = fractional * 
fractionalMultiplier;
+        final long microseconds = Math.round(fractionalMicroseconds);
+        final Instant instant = epoch.plus(microseconds, ChronoUnit.MICROS);
 
-    private LocalDateTime toLocalDateTime(final long epochSeconds, final long 
nanosPastSecond) {
-        final Instant instant = 
Instant.ofEpochSecond(epochSeconds).plusNanos(nanosPastSecond);
         return ofInstant(instant);
     }
 
     private LocalDateTime toLocalDateTime(final long value) {
         if (value > YEAR_TEN_THOUSAND) {
-            // Value is too large. Assume microseconds instead of milliseconds.
-            final Instant microsInstant = Instant.ofEpochSecond(value / 
1_000_000, (value % 1_000_000) * 1_000);
-            return ofInstant(microsInstant);
+            // Handle number as microseconds for large values
+            final long epochSecond = value / SECONDS_TO_MICROSECONDS;
+            // Calculate microseconds from remainder
+            final long microseconds = value % SECONDS_TO_MICROSECONDS;
+            final Instant instant = 
Instant.ofEpochSecond(epochSecond).plus(microseconds, ChronoUnit.MICROS);
+            return ofInstant(instant);
         }
 
         final Instant instant = Instant.ofEpochMilli(value);
diff --git 
a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/TestObjectLocalDateTimeFieldConverter.java
 
b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/TestObjectLocalDateTimeFieldConverter.java
index d44826d45b..1aeabfc527 100644
--- 
a/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/TestObjectLocalDateTimeFieldConverter.java
+++ 
b/nifi-commons/nifi-record/src/test/java/org/apache/nifi/serialization/record/field/TestObjectLocalDateTimeFieldConverter.java
@@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
+import java.time.temporal.ChronoUnit;
 import java.util.Optional;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -40,8 +41,15 @@ public class TestObjectLocalDateTimeFieldConverter {
     private static final LocalDateTime LOCAL_DATE_TIME_MILLIS_PRECISION = 
LocalDateTime.ofInstant(INSTANT_MILLIS_PRECISION, ZoneId.systemDefault());
     private static final LocalDateTime LOCAL_DATE_TIME_MICROS_PRECISION = 
LocalDateTime.ofInstant(INSTANT_MICROS_PRECISION, ZoneId.systemDefault());
 
-    private final ObjectLocalDateTimeFieldConverter converter = new 
ObjectLocalDateTimeFieldConverter();
+    private static final long INPUT_MILLISECONDS = 1765056655230L;
+    private static final long INPUT_MICROSECONDS = 746;
+    private static final int EXPECTED_NANOSECONDS = 230746000;
+    private static final String MILLIS_WITH_FRACTIONAL_MICROS = 
"%d.%d".formatted(INPUT_MILLISECONDS, INPUT_MICROSECONDS);
+
+    private static final Instant INSTANT_MICROSECOND_PRECISION = 
Instant.ofEpochMilli(INPUT_MILLISECONDS).plus(INPUT_MICROSECONDS, 
ChronoUnit.MICROS);
+    private static final LocalDateTime LOCAL_DATE_TIME_FRACTIONAL_MICROS = 
LocalDateTime.ofInstant(INSTANT_MICROSECOND_PRECISION, ZoneId.systemDefault());
 
+    private final ObjectLocalDateTimeFieldConverter converter = new 
ObjectLocalDateTimeFieldConverter();
 
     @Test
     public void testConvertTimestampMillis() {
@@ -69,8 +77,15 @@ public class TestObjectLocalDateTimeFieldConverter {
     public void testDoubleAsEpochSecondsAsString() {
         final LocalDateTime result = 
converter.convertField(MICROS_TIMESTAMP_STRING, Optional.empty(), FIELD_NAME);
         assertEquals(LOCAL_DATE_TIME_MICROS_PRECISION, result);
-        final double expectedNanos = 351567000L;
-        assertEquals(expectedNanos, result.getNano(), 1D);
+        assertEquals(NANOS_AFTER_SECOND, result.getNano(), 1D);
+    }
+
+    @Test
+    public void testDoubleAsEpochMillisecondsWithMicroseconds() {
+        final LocalDateTime result = 
converter.convertField(MILLIS_WITH_FRACTIONAL_MICROS, Optional.empty(), 
FIELD_NAME);
+        assertEquals(LOCAL_DATE_TIME_FRACTIONAL_MICROS, result);
+        final int nano = result.getNano();
+        assertEquals(EXPECTED_NANOSECONDS, nano);
     }
 
     @Test

Reply via email to