Skip to content

[Incubator kie issues#2023] Decision Engine: Support new property "value" for types date, date and time, years and months duration, days and time duration #6394

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 17, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.OffsetTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.chrono.ChronoPeriod;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
Expand Down Expand Up @@ -109,6 +115,9 @@ public static PropertyValueResult getDefinedValue(final Object current, final St
case "months":
result = ((ChronoPeriod) current).get(ChronoUnit.MONTHS) % 12;
break;
case "value":
result = (((ChronoPeriod) current).get(ChronoUnit.YEARS) * 12) + ((ChronoPeriod) current).get(ChronoUnit.MONTHS);
break;
default:
return PropertyValueResult.notDefined();
}
Expand All @@ -126,6 +135,9 @@ public static PropertyValueResult getDefinedValue(final Object current, final St
case "seconds":
result = ((Duration) current).getSeconds() % 60;
break;
case "value":
result = ((Duration) current).getSeconds();
break;
default:
return PropertyValueResult.notDefined();
}
Expand Down Expand Up @@ -167,6 +179,19 @@ public static PropertyValueResult getDefinedValue(final Object current, final St
case "weekday":
result = ((TemporalAccessor) current).get(ChronoField.DAY_OF_WEEK);
break;
case "value":
result = null;
if (current instanceof LocalTime) {
result = BigDecimal.valueOf(((LocalTime) current).toSecondOfDay());
} else if (current instanceof OffsetTime) {
result = BigDecimal.valueOf(((OffsetTime) current).toLocalTime().toSecondOfDay());
} else if (current instanceof LocalDate date) {
ZonedDateTime dtAtMidnightUTC = date.atStartOfDay(ZoneOffset.UTC);
result = BigDecimal.valueOf(dtAtMidnightUTC.toEpochSecond());
} else if (current instanceof ZonedDateTime) {
result = BigDecimal.valueOf(((ZonedDateTime) current).toEpochSecond());
}
break;
default:
return PropertyValueResult.notDefined();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,8 @@ private static Collection<Object[]> data() {
{ "date( 2016, 8, 2 ).month", BigDecimal.valueOf( 8 ) , null},
{ "date( 2016, 8, 2 ).day", BigDecimal.valueOf( 2 ) , null},
{ "date( 2017, 11, 8 ).weekday", BigDecimal.valueOf( 3 ), null},
{ "date( 2025, 7, 3 ).value", BigDecimal.valueOf(1751500800), null},
{ "date( 1970, 1, 1 ) + duration(\"PT1751500800S\")", LocalDate.of(2025,7,3), null},
{ "date and time(\"2016-07-29T05:48:23.765-05:00\").year", BigDecimal.valueOf( 2016 ) , null},
{ "date and time(\"2016-07-29T05:48:23.765-05:00\").month", BigDecimal.valueOf( 7 ) , null},
{ "date and time(\"2016-07-29T05:48:23.765-05:00\").day", BigDecimal.valueOf( 29 ) , null},
Expand All @@ -280,25 +282,34 @@ private static Collection<Object[]> data() {
{ "date and time(\"2016-07-29T05:48:23.765-05:00\").time offset", Duration.parse( "PT-5H" ), null},
{ "date and time(\"2018-12-10T10:30:00@Europe/Rome\").timezone", "Europe/Rome", null},
{ "date and time(\"2018-12-10T10:30:00@Etc/UTC\").timezone", "Etc/UTC", null},
{ "date and time(\"2016-07-29T05:48:23.765-05:00\").value", BigDecimal.valueOf(1469789303) , null},
{ "date and time(\"2025-07-08T10:00:00Z\").value", BigDecimal.valueOf(1751968800) , null},
{ "date and time(\"1970-01-01T00:00:00Z\") + duration(\"PT1751968800S\")", ZonedDateTime.of(2025, 7, 8, 10, 0, 0, 0, ZoneId.of("Z").normalized()) , null},
{ "time(\"13:20:00-05:00\").hour", BigDecimal.valueOf( 13 ), null},
{ "time(\"13:20:00-05:00\").minute", BigDecimal.valueOf( 20 ), null},
{ "time(\"13:20:00-05:00\").second", BigDecimal.valueOf( 0 ), null},
{ "time(\"13:20:00-05:00\").time offset", Duration.parse( "PT-5H" ), null},
{ "time(\"13:20:00@Europe/Rome\").timezone", "Europe/Rome" , null},
{ "time(\"13:20:00@Etc/UTC\").timezone", "Etc/UTC" , null},
{ "time(\"13:20:00@Etc/GMT\").timezone", "Etc/GMT" , null},
{ "time(\"13:20:00-05:00\").value", BigDecimal.valueOf(48000), null},
{ "time(\"01:01:01\").value", BigDecimal.valueOf(3661), null},
{ "time(\"00:00:00\") + duration(\"PT3661S\")", LocalTime.of(1, 1, 1), null},
{ "duration( \"P2DT20H14M\" ).days", BigDecimal.valueOf(2) , null},
{ "duration( \"P2DT20H14M\" ).hours", BigDecimal.valueOf(20) , null},
{ "duration( \"P2DT20H14M\" ).minutes", BigDecimal.valueOf(14) , null},
{ "duration( \"P2DT20H14M5S\" ).seconds", BigDecimal.valueOf(5) , null},
{ "duration( \"P1DT1H\" ).value", BigDecimal.valueOf(90000) , null},
{ "duration(\"P1Y\").years", BigDecimal.valueOf(1) , null},
{ "duration(\"P1Y\").months", BigDecimal.valueOf(0) , null},
{ "duration(\"P1Y\").days", null , null},
{ "duration(\"P1Y\").hours", null , null},
{ "duration(\"P1Y\").minutes", null , null},
{ "duration(\"P1Y\").seconds", null , null},
{ "years and months duration( date(\"2011-12-22\"), date(\"2013-08-24\") ).years", BigDecimal.valueOf(1) , null},
{ "years and months duration( date(\"2011-12-22\"), date(\"2013-08-24\") ).months", BigDecimal.valueOf(8) , null},
{ "duration(\"P1Y1M\").value", BigDecimal.valueOf(13) , null},
{ "years and months duration( date(\"2011-12-22\"), date(\"2013-08-24\") ).years", BigDecimal.valueOf(1), null},
{ "years and months duration( date(\"2011-12-22\"), date(\"2013-08-24\") ).months", BigDecimal.valueOf(8), null},
{ "years and months duration( date(\"2011-12-22\"), date(\"2013-08-24\") ).value", BigDecimal.valueOf(20), null},
{ "date and time(\"2017-05-14\")", LocalDateTime.of( 2017, 5, 14, 0, 0, 0, 0 ) , null},
{ "date(\"2017-05-12\")-date(\"2017-04-25\")", Duration.ofDays( 17 ) , null},
{ "time(date(\"2017-08-10\"))", DateTimeFormatter.ISO_TIME.parse( "00:00:00z", OffsetTime::from ) , null },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,19 @@
import org.junit.jupiter.api.Test;
import org.kie.dmn.feel.lang.FEELProperty;

import java.math.BigDecimal;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.chrono.ChronoPeriod;
import java.time.temporal.ChronoUnit;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.kie.dmn.feel.util.EvalHelper.getDefinedValue;

class EvalHelperTest {

Expand All @@ -43,4 +55,60 @@ public String getAProperty() {
return null;
}
}

@Test
void testValueForLocalTime() {
LocalTime localTime = LocalTime.of(1, 1, 1);
EvalHelper.PropertyValueResult value = getDefinedValue(localTime, "value");
Optional<Object> result = value.getValueResult().getRight();
long secondsToAdd = ((BigDecimal) result.orElseThrow(
() -> new AssertionError("Missing result for localTime: " + localTime))).longValue();
LocalTime roundTripTime = LocalTime.of(0, 0, 0).plusSeconds(secondsToAdd);
assertThat(localTime).isEqualTo(roundTripTime);
}

@Test
void testValueForZonedDateTime() {
ZonedDateTime zonedDateTime = ZonedDateTime.of(2025, 7, 8, 10, 0, 0, 0, ZoneId.of("Z"));
EvalHelper.PropertyValueResult value = getDefinedValue(zonedDateTime, "value");
Optional<Object> result = value.getValueResult().getRight();
long secondsToAdd = ((BigDecimal) result.orElseThrow(
() -> new AssertionError("Missing result for zonedDateTime: " + zonedDateTime))).longValue();
ZonedDateTime roundTrip = ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneId.of("Z")).plusSeconds(secondsToAdd);
assertThat(roundTrip).isEqualTo(zonedDateTime);
}

@Test
void testValueForDate() {
LocalDate localDate = LocalDate.of(2025, 7, 3);
EvalHelper.PropertyValueResult value = getDefinedValue(localDate, "value");
Optional<Object> result = value.getValueResult().getRight();
long secondsToAdd = ((BigDecimal) result.orElseThrow(
() -> new AssertionError("Missing result for duration: " + localDate))).longValue();
LocalDate roundTrip = LocalDate.from(ZonedDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneId.of("Z")).plusSeconds(secondsToAdd));
assertThat(roundTrip).isEqualTo(localDate);
}

@Test
void testValueForDuration() {
Duration duration = Duration.of(1, ChronoUnit.DAYS).plusHours(1);
EvalHelper.PropertyValueResult value = getDefinedValue(duration, "value");
Optional<Object> result = value.getValueResult().getRight();
long secondsToAdd = ((BigDecimal) result.orElseThrow(
() -> new AssertionError("Missing result for duration: " + duration))).longValue();
Duration roundTrip = Duration.of(0, ChronoUnit.HOURS).plusSeconds(secondsToAdd);
assertThat(roundTrip).isEqualTo(duration);
}

@Test
void testValueForDurationYears() {
ChronoPeriod period = Period.parse("P2Y1M");
EvalHelper.PropertyValueResult value = getDefinedValue(period, "value");
Optional<Object> result = value.getValueResult().getRight();
long durationToAdd = ((BigDecimal) result.orElseThrow(
() -> new AssertionError("Missing result for period: " + period))).longValue();
Period roundTrip = Period.ofYears(0).plusMonths(durationToAdd);
assertThat(roundTrip.normalized()).isEqualTo(period);
}

}