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