This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 7eb4f5c7ea Issue #2584 : Add Calculator conversions between JDE Julian
and ISO dates (#8257)
7eb4f5c7ea is described below
commit 7eb4f5c7eacfdc7ff3c8cc55a0ce8b8c8f9af000
Author: Matt Casters <[email protected]>
AuthorDate: Fri Sep 4 15:41:13 2026 +0200
Issue #2584 : Add Calculator conversions between JDE Julian and ISO dates
(#8257)
---
.../ROOT/pages/pipeline/transforms/calculator.adoc | 7 +
.../transforms/0011-calculator-jde-julian.hpl | 193 ++++++++++++
.../datasets/golden-calculator-jde-julian.csv | 8 +
.../dataset/golden-calculator-jde-julian.json | 40 +++
.../unit-test/0011-calculator-jde-julian UNIT.json | 41 +++
.../transforms/calculator/CalculationType.java | 4 +
.../calculations/date/DateToJdeJulian.java | 54 ++++
.../calculator/calculations/date/JdeJulian.java | 83 ++++++
.../calculations/date/JdeJulianToDate.java | 54 ++++
.../calculator/messages/messages_en_US.properties | 6 +-
.../samples/transforms/calculator-jde-julian.hpl | 208 +++++++++++++
.../transforms/calculator/JdeJulianTest.java | 323 +++++++++++++++++++++
12 files changed, 1020 insertions(+), 1 deletion(-)
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calculator.adoc
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calculator.adoc
index 64a3b2f1aa..7c3151d184 100644
---
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calculator.adoc
+++
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/calculator.adoc
@@ -203,6 +203,13 @@ TIP: Only integer values for B are supported. If you need
non-integer calculatio
|Base64 Decode|BASE64_DECODE|Decode a Base64 encoded string. Supports both
padded or non-padded input.
|First day of month (A) date|FIRST_DAY_OF_MONTH|Set the day of field (A) to 1.
|Last day of month (A) date|LAST_DAY_OF_MONTH|Set the day of field (A) to the
last day of the given month.
+|Date A to JDE Julian (CYYDDD)|DATE_TO_JDE_JULIAN a|Convert date A to a JD
Edwards Julian date stored as CYYDDD. +
+
+C is the century relative to 1900 (0 = 1900-1999, 1 = 2000-2099), YY is the
two-digit year, and DDD is the day of the year (001-366). +
+January 1, 1995 becomes `95001`. January 1, 2009 becomes `109001`. Years
before 1900 are not supported.
+|JDE Julian A (CYYDDD) to Date|JDE_JULIAN_TO_DATE a|Convert a JD Edwards
Julian date in field A to a Date. +
+
+Field A can be an Integer, Number or String in CYYDDD format. Five-digit
values such as `95001` are treated as 1 January 1995.
|Encode a URL|URL_ENCODE|Encode a URL to have browser/webservice save url
calls.
|Decode a URL|URL_DECODE|Decode a URL.
|===
diff --git a/integration-tests/transforms/0011-calculator-jde-julian.hpl
b/integration-tests/transforms/0011-calculator-jde-julian.hpl
new file mode 100644
index 0000000000..fd082ed819
--- /dev/null
+++ b/integration-tests/transforms/0011-calculator-jde-julian.hpl
@@ -0,0 +1,193 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+ <info>
+ <name>0011-calculator-jde-julian</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description/>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/09/03 00:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/09/03 00:00:00.000</modified_date>
+ <key_for_session_key/>
+ <is_key_private>N</is_key_private>
+ </info>
+ <notepads>
+ </notepads>
+ <order>
+ <hop>
+ <from>test-data</from>
+ <to>Calculator</to>
+ <enabled>Y</enabled>
+ </hop>
+ <hop>
+ <from>Calculator</from>
+ <to>Verify</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>test-data</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>isoDate</name>
+ <type>Date</type>
+ <format>yyyy-MM-dd</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>julianIn</name>
+ <type>Integer</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <data>
+ <line>
+ <item>1995-01-01</item>
+ <item>95001</item>
+ </line>
+ <line>
+ <item>1997-11-04</item>
+ <item>97308</item>
+ </line>
+ <line>
+ <item>2009-01-01</item>
+ <item>109001</item>
+ </line>
+ <line>
+ <item>2011-01-13</item>
+ <item>111013</item>
+ </line>
+ <line>
+ <item>2000-08-23</item>
+ <item>100236</item>
+ </line>
+ <line>
+ <item>2000-12-31</item>
+ <item>100366</item>
+ </line>
+ <line>
+ <item>1999-12-31</item>
+ <item>99365</item>
+ </line>
+ </data>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Calculator</name>
+ <type>Calculator</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <failIfNoFile>Y</failIfNoFile>
+ <calculation>
+ <field_name>julianOut</field_name>
+ <calc_type>DATE_TO_JDE_JULIAN</calc_type>
+ <field_a>isoDate</field_a>
+ <field_b/>
+ <field_c/>
+ <value_type>Integer</value_type>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <remove>N</remove>
+ <conversion_mask/>
+ <decimal_symbol/>
+ <grouping_symbol/>
+ <currency_symbol/>
+ </calculation>
+ <calculation>
+ <field_name>isoOut</field_name>
+ <calc_type>JDE_JULIAN_TO_DATE</calc_type>
+ <field_a>julianIn</field_a>
+ <field_b/>
+ <field_c/>
+ <value_type>Date</value_type>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <remove>N</remove>
+ <conversion_mask>yyyy-MM-dd</conversion_mask>
+ <decimal_symbol/>
+ <grouping_symbol/>
+ <currency_symbol/>
+ </calculation>
+ <attributes/>
+ <GUI>
+ <xloc>240</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Verify</name>
+ <type>Dummy</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <attributes/>
+ <GUI>
+ <xloc>416</xloc>
+ <yloc>80</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git
a/integration-tests/transforms/datasets/golden-calculator-jde-julian.csv
b/integration-tests/transforms/datasets/golden-calculator-jde-julian.csv
new file mode 100644
index 0000000000..8b57c331ce
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-calculator-jde-julian.csv
@@ -0,0 +1,8 @@
+isoDate,julianIn,julianOut,isoOut
+1995-01-01,95001,95001,1995-01-01
+1997-11-04,97308,97308,1997-11-04
+2009-01-01,109001,109001,2009-01-01
+2011-01-13,111013,111013,2011-01-13
+2000-08-23,100236,100236,2000-08-23
+2000-12-31,100366,100366,2000-12-31
+1999-12-31,99365,99365,1999-12-31
diff --git
a/integration-tests/transforms/metadata/dataset/golden-calculator-jde-julian.json
b/integration-tests/transforms/metadata/dataset/golden-calculator-jde-julian.json
new file mode 100644
index 0000000000..f554ed8f6a
--- /dev/null
+++
b/integration-tests/transforms/metadata/dataset/golden-calculator-jde-julian.json
@@ -0,0 +1,40 @@
+{
+ "base_filename": "golden-calculator-jde-julian.csv",
+ "name": "golden-calculator-jde-julian",
+ "description": "",
+ "dataset_fields": [
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 3,
+ "field_precision": -1,
+ "field_format": "yyyy-MM-dd",
+ "field_name": "isoDate"
+ },
+ {
+ "field_comment": "",
+ "field_length": -1,
+ "field_type": 5,
+ "field_precision": 0,
+ "field_format": "####0;-####0",
+ "field_name": "julianIn"
+ },
+ {
+ "field_comment": "DATE_TO_JDE_JULIAN",
+ "field_length": -1,
+ "field_type": 5,
+ "field_precision": 0,
+ "field_format": "####0;-####0",
+ "field_name": "julianOut"
+ },
+ {
+ "field_comment": "JDE_JULIAN_TO_DATE",
+ "field_length": -1,
+ "field_type": 3,
+ "field_precision": -1,
+ "field_format": "yyyy-MM-dd",
+ "field_name": "isoOut"
+ }
+ ],
+ "folder_name": ""
+}
diff --git
a/integration-tests/transforms/metadata/unit-test/0011-calculator-jde-julian
UNIT.json
b/integration-tests/transforms/metadata/unit-test/0011-calculator-jde-julian
UNIT.json
new file mode 100644
index 0000000000..2ae7d73f15
--- /dev/null
+++
b/integration-tests/transforms/metadata/unit-test/0011-calculator-jde-julian
UNIT.json
@@ -0,0 +1,41 @@
+{
+ "variableValues": [],
+ "database_replacements": [],
+ "autoOpening": false,
+ "basePath": "",
+ "golden_data_sets": [
+ {
+ "field_mappings": [
+ {
+ "transform_field": "isoDate",
+ "data_set_field": "isoDate"
+ },
+ {
+ "transform_field": "julianIn",
+ "data_set_field": "julianIn"
+ },
+ {
+ "transform_field": "julianOut",
+ "data_set_field": "julianOut"
+ },
+ {
+ "transform_field": "isoOut",
+ "data_set_field": "isoOut"
+ }
+ ],
+ "field_order": [
+ "isoDate",
+ "julianIn"
+ ],
+ "transform_name": "Verify",
+ "data_set_name": "golden-calculator-jde-julian"
+ }
+ ],
+ "input_data_sets": [],
+ "name": "0011-calculator-jde-julian UNIT",
+ "description": "",
+ "trans_test_tweaks": [],
+ "persist_filename": "",
+ "pipeline_filename": "./0011-calculator-jde-julian.hpl",
+ "test_type": "UNIT_TEST"
+}
diff --git
a/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/CalculationType.java
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/CalculationType.java
index 27a338de3e..e6c1f6ac7e 100644
---
a/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/CalculationType.java
+++
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/CalculationType.java
@@ -46,12 +46,14 @@ import
org.apache.hop.pipeline.transforms.calculator.calculations.date.AddMonths
import
org.apache.hop.pipeline.transforms.calculator.calculations.date.AddSeconds;
import
org.apache.hop.pipeline.transforms.calculator.calculations.date.AddTimeToDate;
import
org.apache.hop.pipeline.transforms.calculator.calculations.date.DateDiff;
+import
org.apache.hop.pipeline.transforms.calculator.calculations.date.DateToJdeJulian;
import
org.apache.hop.pipeline.transforms.calculator.calculations.date.DateWorkingDiff;
import
org.apache.hop.pipeline.transforms.calculator.calculations.date.DayOfMonth;
import
org.apache.hop.pipeline.transforms.calculator.calculations.date.DayOfWeek;
import
org.apache.hop.pipeline.transforms.calculator.calculations.date.DayOfYear;
import
org.apache.hop.pipeline.transforms.calculator.calculations.date.FirstDayOfMonth;
import
org.apache.hop.pipeline.transforms.calculator.calculations.date.HourOfDay;
+import
org.apache.hop.pipeline.transforms.calculator.calculations.date.JdeJulianToDate;
import
org.apache.hop.pipeline.transforms.calculator.calculations.date.LastDayOfMonth;
import
org.apache.hop.pipeline.transforms.calculator.calculations.date.MinuteOfHour;
import
org.apache.hop.pipeline.transforms.calculator.calculations.date.MonthOfDate;
@@ -210,6 +212,8 @@ public enum CalculationType implements
IEnumHasCodeAndDescription {
BASE64_DECODE(new Base64Decode()),
FIRST_DAY_OF_MONTH(new FirstDayOfMonth()),
LAST_DAY_OF_MONTH(new LastDayOfMonth()),
+ DATE_TO_JDE_JULIAN(new DateToJdeJulian()),
+ JDE_JULIAN_TO_DATE(new JdeJulianToDate()),
URL_ENCODE(new UrlEncode()),
URL_DECODE(new UrlDecode());
diff --git
a/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/date/DateToJdeJulian.java
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/date/DateToJdeJulian.java
new file mode 100644
index 0000000000..ceb979efad
--- /dev/null
+++
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/date/DateToJdeJulian.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.calculator.calculations.date;
+
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.transforms.calculator.CalculationInput;
+import org.apache.hop.pipeline.transforms.calculator.CalculationOutput;
+import org.apache.hop.pipeline.transforms.calculator.ICalculation;
+
+public class DateToJdeJulian implements ICalculation {
+ private static final String description =
+ BaseMessages.getString(PKG,
"CalculatorMetaFunction.CalcFunctions.DateToJdeJulian");
+
+ @Override
+ public String getCode() {
+ return "DATE_TO_JDE_JULIAN";
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public int getDefaultResultType() {
+ return IValueMeta.TYPE_INTEGER;
+ }
+
+ @Override
+ public CalculationOutput calculate(CalculationInput in) throws
HopValueException {
+ Object value = null;
+ if (in.metaA != null && !in.metaA.isNull(in.dataA)) {
+ value = JdeJulian.fromDate(in.metaA.getDate(in.dataA));
+ }
+ return new CalculationOutput(getDefaultResultType(), value);
+ }
+}
diff --git
a/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/date/JdeJulian.java
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/date/JdeJulian.java
new file mode 100644
index 0000000000..0b85c4777b
--- /dev/null
+++
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/date/JdeJulian.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.calculator.calculations.date;
+
+import java.text.SimpleDateFormat;
+import java.time.DateTimeException;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Calendar;
+import java.util.Date;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.transforms.calculator.ICalculation;
+
+/**
+ * Converts between a {@link Date} and a JD Edwards Julian date stored as
CYYDDD.
+ *
+ * <p>C is the century relative to 1900 (0 = 1900-1999, 1 = 2000-2099), YY is
the two-digit year,
+ * and DDD is the day of the year (001-366). Examples: 95001 is 1995-01-01,
109001 is 2009-01-01.
+ */
+public final class JdeJulian {
+ private JdeJulian() {}
+
+ public static Long fromDate(Date date) throws HopValueException {
+ if (date == null) {
+ return null;
+ }
+
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(date);
+ int year = calendar.get(Calendar.YEAR);
+ if (year < 1900) {
+ throw new HopValueException(
+ BaseMessages.getString(
+ ICalculation.PKG, "Calculator.Error.DateBeforeJdeEpoch",
formatDate(date)));
+ }
+
+ return (year - 1900) * 1000L + calendar.get(Calendar.DAY_OF_YEAR);
+ }
+
+ public static Date toDate(Long julian) throws HopValueException {
+ if (julian == null) {
+ return null;
+ }
+
+ long yearLong = 1900L + julian / 1000L;
+ int dayOfYear = (int) (julian % 1000L);
+ if (julian < 1L || yearLong < 1900L || yearLong > LocalDate.MAX.getYear())
{
+ throw invalidJulian(julian);
+ }
+
+ try {
+ LocalDate localDate = LocalDate.ofYearDay((int) yearLong, dayOfYear);
+ return
Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
+ } catch (DateTimeException e) {
+ throw invalidJulian(julian);
+ }
+ }
+
+ private static HopValueException invalidJulian(long julian) {
+ return new HopValueException(
+ BaseMessages.getString(ICalculation.PKG,
"Calculator.Error.InvalidJdeJulian", julian));
+ }
+
+ private static String formatDate(Date date) {
+ return new SimpleDateFormat("yyyy-MM-dd").format(date);
+ }
+}
diff --git
a/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/date/JdeJulianToDate.java
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/date/JdeJulianToDate.java
new file mode 100644
index 0000000000..e5e798a791
--- /dev/null
+++
b/plugins/transforms/calculator/src/main/java/org/apache/hop/pipeline/transforms/calculator/calculations/date/JdeJulianToDate.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.calculator.calculations.date;
+
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.pipeline.transforms.calculator.CalculationInput;
+import org.apache.hop.pipeline.transforms.calculator.CalculationOutput;
+import org.apache.hop.pipeline.transforms.calculator.ICalculation;
+
+public class JdeJulianToDate implements ICalculation {
+ private static final String description =
+ BaseMessages.getString(PKG,
"CalculatorMetaFunction.CalcFunctions.JdeJulianToDate");
+
+ @Override
+ public String getCode() {
+ return "JDE_JULIAN_TO_DATE";
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public int getDefaultResultType() {
+ return IValueMeta.TYPE_DATE;
+ }
+
+ @Override
+ public CalculationOutput calculate(CalculationInput in) throws
HopValueException {
+ Object value = null;
+ if (in.metaA != null && !in.metaA.isNull(in.dataA)) {
+ value = JdeJulian.toDate(in.metaA.getInteger(in.dataA));
+ }
+ return new CalculationOutput(getDefaultResultType(), value);
+ }
+}
diff --git
a/plugins/transforms/calculator/src/main/resources/org/apache/hop/pipeline/transforms/calculator/messages/messages_en_US.properties
b/plugins/transforms/calculator/src/main/resources/org/apache/hop/pipeline/transforms/calculator/messages/messages_en_US.properties
index 774fbbf584..05a4e6eaf6 100644
---
a/plugins/transforms/calculator/src/main/resources/org/apache/hop/pipeline/transforms/calculator/messages/messages_en_US.properties
+++
b/plugins/transforms/calculator/src/main/resources/org/apache/hop/pipeline/transforms/calculator/messages/messages_en_US.properties
@@ -17,6 +17,8 @@
BaseTransform.TypeLongDesc.Calculator=Calculator
BaseTransform.TypeTooltipDesc.Calculator=Create new fields by performing
simple calculations
+Calculator.Error.DateBeforeJdeEpoch=Date {0} is before the year 1900 and
cannot be converted to a JD Edwards Julian date (CYYDDD)
+Calculator.Error.InvalidJdeJulian=Invalid JD Edwards Julian date {0}. Expected
CYYDDD (century, two-digit year, day of year)
Calculator.Error.NoNameField=There is no name specified for calculated field
\# {0}
Calculator.Error.UnableFindField=Unable to find the specified fieldname '{0}'
for calculation \#{1}
Calculator.ErrorInTransformRunning=Error while running the transform
@@ -63,7 +65,7 @@ CalculatorMeta.Injection.Calculation.ValuePrecision=Precision
CalculatorMeta.Injection.Calculation.ValueType=Output value type
CalculatorMeta.Injection.Calculations=List of calculations
CalculatorMeta.Injection.FailIfNoFile=Fail if no file
-CalculatorMeta.keyword=calculate,math,formula,arithmetic,compute
+CalculatorMeta.keyword=calculate,math,formula,arithmetic,compute,julian,jde,date
CalculatorMetaFunction.CalcFunctions.Abs=Absolute value ABS( A )
CalculatorMetaFunction.CalcFunctions.AddSeconds=Date A + B Seconds
CalculatorMetaFunction.CalcFunctions.AddTimeToDate=Add time B to date A
@@ -89,6 +91,7 @@ CalculatorMetaFunction.CalcFunctions.DatePlusDays=Date A + B
Days
CalculatorMetaFunction.CalcFunctions.DatePlusHours=Date A + B Hours
CalculatorMetaFunction.CalcFunctions.DatePlusMinutes=Date A + B Minutes
CalculatorMetaFunction.CalcFunctions.DatePlusMonths=Date A + B Months
+CalculatorMetaFunction.CalcFunctions.DateToJdeJulian=Date A to JDE Julian
(CYYDDD)
CalculatorMetaFunction.CalcFunctions.DayOfMonth=Day of month of date A
CalculatorMetaFunction.CalcFunctions.DayOfWeek=Day of week of date A
CalculatorMetaFunction.CalcFunctions.DayOfYear=Day of year of date A
@@ -96,6 +99,7 @@ CalculatorMetaFunction.CalcFunctions.DoubleMetaphone=Double
metaphone of A(phone
CalculatorMetaFunction.CalcFunctions.EscapeHTML=Escape HTML content
CalculatorMetaFunction.CalcFunctions.EscapeSQL=Escape SQL content
CalculatorMetaFunction.CalcFunctions.FirstDayOfMonthDate=First day of month
(A) date
+CalculatorMetaFunction.CalcFunctions.JdeJulianToDate=JDE Julian A (CYYDDD) to
Date
CalculatorMetaFunction.CalcFunctions.LastDayOfMonthDate=Last day of month (A)
date
CalculatorMetaFunction.CalcFunctions.Floor=FLOOR( A )
CalculatorMetaFunction.CalcFunctions.GetFileEncoding=Get encoding of file A
diff --git
a/plugins/transforms/calculator/src/main/samples/transforms/calculator-jde-julian.hpl
b/plugins/transforms/calculator/src/main/samples/transforms/calculator-jde-julian.hpl
new file mode 100644
index 0000000000..cc25a6dcb4
--- /dev/null
+++
b/plugins/transforms/calculator/src/main/samples/transforms/calculator-jde-julian.hpl
@@ -0,0 +1,208 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+ <info>
+ <name>calculator-jde-julian</name>
+ <name_sync_with_filename>Y</name_sync_with_filename>
+ <description>Cross convert between JD Edwards Julian dates (CYYDDD) and
ISO dates</description>
+ <extended_description/>
+ <pipeline_version/>
+ <pipeline_type>Normal</pipeline_type>
+ <parameters>
+ </parameters>
+ <capture_transform_performance>N</capture_transform_performance>
+
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+ <created_user>-</created_user>
+ <created_date>2026/09/03 00:00:00.000</created_date>
+ <modified_user>-</modified_user>
+ <modified_date>2026/09/03 00:00:00.000</modified_date>
+ <key_for_session_key/>
+ <is_key_private>N</is_key_private>
+ </info>
+ <notepads>
+ <notepad>
+ <note>JD Edwards stores dates as CYYDDD.
+
+C = century relative to 1900 (0 = 1900-1999, 1 = 2000-2099)
+YY = two-digit year
+DDD = day of the year (001-366)
+
+95001 = 1 January 1995
+97318 = 14 November 1997
+109001 = 1 January 2009
+111013 = 13 January 2011</note>
+ <xloc>64</xloc>
+ <yloc>256</yloc>
+ <width>520</width>
+ <heigth>170</heigth>
+ <fontname>Segoe UI</fontname>
+ <fontsize>11</fontsize>
+ <fontbold>N</fontbold>
+ <fontitalic>N</fontitalic>
+ <fontcolorred>14</fontcolorred>
+ <fontcolorgreen>58</fontcolorgreen>
+ <fontcolorblue>90</fontcolorblue>
+ <backgroundcolorred>201</backgroundcolorred>
+ <backgroundcolorgreen>232</backgroundcolorgreen>
+ <backgroundcolorblue>251</backgroundcolorblue>
+ <bordercolorred>14</bordercolorred>
+ <bordercolorgreen>58</bordercolorgreen>
+ <bordercolorblue>90</bordercolorblue>
+ </notepad>
+ </notepads>
+ <order>
+ <hop>
+ <from>Test date cases</from>
+ <to>Calculator</to>
+ <enabled>Y</enabled>
+ </hop>
+ </order>
+ <transform>
+ <name>Calculator</name>
+ <type>Calculator</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <failIfNoFile>Y</failIfNoFile>
+ <calculation>
+ <field_name>julianOut</field_name>
+ <calc_type>DATE_TO_JDE_JULIAN</calc_type>
+ <field_a>isoDate</field_a>
+ <field_b/>
+ <field_c/>
+ <value_type>Integer</value_type>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <remove>N</remove>
+ <conversion_mask/>
+ <decimal_symbol/>
+ <grouping_symbol/>
+ <currency_symbol/>
+ </calculation>
+ <calculation>
+ <field_name>isoOut</field_name>
+ <calc_type>JDE_JULIAN_TO_DATE</calc_type>
+ <field_a>julianIn</field_a>
+ <field_b/>
+ <field_c/>
+ <value_type>Date</value_type>
+ <value_length>-1</value_length>
+ <value_precision>-1</value_precision>
+ <remove>N</remove>
+ <conversion_mask>yyyy-MM-dd</conversion_mask>
+ <decimal_symbol/>
+ <grouping_symbol/>
+ <currency_symbol/>
+ </calculation>
+ <attributes/>
+ <GUI>
+ <xloc>336</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform>
+ <name>Test date cases</name>
+ <type>DataGrid</type>
+ <description/>
+ <distribute>Y</distribute>
+ <custom_distribution/>
+ <copies>1</copies>
+ <partitioning>
+ <method>none</method>
+ <schema_name/>
+ </partitioning>
+ <fields>
+ <field>
+ <name>isoDate</name>
+ <type>Date</type>
+ <format>yyyy-MM-dd</format>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>julianIn</name>
+ <type>Integer</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ <field>
+ <name>note</name>
+ <type>String</type>
+ <format/>
+ <currency/>
+ <decimal/>
+ <group/>
+ <length>-1</length>
+ <precision>-1</precision>
+ <set_empty_string>N</set_empty_string>
+ </field>
+ </fields>
+ <data>
+ <line>
+ <item>1995-01-01</item>
+ <item>95001</item>
+ <item>The 1st day of 1995</item>
+ </line>
+ <line>
+ <item>1997-11-04</item>
+ <item>97308</item>
+ <item>The 308th day of 1997</item>
+ </line>
+ <line>
+ <item>2009-01-01</item>
+ <item>109001</item>
+ <item>The 1st day of 2009</item>
+ </line>
+ <line>
+ <item>2011-01-13</item>
+ <item>111013</item>
+ <item>The 13th day of 2011</item>
+ </line>
+ <line>
+ <item>2000-08-23</item>
+ <item>100236</item>
+ <item>The 236th day of leap year 2000</item>
+ </line>
+ </data>
+ <attributes/>
+ <GUI>
+ <xloc>96</xloc>
+ <yloc>96</yloc>
+ </GUI>
+ </transform>
+ <transform_error_handling>
+ </transform_error_handling>
+ <attributes/>
+</pipeline>
diff --git
a/plugins/transforms/calculator/src/test/java/org/apache/hop/pipeline/transforms/calculator/JdeJulianTest.java
b/plugins/transforms/calculator/src/test/java/org/apache/hop/pipeline/transforms/calculator/JdeJulianTest.java
new file mode 100644
index 0000000000..6391f0096a
--- /dev/null
+++
b/plugins/transforms/calculator/src/test/java/org/apache/hop/pipeline/transforms/calculator/JdeJulianTest.java
@@ -0,0 +1,323 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.pipeline.transforms.calculator;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.stream.Stream;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.IRowSet;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.exception.HopValueException;
+import org.apache.hop.core.logging.ILoggingObject;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.IValueMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaDate;
+import org.apache.hop.core.row.value.ValueMetaInteger;
+import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.apache.hop.core.row.value.ValueMetaString;
+import org.apache.hop.junit.rules.RestoreHopEngineEnvironmentExtension;
+import org.apache.hop.pipeline.transform.RowAdapter;
+import
org.apache.hop.pipeline.transforms.calculator.calculations.date.DateToJdeJulian;
+import
org.apache.hop.pipeline.transforms.calculator.calculations.date.JdeJulian;
+import
org.apache.hop.pipeline.transforms.calculator.calculations.date.JdeJulianToDate;
+import org.apache.hop.pipeline.transforms.mock.TransformMockHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class JdeJulianTest {
+ private static final SimpleDateFormat ISO_DATE = new
SimpleDateFormat("yyyy-MM-dd");
+
+ private TransformMockHelper<CalculatorMeta, CalculatorData> smh;
+
+ @RegisterExtension
+ static RestoreHopEngineEnvironmentExtension env = new
RestoreHopEngineEnvironmentExtension();
+
+ @BeforeAll
+ static void init() throws HopException {
+ HopEnvironment.init();
+ }
+
+ @BeforeEach
+ void setUp() {
+ smh = new TransformMockHelper<>("Calculator", CalculatorMeta.class,
CalculatorData.class);
+ when(smh.logChannelFactory.create(any(), any(ILoggingObject.class)))
+ .thenReturn(smh.iLogChannel);
+ when(smh.pipeline.isRunning()).thenReturn(true);
+ }
+
+ @AfterEach
+ void cleanUp() {
+ smh.cleanUp();
+ }
+
+ static Stream<Arguments> sampleConversions() {
+ return Stream.of(
+ Arguments.of("1995-01-01", 95001L),
+ Arguments.of("1997-11-04", 97308L),
+ Arguments.of("1997-11-14", 97318L),
+ Arguments.of("2000-08-23", 100236L),
+ Arguments.of("2000-12-31", 100366L),
+ Arguments.of("1999-12-31", 99365L),
+ Arguments.of("2009-01-01", 109001L),
+ Arguments.of("2011-01-13", 111013L),
+ Arguments.of("1900-01-01", 1L),
+ Arguments.of("2000-02-29", 100060L));
+ }
+
+ @ParameterizedTest
+ @MethodSource("sampleConversions")
+ void convertsDateToJulianAndBack(String isoDate, long julian) throws
Exception {
+ Date date = ISO_DATE.parse(isoDate);
+ assertEquals(julian, JdeJulian.fromDate(date));
+ assertEquals(midnight(date), JdeJulian.toDate(julian));
+ }
+
+ @Test
+ void fromDateUsesDayOfYearAndIgnoresTime() throws Exception {
+ Date afternoon = new SimpleDateFormat("yyyy-MM-dd
HH:mm:ss").parse("2009-01-01 15:30:45");
+ assertEquals(109001L, JdeJulian.fromDate(afternoon));
+ assertEquals(ISO_DATE.parse("2009-01-01"), JdeJulian.toDate(109001L));
+ }
+
+ @Test
+ void nullInputsStayNull() throws Exception {
+ assertNull(JdeJulian.fromDate(null));
+ assertNull(JdeJulian.toDate(null));
+
+ DateToJdeJulian dateToJulian = new DateToJdeJulian();
+ JdeJulianToDate julianToDate = new JdeJulianToDate();
+ ValueMetaDate dateMeta = new ValueMetaDate("isoDate");
+ ValueMetaInteger julianMeta = new ValueMetaInteger("julian");
+
+ assertNull(
+ dateToJulian.calculate(
+ new CalculationInput(
+ dateMeta, null, null, null, null, null, null, null, null,
false))
+ .value);
+ assertNull(
+ julianToDate.calculate(
+ new CalculationInput(
+ julianMeta, null, null, null, null, null, null, null,
null, false))
+ .value);
+ }
+
+ @Test
+ void rejectsDatesBefore1900() throws Exception {
+ Date date = ISO_DATE.parse("1899-12-31");
+ HopValueException exception =
+ assertThrows(HopValueException.class, () -> JdeJulian.fromDate(date));
+ assertTrue(exception.getMessage().contains("1899-12-31"));
+ }
+
+ @Test
+ void rejectsInvalidJulianValues() {
+ assertThrows(HopValueException.class, () -> JdeJulian.toDate(0L));
+ assertThrows(HopValueException.class, () -> JdeJulian.toDate(-1L));
+ assertThrows(HopValueException.class, () -> JdeJulian.toDate(100000L));
+ assertThrows(HopValueException.class, () -> JdeJulian.toDate(99366L));
+ assertThrows(HopValueException.class, () -> JdeJulian.toDate(100367L));
+ }
+
+ @Test
+ void julianToDateAcceptsIntegerNumberAndString() throws Exception {
+ JdeJulianToDate calc = new JdeJulianToDate();
+ Date expected = ISO_DATE.parse("1997-11-04");
+
+ assertEquals(
+ expected,
+ calc.calculate(
+ new CalculationInput(
+ new ValueMetaInteger("julian"),
+ null,
+ null,
+ 97308L,
+ null,
+ null,
+ null,
+ null,
+ null,
+ false))
+ .value);
+ assertEquals(
+ expected,
+ calc.calculate(
+ new CalculationInput(
+ new ValueMetaNumber("julian"),
+ null,
+ null,
+ 97308.0,
+ null,
+ null,
+ null,
+ null,
+ null,
+ false))
+ .value);
+ assertEquals(
+ expected,
+ calc.calculate(
+ new CalculationInput(
+ new ValueMetaString("julian"),
+ null,
+ null,
+ "097308",
+ null,
+ null,
+ null,
+ null,
+ null,
+ false))
+ .value);
+ assertEquals(
+ ISO_DATE.parse("2009-01-01"),
+ calc.calculate(
+ new CalculationInput(
+ new ValueMetaString("julian"),
+ null,
+ null,
+ "109001",
+ null,
+ null,
+ null,
+ null,
+ null,
+ false))
+ .value);
+ }
+
+ @Test
+ void calculatorTransformRoundTrip() throws Exception {
+ RowMeta inputRowMeta = new RowMeta();
+ inputRowMeta.addValueMeta(new ValueMetaDate("isoDate"));
+ inputRowMeta.addValueMeta(new ValueMetaInteger("julianIn"));
+
+ Date isoDate = ISO_DATE.parse("2011-01-13");
+ IRowSet inputRowSet = smh.getMockInputRowSet(new Object[][] {{isoDate,
111013L}});
+ inputRowSet.setRowMeta(inputRowMeta);
+
+ CalculatorMeta meta = new CalculatorMeta();
+ meta.getFunctions()
+ .add(
+ new CalculatorMetaFunction(
+ "julianOut",
+ CalculationType.DATE_TO_JDE_JULIAN,
+ "isoDate",
+ null,
+ null,
+ "Integer",
+ 0,
+ 0,
+ "",
+ "",
+ "",
+ "",
+ false));
+ meta.getFunctions()
+ .add(
+ new CalculatorMetaFunction(
+ "isoOut",
+ CalculationType.JDE_JULIAN_TO_DATE,
+ "julianIn",
+ null,
+ null,
+ "Date",
+ 0,
+ 0,
+ "yyyy-MM-dd",
+ "",
+ "",
+ "",
+ false));
+
+ Calculator calculator =
+ new Calculator(
+ smh.transformMeta, meta, new CalculatorData(), 0,
smh.pipelineMeta, smh.pipeline);
+ calculator.addRowSetToInputRowSets(inputRowSet);
+ calculator.setInputRowMeta(inputRowMeta);
+ calculator.init();
+ calculator.addRowListener(
+ new RowAdapter() {
+ @Override
+ public void rowWrittenEvent(IRowMeta rowMeta, Object[] row) {
+ assertEquals(111013L, row[2]);
+ assertEquals(midnight(isoDate), row[3]);
+ }
+ });
+
+ assertTrue(calculator.processRow());
+ }
+
+ @Test
+ void calculationTypeCodesMatchMetadata() {
+ assertEquals("DATE_TO_JDE_JULIAN",
CalculationType.DATE_TO_JDE_JULIAN.getCode());
+ assertEquals("JDE_JULIAN_TO_DATE",
CalculationType.JDE_JULIAN_TO_DATE.getCode());
+ assertEquals(
+ CalculationType.DATE_TO_JDE_JULIAN,
+
CalculationType.findByDescription(CalculationType.DATE_TO_JDE_JULIAN.getDescription()));
+ assertEquals(
+ CalculationType.JDE_JULIAN_TO_DATE,
+
CalculationType.findByDescription(CalculationType.JDE_JULIAN_TO_DATE.getDescription()));
+ }
+
+ @Test
+ void dateToJulianResultTypeIsInteger() throws Exception {
+ DateToJdeJulian calc = new DateToJdeJulian();
+ Date date = ISO_DATE.parse("2000-08-23");
+ CalculationOutput output =
+ calc.calculate(
+ new CalculationInput(
+ new ValueMetaDate("isoDate"),
+ null,
+ null,
+ date,
+ null,
+ null,
+ null,
+ null,
+ null,
+ false));
+ assertEquals(100236L, output.value);
+ assertEquals(IValueMeta.TYPE_INTEGER, output.resultType);
+ }
+
+ private static Date midnight(Date date) {
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(date);
+ calendar.set(Calendar.HOUR_OF_DAY, 0);
+ calendar.set(Calendar.MINUTE, 0);
+ calendar.set(Calendar.SECOND, 0);
+ calendar.set(Calendar.MILLISECOND, 0);
+ return calendar.getTime();
+ }
+}