gnodet-bot commented on code in PR #27585:
URL: https://github.com/apache/camel/pull/27585#discussion_r4224346149
##########
components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/CML.java:
##########
@@ -264,17 +291,188 @@ private Val formatDate(Val value, Val format) {
if (isNull(value)) {
return Val.Null$.MODULE$;
}
- if (!(value instanceof Val.Str valStr)) {
- throw new IllegalArgumentException("Expected String date value,
got: " + value.prettyName());
- }
if (!(format instanceof Val.Str fmtStr)) {
throw new IllegalArgumentException("Expected String format, got: "
+ format.prettyName());
}
- ZonedDateTime dateTime = parseToZonedDateTime(valStr.value());
+ ZonedDateTime dateTime;
+ if (value instanceof Val.Num num) {
+ // epoch milliseconds, as returned by parseDate
+ dateTime = Instant.ofEpochMilli((long)
num.value()).atZone(ZoneId.of("UTC"));
+ } else if (value instanceof Val.Str valStr) {
+ dateTime = parseToZonedDateTime(valStr.value());
+ } else {
+ throw new IllegalArgumentException("Expected String or Number date
value, got: " + value.prettyName());
+ }
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern(fmtStr.value());
return new Val.Str(dateTime.format(formatter));
}
+ // A number with a java.text.DecimalFormat pattern, or an ISO-8601 date or
time with a DateTimeFormatter pattern
+ private Val format(Val value, Val format) {
+ if (isNull(value)) {
+ return Val.Null$.MODULE$;
+ }
+ String pattern = string(format, "format");
+ if (value instanceof Val.Num num) {
+ DecimalFormat decimalFormat = new DecimalFormat(pattern,
DecimalFormatSymbols.getInstance(Locale.ROOT));
+ return new
Val.Str(decimalFormat.format(BigDecimal.valueOf(num.value())));
+ }
+ if (value instanceof Val.Str str) {
+ return new Val.Str(DateTimeFormatter.ofPattern(pattern,
Locale.ROOT).format(parseTemporal(str.value())));
+ }
+ throw new IllegalArgumentException("Cannot format " +
value.prettyName());
+ }
+
+ // A date or time (as an ISO-8601 string) of the given type (Date,
DateTime, LocalDateTime, Time or LocalTime),
+ // from a string in the given format (or ISO-8601 when the format is null)
or a number of seconds since the epoch
+ private Val parseDateTime(Val value, Val format, Val type) {
+ if (isNull(value)) {
+ return Val.Null$.MODULE$;
+ }
+ String kind = string(type, "type");
+ TemporalAccessor parsed;
+ if (value instanceof Val.Num num) {
+ parsed = Instant.ofEpochSecond((long)
num.value()).atZone(ZoneOffset.UTC);
+ } else if (value instanceof Val.Str str && isNull(format)) {
+ parsed = parseTemporal(str.value());
+ } else if (value instanceof Val.Str str) {
+ parsed = DateTimeFormatter.ofPattern(string(format, "format"),
Locale.ROOT).parse(str.value());
+ } else {
+ throw new IllegalArgumentException("Cannot convert " +
value.prettyName() + " to " + kind);
+ }
+ return new Val.Str(switch (kind) {
+ case "Date" -> LocalDate.from(parsed).toString();
+ case "DateTime" ->
DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(toZonedDateTime(parsed));
+ case "LocalDateTime" ->
DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(toLocalDateTime(parsed));
+ case "Time" ->
DateTimeFormatter.ISO_OFFSET_TIME.format(toOffsetTime(parsed));
+ case "LocalTime" ->
DateTimeFormatter.ISO_LOCAL_TIME.format(LocalTime.from(parsed));
+ default -> throw new IllegalArgumentException("Unknown date type:
" + kind);
+ });
+ }
+
+ // A date or time (as an ISO-8601 string) plus an ISO-8601 period or
duration such as P1D, PT1H or -P1M
+ private Val dateAdd(Val value, Val amount) {
+ if (isNull(value)) {
+ return Val.Null$.MODULE$;
+ }
+ String text = string(value, "value");
+ TemporalAccessor temporal = parseTemporal(text);
+ String period = string(amount, "amount");
+ boolean negative = period.startsWith("-");
+ String unsigned = negative ? period.substring(1) : period;
+ TemporalAmount temporalAmount = unsigned.contains("T") ?
Duration.parse(unsigned) : Period.parse(unsigned);
+ Temporal result = negative
+ ? ((Temporal) temporal).minus(temporalAmount) : ((Temporal)
temporal).plus(temporalAmount);
+ return new Val.Str(formatIso(result));
+ }
+
+ // A part of a date or time (as an ISO-8601 string): year, month, day,
hour, minutes, seconds, ...
+ private Val datePart(Val value, Val part) {
+ if (isNull(value)) {
+ return Val.Null$.MODULE$;
+ }
+ TemporalAccessor temporal = parseTemporal(string(value, "value"));
+ String name = string(part, "part");
+ if ("timezone".equals(name)) {
+ return temporal instanceof ZonedDateTime zdt ? new
Val.Str(zdt.getZone().getId()) : Val.Null$.MODULE$;
+ }
+ ChronoField field = switch (name) {
+ case "year" -> ChronoField.YEAR;
+ case "month" -> ChronoField.MONTH_OF_YEAR;
+ case "day" -> ChronoField.DAY_OF_MONTH;
+ case "hour" -> ChronoField.HOUR_OF_DAY;
+ case "minutes" -> ChronoField.MINUTE_OF_HOUR;
+ case "seconds" -> ChronoField.SECOND_OF_MINUTE;
+ case "milliseconds" -> ChronoField.MILLI_OF_SECOND;
+ case "nanoseconds" -> ChronoField.NANO_OF_SECOND;
+ case "dayOfWeek" -> ChronoField.DAY_OF_WEEK;
+ case "dayOfYear" -> ChronoField.DAY_OF_YEAR;
+ case "offsetSeconds" -> ChronoField.OFFSET_SECONDS;
+ case "quarter" -> null;
+ default -> throw new IllegalArgumentException("Unknown date part:
" + name);
+ };
+ if (field == null) {
+ return new Val.Num(temporal.get(IsoFields.QUARTER_OF_YEAR));
+ }
Review Comment:
⚠️ **Bug: `UnsupportedTemporalTypeException` when calling `datePart` with
`"quarter"` on a time-only input**
The `quarter` case maps `field` to `null`, and the `if (field == null)`
block calls `temporal.get(IsoFields.QUARTER_OF_YEAR)` directly — without an
`isSupported` guard. For time-only inputs (e.g. `"12:30:00"` parses as
`LocalTime`), `LocalTime` does not support `IsoFields.QUARTER_OF_YEAR` and will
throw `UnsupportedTemporalTypeException`.
The default path on the next line already uses `temporal.isSupported(field)`
correctly. The same guard should be applied here:
```suggestion
if (field == null) {
return temporal.isSupported(IsoFields.QUARTER_OF_YEAR)
? new Val.Num(temporal.get(IsoFields.QUARTER_OF_YEAR))
: Val.Null$.MODULE$;
}
```
##########
components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/CML.java:
##########
@@ -264,17 +291,188 @@ private Val formatDate(Val value, Val format) {
if (isNull(value)) {
return Val.Null$.MODULE$;
}
- if (!(value instanceof Val.Str valStr)) {
- throw new IllegalArgumentException("Expected String date value,
got: " + value.prettyName());
- }
if (!(format instanceof Val.Str fmtStr)) {
throw new IllegalArgumentException("Expected String format, got: "
+ format.prettyName());
}
- ZonedDateTime dateTime = parseToZonedDateTime(valStr.value());
+ ZonedDateTime dateTime;
+ if (value instanceof Val.Num num) {
+ // epoch milliseconds, as returned by parseDate
+ dateTime = Instant.ofEpochMilli((long)
num.value()).atZone(ZoneId.of("UTC"));
+ } else if (value instanceof Val.Str valStr) {
+ dateTime = parseToZonedDateTime(valStr.value());
+ } else {
+ throw new IllegalArgumentException("Expected String or Number date
value, got: " + value.prettyName());
+ }
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern(fmtStr.value());
return new Val.Str(dateTime.format(formatter));
}
+ // A number with a java.text.DecimalFormat pattern, or an ISO-8601 date or
time with a DateTimeFormatter pattern
+ private Val format(Val value, Val format) {
+ if (isNull(value)) {
+ return Val.Null$.MODULE$;
+ }
+ String pattern = string(format, "format");
+ if (value instanceof Val.Num num) {
+ DecimalFormat decimalFormat = new DecimalFormat(pattern,
DecimalFormatSymbols.getInstance(Locale.ROOT));
+ return new
Val.Str(decimalFormat.format(BigDecimal.valueOf(num.value())));
+ }
+ if (value instanceof Val.Str str) {
+ return new Val.Str(DateTimeFormatter.ofPattern(pattern,
Locale.ROOT).format(parseTemporal(str.value())));
+ }
+ throw new IllegalArgumentException("Cannot format " +
value.prettyName());
+ }
+
+ // A date or time (as an ISO-8601 string) of the given type (Date,
DateTime, LocalDateTime, Time or LocalTime),
+ // from a string in the given format (or ISO-8601 when the format is null)
or a number of seconds since the epoch
+ private Val parseDateTime(Val value, Val format, Val type) {
+ if (isNull(value)) {
+ return Val.Null$.MODULE$;
+ }
+ String kind = string(type, "type");
+ TemporalAccessor parsed;
+ if (value instanceof Val.Num num) {
+ parsed = Instant.ofEpochSecond((long)
num.value()).atZone(ZoneOffset.UTC);
+ } else if (value instanceof Val.Str str && isNull(format)) {
+ parsed = parseTemporal(str.value());
+ } else if (value instanceof Val.Str str) {
+ parsed = DateTimeFormatter.ofPattern(string(format, "format"),
Locale.ROOT).parse(str.value());
+ } else {
+ throw new IllegalArgumentException("Cannot convert " +
value.prettyName() + " to " + kind);
+ }
+ return new Val.Str(switch (kind) {
+ case "Date" -> LocalDate.from(parsed).toString();
+ case "DateTime" ->
DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(toZonedDateTime(parsed));
+ case "LocalDateTime" ->
DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(toLocalDateTime(parsed));
+ case "Time" ->
DateTimeFormatter.ISO_OFFSET_TIME.format(toOffsetTime(parsed));
+ case "LocalTime" ->
DateTimeFormatter.ISO_LOCAL_TIME.format(LocalTime.from(parsed));
+ default -> throw new IllegalArgumentException("Unknown date type:
" + kind);
+ });
+ }
+
+ // A date or time (as an ISO-8601 string) plus an ISO-8601 period or
duration such as P1D, PT1H or -P1M
+ private Val dateAdd(Val value, Val amount) {
+ if (isNull(value)) {
+ return Val.Null$.MODULE$;
+ }
+ String text = string(value, "value");
+ TemporalAccessor temporal = parseTemporal(text);
+ String period = string(amount, "amount");
+ boolean negative = period.startsWith("-");
+ String unsigned = negative ? period.substring(1) : period;
+ TemporalAmount temporalAmount = unsigned.contains("T") ?
Duration.parse(unsigned) : Period.parse(unsigned);
Review Comment:
💡 **Minor: `dateAdd` only handles the `-P1D` form of negative ISO-8601
durations**
The code strips a leading `-` and parses the unsigned remainder, then
negates the result. This handles the `-P1D` form correctly. However, ISO-8601
also allows negative components within the value itself (e.g. `P-1D`,
`P1DT-2H`), which `Duration.parse` / `Period.parse` handle natively.
If the `amount` field could ever contain the `P-1D` form (e.g. from an
upstream DataWeave expression or user input), the current code would strip the
`-` (nothing to strip), parse `P-1D` successfully, and then leave the sign
correct — but it would also parse `-P-1D` as a double-negation incorrectly.
Consider documenting which form is expected, or add a guard:
```java
// Only strip a leading '-'; mixed-sign forms (e.g. P-1D) are passed through
as-is
boolean negative = period.startsWith("-");
String unsigned = negative ? period.substring(1) : period;
```
Low severity — worth a comment or a test to clarify intent.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]