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 98aaee2939 Issue 7023 : group by moving average (#7383)
98aaee2939 is described below

commit 98aaee2939a2c0f077ed222a6213ce8370546f36
Author: Matt Casters <[email protected]>
AuthorDate: Wed Jul 1 16:27:29 2026 +0200

    Issue 7023 : group by moving average (#7383)
    
    * issue #7118 : field selection for table view export to spreadsheet
    
    * issue #7023 : Moving average for the group by transform
    
    * issue #7023 : Moving average for the group by transform (IT fix)
---
 .../ROOT/pages/pipeline/transforms/groupby.adoc    |   1 +
 .../transforms/0006-groupby-moving-average.hpl     | 268 +++++++++++++++++++++
 .../datasets/golden-groupby-moving-average.csv     |  11 +
 integration-tests/transforms/main-0006-groupby.hwf |  86 +++----
 .../dataset/golden-groupby-moving-average.json     |  40 +++
 .../0006-groupby-moving-average UNIT.json          |  41 ++++
 .../pipeline/transforms/groupby/Aggregation.java   |  45 +++-
 .../hop/pipeline/transforms/groupby/GroupBy.java   |  79 ++++++
 .../pipeline/transforms/groupby/GroupByData.java   |  14 ++
 .../pipeline/transforms/groupby/GroupByDialog.java |  20 +-
 .../pipeline/transforms/groupby/GroupByMeta.java   |   1 +
 .../groupby/messages/messages_en_US.properties     |   6 +-
 .../hop/pipeline/transforms/GroupByMetaTest.java   |   3 +-
 .../groupby/MovingAverageAggregationTest.java      | 193 +++++++++++++++
 14 files changed, 757 insertions(+), 51 deletions(-)

diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/groupby.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/groupby.adoc
index 6ba7f824da..2b139e181b 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/groupby.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/groupby.adoc
@@ -92,4 +92,5 @@ Here are the available aggregation methods :
 - Percentile (nearest-rank method)
 - Concatenate string separated by new line (CRLF)
 - Concatenate distinct values separated by <Value>: specify the separator in 
the Value column (This supports 
xref::variables.adoc#_hexadecimal_values[hexadecimals])
+- Moving average (last N rows) (all rows option only!): computes the rolling 
average over the last N events. Specify the window size N in the Value column 
(e.g. 5) and the sequencing field in the Order field column (input data must be 
pre-sorted).
 |===
\ No newline at end of file
diff --git a/integration-tests/transforms/0006-groupby-moving-average.hpl 
b/integration-tests/transforms/0006-groupby-moving-average.hpl
new file mode 100644
index 0000000000..ea81bd642f
--- /dev/null
+++ b/integration-tests/transforms/0006-groupby-moving-average.hpl
@@ -0,0 +1,268 @@
+<?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>
+    <pipeline_version/>
+    <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>
+    <pipeline_type>Normal</pipeline_type>
+    <pipeline_status>0</pipeline_status>
+    <parameters/>
+    <name>0006-groupby-moving-average</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <created_user>-</created_user>
+    <modified_user>-</modified_user>
+    <created_date>2026/06/30 20:50:00.000</created_date>
+    <modified_date>2026/06/30 20:50:00.000</modified_date>
+  </info>
+  <transform>
+    <type>DataGrid</type>
+    <name>Sample Data</name>
+    <fields>
+      <field>
+        <currency/>
+        <decimal/>
+        <group/>
+        <name>customer_id</name>
+        <type>String</type>
+        <format/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <group/>
+        <name>timestamp</name>
+        <type>Integer</type>
+        <format/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+      <field>
+        <currency/>
+        <decimal/>
+        <group/>
+        <name>amount</name>
+        <type>Number</type>
+        <format/>
+        <length>-1</length>
+        <precision>-1</precision>
+        <set_empty_string>N</set_empty_string>
+      </field>
+    </fields>
+    <data>
+      <line>
+        <item>c1</item>
+        <item>1</item>
+        <item>10.0</item>
+      </line>
+      <line>
+        <item>c1</item>
+        <item>2</item>
+        <item>20.0</item>
+      </line>
+      <line>
+        <item>c1</item>
+        <item>3</item>
+        <item>30.0</item>
+      </line>
+      <line>
+        <item>c1</item>
+        <item>4</item>
+        <item>40.0</item>
+      </line>
+      <line>
+        <item>c1</item>
+        <item>5</item>
+        <item>50.0</item>
+      </line>
+      <line>
+        <item>c2</item>
+        <item>1</item>
+        <item>100.0</item>
+      </line>
+      <line>
+        <item>c2</item>
+        <item>2</item>
+        <item>200.0</item>
+      </line>
+      <line>
+        <item>c2</item>
+        <item>3</item>
+        <item/>
+      </line>
+      <line>
+        <item>c2</item>
+        <item>4</item>
+        <item>300.0</item>
+      </line>
+      <line>
+        <item>c2</item>
+        <item>5</item>
+        <item>400.0</item>
+      </line>
+    </data>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>80</xloc>
+      <yloc>80</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>GroupBy</type>
+    <name>Group by customer</name>
+    <all_rows>Y</all_rows>
+    <directory>${java.io.tmpdir}</directory>
+    <prefix>grp</prefix>
+    <ignore_aggregate>N</ignore_aggregate>
+    <field_ignore/>
+    <group>
+      <field>
+        <name>customer_id</name>
+      </field>
+    </group>
+    <fields>
+      <field>
+        <aggregate>moving_avg</aggregate>
+        <subject>amount</subject>
+        <type>MOVING_AVG</type>
+        <valuefield>3</valuefield>
+        <orderfield>timestamp</orderfield>
+      </field>
+    </fields>
+    <add_linenr>N</add_linenr>
+    <linenr_fieldname/>
+    <give_back_row>N</give_back_row>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>240</xloc>
+      <yloc>80</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>SelectValues</type>
+    <name>numbers to string</name>
+    <fields>
+      <select_unspecified>N</select_unspecified>
+      <meta>
+        <name>amount</name>
+        <rename>amount</rename>
+        <type>String</type>
+        <length>-2</length>
+        <precision>-2</precision>
+        <storage_type/>
+        <conversion_mask>0.0;-0.0</conversion_mask>
+        <date_format_lenient>N</date_format_lenient>
+        <date_format_locale/>
+        <date_format_timezone/>
+        <lenient_string_to_number>N</lenient_string_to_number>
+        <decimal_symbol>.</decimal_symbol>
+        <grouping_symbol/>
+        <currency_symbol/>
+        <encoding/>
+      </meta>
+      <meta>
+        <name>moving_avg</name>
+        <rename>moving_avg</rename>
+        <type>String</type>
+        <length>-2</length>
+        <precision>-2</precision>
+        <storage_type/>
+        <conversion_mask>0.0;-0.0</conversion_mask>
+        <date_format_lenient>N</date_format_lenient>
+        <date_format_locale/>
+        <date_format_timezone/>
+        <lenient_string_to_number>N</lenient_string_to_number>
+        <decimal_symbol>.</decimal_symbol>
+        <grouping_symbol/>
+        <currency_symbol/>
+        <encoding/>
+      </meta>
+    </fields>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>400</xloc>
+      <yloc>80</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <transform>
+    <type>Dummy</type>
+    <name>Validate</name>
+    <distribute>Y</distribute>
+    <copies>1</copies>
+    <GUI>
+      <xloc>560</xloc>
+      <yloc>80</yloc>
+    </GUI>
+    <description/>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+  </transform>
+  <order>
+    <hop>
+      <from>Sample Data</from>
+      <to>Group by customer</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Group by customer</from>
+      <to>numbers to string</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>numbers to string</from>
+      <to>Validate</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <notepads/>
+  <attributes/>
+  <transform_error_handling/>
+</pipeline>
diff --git 
a/integration-tests/transforms/datasets/golden-groupby-moving-average.csv 
b/integration-tests/transforms/datasets/golden-groupby-moving-average.csv
new file mode 100644
index 0000000000..8610d23782
--- /dev/null
+++ b/integration-tests/transforms/datasets/golden-groupby-moving-average.csv
@@ -0,0 +1,11 @@
+customer_id,timestamp,amount,moving_avg
+c1,1,10.0,
+c1,2,20.0,
+c1,3,30.0,20.0
+c1,4,40.0,30.0
+c1,5,50.0,40.0
+c2,1,100.0,
+c2,2,200.0,
+c2,3,,
+c2,4,300.0,200.0
+c2,5,400.0,300.0
diff --git a/integration-tests/transforms/main-0006-groupby.hwf 
b/integration-tests/transforms/main-0006-groupby.hwf
index 8ba8f1a928..b933d71d93 100644
--- a/integration-tests/transforms/main-0006-groupby.hwf
+++ b/integration-tests/transforms/main-0006-groupby.hwf
@@ -22,37 +22,33 @@ limitations under the License.
   <name_sync_with_filename>Y</name_sync_with_filename>
   <description/>
   <extended_description/>
-  <workflow_version/>
   <created_user>-</created_user>
-  <created_date>2021/04/19 12:05:51.497</created_date>
   <modified_user>-</modified_user>
+  <created_date>2021/04/19 12:05:51.497</created_date>
   <modified_date>2021/04/19 12:05:51.497</modified_date>
-  <parameters>
-    </parameters>
+  <workflow_version/>
+  <parameters/>
   <actions>
     <action>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <intervalSeconds>0</intervalSeconds>
+      <intervalMinutes>60</intervalMinutes>
+      <DayOfMonth>1</DayOfMonth>
+      <weekDay>1</weekDay>
+      <minutes>0</minutes>
+      <hour>12</hour>
+      <doNotWaitOnFirstExecution>N</doNotWaitOnFirstExecution>
       <name>Start</name>
       <description/>
       <type>SPECIAL</type>
       <attributes/>
-      <DayOfMonth>1</DayOfMonth>
-      <hour>12</hour>
-      <intervalMinutes>60</intervalMinutes>
-      <intervalSeconds>0</intervalSeconds>
-      <minutes>0</minutes>
-      <repeat>N</repeat>
-      <schedulerType>0</schedulerType>
-      <weekDay>1</weekDay>
-      <parallel>N</parallel>
       <xloc>80</xloc>
       <yloc>80</yloc>
+      <parallel>N</parallel>
       <attributes_hac/>
     </action>
     <action>
-      <name>Run Group By tests</name>
-      <description/>
-      <type>RunPipelineTests</type>
-      <attributes/>
       <test_names>
         <test_name>
           <name>0006-groupby-basics UNIT</name>
@@ -66,66 +62,73 @@ limitations under the License.
         <test_name>
           <name>0006-groupby-concat UNIT</name>
         </test_name>
+        <test_name>
+          <name>0006-groupby-moving-average UNIT</name>
+        </test_name>
       </test_names>
-      <parallel>N</parallel>
-      <xloc>608</xloc>
+      <name>Run Group By tests</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <xloc>768</xloc>
       <yloc>80</yloc>
+      <parallel>N</parallel>
       <attributes_hac/>
     </action>
     <action>
-      <name>0006-groupby-alwaysgiveback-result.hpl</name>
-      <description/>
-      <type>PIPELINE</type>
-      <attributes/>
       
<filename>${PROJECT_HOME}/0006-groupby-alwaysgiveback-result.hpl</filename>
       <params_from_previous>N</params_from_previous>
       <exec_per_row>N</exec_per_row>
       <clear_rows>N</clear_rows>
       <clear_files>N</clear_files>
+      <create_parent_folder>N</create_parent_folder>
       <set_logfile>N</set_logfile>
+      <set_append_logfile>N</set_append_logfile>
       <logfile/>
       <logext/>
       <add_date>N</add_date>
       <add_time>N</add_time>
       <loglevel>Basic</loglevel>
-      <set_append_logfile>N</set_append_logfile>
       <wait_until_finished>Y</wait_until_finished>
-      <create_parent_folder>N</create_parent_folder>
-      <run_configuration>local</run_configuration>
       <parameters>
         <pass_all_parameters>Y</pass_all_parameters>
       </parameters>
-      <parallel>N</parallel>
-      <xloc>240</xloc>
+      <run_configuration>local</run_configuration>
+      <name>0006-groupby-alwaysgiveback-result.hpl</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      <xloc>272</xloc>
       <yloc>80</yloc>
+      <parallel>N</parallel>
       <attributes_hac/>
     </action>
     <action>
-      <name>0006-groupby-alwaysgiveback-result.hpl 2</name>
-      <description/>
-      <type>PIPELINE</type>
-      <attributes/>
       
<filename>${PROJECT_HOME}/0006-groupby-alwaysgiveback-result.hpl</filename>
       <params_from_previous>N</params_from_previous>
       <exec_per_row>N</exec_per_row>
       <clear_rows>N</clear_rows>
       <clear_files>N</clear_files>
+      <create_parent_folder>N</create_parent_folder>
       <set_logfile>N</set_logfile>
+      <set_append_logfile>N</set_append_logfile>
       <logfile/>
       <logext/>
       <add_date>N</add_date>
       <add_time>N</add_time>
       <loglevel>Basic</loglevel>
-      <set_append_logfile>N</set_append_logfile>
       <wait_until_finished>Y</wait_until_finished>
-      <create_parent_folder>N</create_parent_folder>
-      <run_configuration>local</run_configuration>
       <parameters>
         <pass_all_parameters>Y</pass_all_parameters>
       </parameters>
-      <parallel>N</parallel>
-      <xloc>448</xloc>
+      <run_configuration>local</run_configuration>
+      <name>0006-groupby-alwaysgiveback-result.hpl 2</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      <xloc>544</xloc>
       <yloc>80</yloc>
+      <parallel>N</parallel>
       <attributes_hac/>
     </action>
   </actions>
@@ -133,26 +136,25 @@ limitations under the License.
     <hop>
       <from>Start</from>
       <to>0006-groupby-alwaysgiveback-result.hpl</to>
-      <enabled>Y</enabled>
       <evaluation>Y</evaluation>
       <unconditional>Y</unconditional>
+      <enabled>Y</enabled>
     </hop>
     <hop>
       <from>0006-groupby-alwaysgiveback-result.hpl</from>
       <to>0006-groupby-alwaysgiveback-result.hpl 2</to>
-      <enabled>Y</enabled>
       <evaluation>N</evaluation>
       <unconditional>Y</unconditional>
+      <enabled>Y</enabled>
     </hop>
     <hop>
       <from>0006-groupby-alwaysgiveback-result.hpl 2</from>
       <to>Run Group By tests</to>
-      <enabled>Y</enabled>
       <evaluation>Y</evaluation>
       <unconditional>Y</unconditional>
+      <enabled>Y</enabled>
     </hop>
   </hops>
-  <notepads>
-  </notepads>
+  <notepads/>
   <attributes/>
 </workflow>
diff --git 
a/integration-tests/transforms/metadata/dataset/golden-groupby-moving-average.json
 
b/integration-tests/transforms/metadata/dataset/golden-groupby-moving-average.json
new file mode 100644
index 0000000000..7fa62b3282
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/dataset/golden-groupby-moving-average.json
@@ -0,0 +1,40 @@
+{
+  "base_filename": "golden-groupby-moving-average.csv",
+  "name": "golden-groupby-moving-average",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_format": "",
+      "field_name": "customer_id"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 5,
+      "field_precision": -1,
+      "field_format": "",
+      "field_name": "timestamp"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_format": "",
+      "field_name": "amount"
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_format": "",
+      "field_name": "moving_avg"
+    }
+  ],
+  "folder_name": ""
+}
diff --git 
a/integration-tests/transforms/metadata/unit-test/0006-groupby-moving-average 
UNIT.json 
b/integration-tests/transforms/metadata/unit-test/0006-groupby-moving-average 
UNIT.json
new file mode 100644
index 0000000000..2aff6f64a0
--- /dev/null
+++ 
b/integration-tests/transforms/metadata/unit-test/0006-groupby-moving-average 
UNIT.json     
@@ -0,0 +1,41 @@
+{
+  "variableValues": [],
+  "database_replacements": [],
+  "autoOpening": true,
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "customer_id",
+          "data_set_field": "customer_id"
+        },
+        {
+          "transform_field": "timestamp",
+          "data_set_field": "timestamp"
+        },
+        {
+          "transform_field": "amount",
+          "data_set_field": "amount"
+        },
+        {
+          "transform_field": "moving_avg",
+          "data_set_field": "moving_avg"
+        }
+      ],
+      "field_order": [
+        "customer_id",
+        "timestamp"
+      ],
+      "transform_name": "Validate",
+      "data_set_name": "golden-groupby-moving-average"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0006-groupby-moving-average UNIT",
+  "description": "",
+  "trans_test_tweaks": [],
+  "persist_filename": "",
+  "pipeline_filename": "./0006-groupby-moving-average.hpl",
+  "test_type": "UNIT_TEST"
+}
diff --git 
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/Aggregation.java
 
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/Aggregation.java
index 7d318fffe7..ac8e7f2c0b 100644
--- 
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/Aggregation.java
+++ 
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/Aggregation.java
@@ -72,6 +72,8 @@ public class Aggregation implements Cloneable {
 
   public static final int TYPE_GROUP_CONCAT_DISTINCT = 22;
 
+  public static final int TYPE_GROUP_MOVING_AVERAGE = 23;
+
   public static final String[]
       typeGroupLabel = /* WARNING: DO NOT TRANSLATE THIS. WE ARE SERIOUS, 
DON'T TRANSLATE! */ {
     "-",
@@ -97,6 +99,7 @@ public class Aggregation implements Cloneable {
     "PERCENTILE_NEAREST_RANK",
     "CONCAT_STRING_CRLF",
     "CONCAT_DISTINCT",
+    "MOVING_AVG",
   };
 
   public static final String[] typeGroupLongDesc = {
@@ -122,7 +125,8 @@ public class Aggregation implements Cloneable {
     BaseMessages.getString(PKG, 
"GroupByMeta.TypeGroupLongDesc.STANDARD_DEVIATION_SAMPLE"),
     BaseMessages.getString(PKG, 
"GroupByMeta.TypeGroupLongDesc.PERCENTILE_NEAREST_RANK"),
     BaseMessages.getString(PKG, 
"GroupByMeta.TypeGroupLongDesc.CONCAT_STRING_CRLF"),
-    BaseMessages.getString(PKG, 
"GroupByMeta.TypeGroupLongDesc.CONCAT_DISTINCT")
+    BaseMessages.getString(PKG, 
"GroupByMeta.TypeGroupLongDesc.CONCAT_DISTINCT"),
+    BaseMessages.getString(PKG, "GroupByMeta.TypeGroupLongDesc.MOVING_AVERAGE")
   };
 
   @HopMetadataProperty(
@@ -150,6 +154,12 @@ public class Aggregation implements Cloneable {
       injectionKeyDescription = "GroupByMeta.Injection.AGG_VALUE")
   private String value;
 
+  @HopMetadataProperty(
+      key = "orderfield",
+      injectionKey = "AGG_ORDER_FIELD",
+      injectionKeyDescription = "GroupByMeta.Injection.AGG_ORDER_FIELD")
+  private String orderField;
+
   public Aggregation() {}
 
   public Aggregation(String field, String subject, String typeDesc, String 
value) {
@@ -160,10 +170,20 @@ public class Aggregation implements Cloneable {
     this.value = value;
   }
 
+  public Aggregation(
+      String field, String subject, String typeDesc, String value, String 
orderField) {
+    this(field, subject, typeDesc, value);
+    this.orderField = orderField;
+  }
+
   @Override
   public Aggregation clone() {
     return new Aggregation(
-        field, subject, 
getTypeDescLongFromCode(getTypeCodeFromLabel(typeLabel)), value);
+        field,
+        subject,
+        getTypeDescLongFromCode(getTypeCodeFromLabel(typeLabel)),
+        value,
+        orderField);
   }
 
   @Override
@@ -175,12 +195,13 @@ public class Aggregation implements Cloneable {
         && Objects.equals(field, that.field)
         && Objects.equals(subject, that.subject)
         && Objects.equals(typeLabel, that.typeLabel)
-        && Objects.equals(value, that.value);
+        && Objects.equals(value, that.value)
+        && Objects.equals(orderField, that.orderField);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(field, subject, typeLabel, type, value);
+    return Objects.hash(field, subject, typeLabel, type, value, orderField);
   }
 
   /**
@@ -245,6 +266,22 @@ public class Aggregation implements Cloneable {
     this.value = value;
   }
 
+  /**
+   * Gets orderField
+   *
+   * @return value of orderField
+   */
+  public String getOrderField() {
+    return orderField;
+  }
+
+  /**
+   * @param orderField The orderField to set
+   */
+  public void setOrderField(String orderField) {
+    this.orderField = orderField;
+  }
+
   public int getType() {
     return type;
   }
diff --git 
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupBy.java
 
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupBy.java
index bca8b14a6d..02fa2b2046 100644
--- 
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupBy.java
+++ 
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupBy.java
@@ -122,6 +122,11 @@ public class GroupBy extends BaseTransform<GroupByMeta, 
GroupByData> {
       data.cumulativeAvgSourceIndexes = new ArrayList<>();
       data.cumulativeAvgTargetIndexes = new ArrayList<>();
 
+      data.movingAvgSourceIndexes = new ArrayList<>();
+      data.movingAvgTargetIndexes = new ArrayList<>();
+      data.movingAvgWidths = new ArrayList<>();
+      data.movingAvgIndexes = new ArrayList<>();
+
       for (int i = 0; i < meta.getAggregations().size(); i++) {
         Aggregation aggregation = meta.getAggregations().get(i);
         if (aggregation.getType() == Aggregation.TYPE_GROUP_COUNT_ANY) {
@@ -152,6 +157,20 @@ public class GroupBy extends BaseTransform<GroupByMeta, 
GroupByData> {
           //
           data.cumulativeAvgTargetIndexes.add(data.inputRowMeta.size() + i);
         }
+        if (aggregation.getType() == Aggregation.TYPE_GROUP_MOVING_AVERAGE) {
+          data.movingAvgSourceIndexes.add(data.subjectnrs[i]);
+          data.movingAvgTargetIndexes.add(data.inputRowMeta.size() + i);
+          data.movingAvgIndexes.add(i);
+          int windowSize = 1;
+          if (!Utils.isEmpty(aggregation.getValue())) {
+            try {
+              windowSize = Math.max(1, 
Integer.parseInt(resolve(aggregation.getValue())));
+            } catch (NumberFormatException e) {
+              // default to 1
+            }
+          }
+          data.movingAvgWidths.add(windowSize);
+        }
       }
 
       data.previousSums = new Object[data.cumulativeSumTargetIndexes.size()];
@@ -159,6 +178,18 @@ public class GroupBy extends BaseTransform<GroupByMeta, 
GroupByData> {
       data.previousAvgSum = new Object[data.cumulativeAvgTargetIndexes.size()];
       data.previousAvgCount = new long[data.cumulativeAvgTargetIndexes.size()];
 
+      // Initialise per-aggregation sliding windows for MOVING_AVERAGE
+      //
+      @SuppressWarnings("unchecked")
+      java.util.ArrayDeque<Double>[] windows =
+          new java.util.ArrayDeque[meta.getAggregations().size()];
+      data.movingAvgWindows = windows;
+      for (int i = 0; i < meta.getAggregations().size(); i++) {
+        if (meta.getAggregations().get(i).getType() == 
Aggregation.TYPE_GROUP_MOVING_AVERAGE) {
+          data.movingAvgWindows[i] = new java.util.ArrayDeque<>();
+        }
+      }
+
       data.groupnrs = new int[meta.getGroupingFields().size()];
       for (int i = 0; i < meta.getGroupingFields().size(); i++) {
         data.groupnrs[i] =
@@ -242,6 +273,7 @@ public class GroupBy extends BaseTransform<GroupByMeta, 
GroupByData> {
 
           addCumulativeSums(row);
           addCumulativeAverages(row);
+          addMovingAverages(row);
 
           putRow(data.outputRowMeta, row);
           row = getRowFromBuffer();
@@ -291,6 +323,7 @@ public class GroupBy extends BaseTransform<GroupByMeta, 
GroupByData> {
 
         addCumulativeSums(row);
         addCumulativeAverages(row);
+        addMovingAverages(row);
 
         putRow(data.outputRowMeta, row);
         row = getRowFromBuffer();
@@ -402,6 +435,37 @@ public class GroupBy extends BaseTransform<GroupByMeta, 
GroupByData> {
     }
   }
 
+  void addMovingAverages(Object[] row) throws HopValueException {
+    for (int i = 0; i < data.movingAvgSourceIndexes.size(); i++) {
+      int sourceIndex = data.movingAvgSourceIndexes.get(i);
+      int targetIndex = data.movingAvgTargetIndexes.get(i);
+      int windowSize = data.movingAvgWidths.get(i);
+      int aggIndex = data.movingAvgIndexes.get(i);
+
+      Object sourceValue = row[sourceIndex];
+      IValueMeta sourceMeta = data.inputRowMeta.getValueMeta(sourceIndex);
+
+      if (!sourceMeta.isNull(sourceValue)) {
+        java.util.ArrayDeque<Double> window = data.movingAvgWindows[aggIndex];
+        window.addLast(sourceMeta.getNumber(sourceValue));
+        while (window.size() > windowSize) {
+          window.pollFirst();
+        }
+        if (window.size() == windowSize) {
+          double sum = 0.0;
+          for (double val : window) {
+            sum += val;
+          }
+          row[targetIndex] = sum / windowSize;
+        } else {
+          row[targetIndex] = null;
+        }
+      } else {
+        row[targetIndex] = null;
+      }
+    }
+  }
+
   // Is the row r of the same group as previous?
   boolean sameGroup(Object[] previous, Object[] r) throws HopValueException {
     return data.inputRowMeta.compare(previous, r, data.groupnrs) == 0;
@@ -574,6 +638,9 @@ public class GroupBy extends BaseTransform<GroupByMeta, 
GroupByData> {
             SortedSet<Object> set = (SortedSet<Object>) value;
             set.add(subj);
           }
+          break;
+        case Aggregation.TYPE_GROUP_MOVING_AVERAGE:
+          break;
         default:
           break;
       }
@@ -655,6 +722,15 @@ public class GroupBy extends BaseTransform<GroupByMeta, 
GroupByData> {
           vMeta = new ValueMetaString(fieldName);
           v = new TreeSet<>();
           break;
+        case Aggregation.TYPE_GROUP_MOVING_AVERAGE:
+          vMeta = new ValueMetaNumber(fieldName);
+          // agg[i] is null until the first full window is seen; set by 
calcAggregate per row
+          v = null;
+          // Reset (clear) the sliding window for this aggregation within the 
new group
+          if (data.movingAvgWindows != null && data.movingAvgWindows[i] != 
null) {
+            data.movingAvgWindows[i].clear();
+          }
+          break;
         default:
           // TODO raise an error here because we cannot continue successfully 
maybe the UI should
           // validate this
@@ -790,6 +866,9 @@ public class GroupBy extends BaseTransform<GroupByMeta, 
GroupByData> {
             Aggregation.TYPE_GROUP_CONCAT_STRING_CRLF:
           ag = ((StringBuilder) ag).toString();
           break;
+        case Aggregation.TYPE_GROUP_MOVING_AVERAGE:
+          // The result is already computed per-row in calcAggregate; pass 
through as-is.
+          break;
         case Aggregation.TYPE_GROUP_CONCAT_DISTINCT:
           IValueMeta subjMeta = 
data.inputRowMeta.getValueMeta(data.subjectnrs[i]);
           String separator = "";
diff --git 
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByData.java
 
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByData.java
index 89e867bda8..f800d681bc 100644
--- 
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByData.java
+++ 
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByData.java
@@ -22,6 +22,7 @@ import java.io.DataOutputStream;
 import java.io.File;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -80,6 +81,11 @@ public class GroupByData extends BaseTransformData 
implements ITransformData {
   public List<Integer> cumulativeAvgSourceIndexes;
   public List<Integer> cumulativeAvgTargetIndexes;
 
+  public List<Integer> movingAvgSourceIndexes;
+  public List<Integer> movingAvgTargetIndexes;
+  public List<Integer> movingAvgWidths;
+  public List<Integer> movingAvgIndexes;
+
   public Object[] previousSums;
 
   public Object[] previousAvgSum;
@@ -93,6 +99,14 @@ public class GroupByData extends BaseTransformData 
implements ITransformData {
 
   public boolean newBatch;
 
+  /**
+   * Sliding window buffers for MOVING_AVERAGE aggregations. One ArrayDeque 
per aggregation index;
+   * null for aggregations that are not MOVING_AVERAGE. Each deque holds the 
last N non-null numeric
+   * values of the subject field.
+   */
+  @SuppressWarnings("java:S1104")
+  public ArrayDeque<Double>[] movingAvgWindows;
+
   public GroupByData() {
     super();
 
diff --git 
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByDialog.java
 
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByDialog.java
index 31fe5629a0..21c9819dd2 100644
--- 
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByDialog.java
+++ 
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByDialog.java
@@ -55,6 +55,7 @@ public class GroupByDialog extends BaseTransformDialog {
 
   public static final String STRING_SORT_WARNING_PARAMETER = 
"GroupSortWarning";
   private static final int AGGREGATION_TABLE_TYPE_INDEX = 3;
+  private static final int AGGREGATION_TABLE_ORDER_FIELD_INDEX = 5;
 
   private TableView wGroup;
 
@@ -309,7 +310,7 @@ public class GroupByDialog extends BaseTransformDialog {
     fdlAgg.top = new FormAttachment(wGroup, margin);
     wlAgg.setLayoutData(fdlAgg);
 
-    int nrCols = 4;
+    int nrCols = 5;
     int nrRows = input.getAggregations().size();
 
     ciReturn = new ColumnInfo[nrCols];
@@ -336,6 +337,14 @@ public class GroupByDialog extends BaseTransformDialog {
             false);
     ciReturn[3].setToolTip(BaseMessages.getString(PKG, 
"GroupByDialog.ColumnInfo.Value.Tooltip"));
     ciReturn[3].setUsingVariables(true);
+    ciReturn[4] =
+        new ColumnInfo(
+            BaseMessages.getString(PKG, "GroupByDialog.ColumnInfo.OrderField"),
+            ColumnInfo.COLUMN_TYPE_CCOMBO,
+            new String[] {""},
+            false);
+    ciReturn[4].setToolTip(
+        BaseMessages.getString(PKG, 
"GroupByDialog.ColumnInfo.OrderField.Tooltip"));
 
     wAgg =
         new TableView(
@@ -408,6 +417,7 @@ public class GroupByDialog extends BaseTransformDialog {
     String[] fieldNames = ConstUi.sortFieldNames(inputFields);
     ciKey[0].setComboValues(fieldNames);
     ciReturn[1].setComboValues(fieldNames);
+    ciReturn[4].setComboValues(fieldNames);
   }
 
   public void setFlags() {
@@ -457,6 +467,7 @@ public class GroupByDialog extends BaseTransformDialog {
       item.setText(2, Const.NVL(aggregation.getSubject(), ""));
       item.setText(3, 
Const.NVL(Aggregation.getTypeDescLongFromCode(aggregation.getType()), ""));
       item.setText(4, Const.NVL(aggregation.getValue(), ""));
+      item.setText(5, Const.NVL(aggregation.getOrderField(), ""));
     }
 
     wGroup.setRowNums();
@@ -502,7 +513,9 @@ public class GroupByDialog extends BaseTransformDialog {
       String aggSubject = item.getText(2);
       String aggTypeDesc = item.getText(3);
       String aggValue = item.getText(4);
-      Aggregation aggr = new Aggregation(aggField, aggSubject, aggTypeDesc, 
aggValue);
+      String aggOrderField = item.getText(5);
+      Aggregation aggr =
+          new Aggregation(aggField, aggSubject, aggTypeDesc, aggValue, 
aggOrderField);
       input.getAggregations().add(aggr);
     }
 
@@ -582,7 +595,8 @@ public class GroupByDialog extends BaseTransformDialog {
             .anyMatch(
                 pred ->
                     pred == Aggregation.TYPE_GROUP_CUMULATIVE_SUM
-                        || pred == Aggregation.TYPE_GROUP_CUMULATIVE_AVERAGE);
+                        || pred == Aggregation.TYPE_GROUP_CUMULATIVE_AVERAGE
+                        || pred == Aggregation.TYPE_GROUP_MOVING_AVERAGE);
 
     allRowsButton.setEnabled(!isCumulativeSelected);
 
diff --git 
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByMeta.java
 
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByMeta.java
index 832e0ee7da..64b2983f96 100644
--- 
a/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByMeta.java
+++ 
b/plugins/transforms/groupby/src/main/java/org/apache/hop/pipeline/transforms/groupby/GroupByMeta.java
@@ -274,6 +274,7 @@ public class GroupByMeta extends BaseTransformMeta<GroupBy, 
GroupByData> {
           case Aggregation.TYPE_GROUP_STANDARD_DEVIATION_SAMPLE:
           case Aggregation.TYPE_GROUP_PERCENTILE:
           case Aggregation.TYPE_GROUP_PERCENTILE_NEAREST_RANK:
+          case Aggregation.TYPE_GROUP_MOVING_AVERAGE:
             valueType = IValueMeta.TYPE_NUMBER;
             break;
           case Aggregation.TYPE_GROUP_CONCAT_STRING:
diff --git 
a/plugins/transforms/groupby/src/main/resources/org/apache/hop/pipeline/transforms/groupby/messages/messages_en_US.properties
 
b/plugins/transforms/groupby/src/main/resources/org/apache/hop/pipeline/transforms/groupby/messages/messages_en_US.properties
index 2d95207d11..64ba278c6c 100644
--- 
a/plugins/transforms/groupby/src/main/resources/org/apache/hop/pipeline/transforms/groupby/messages/messages_en_US.properties
+++ 
b/plugins/transforms/groupby/src/main/resources/org/apache/hop/pipeline/transforms/groupby/messages/messages_en_US.properties
@@ -32,10 +32,12 @@ GroupByDialog.AlwaysAddResult.ToolTip=To make sure we 
always output a correct co
 GroupByDialog.Browse.Button=&Browse...
 GroupByDialog.ColumnInfo.GroupField=Group field
 GroupByDialog.ColumnInfo.Name=Name
+GroupByDialog.ColumnInfo.OrderField=Order field
 GroupByDialog.ColumnInfo.Subject=Subject
 GroupByDialog.ColumnInfo.Type=Type
 GroupByDialog.ColumnInfo.Value=Value
-GroupByDialog.ColumnInfo.Value.Tooltip=Fill here if more value is 
requested.\nFor example for ''Concatenate strings separated by'' option.
+GroupByDialog.ColumnInfo.Value.Tooltip=Fill here if more value is 
requested.\nFor example for ''Concatenate strings separated by'' option.\nFor 
Moving Average, enter the window size N (e.g. 5).
+GroupByDialog.ColumnInfo.OrderField.Tooltip=Specify the field that defines the 
order of rows within a group.\nEnsure the input data is sorted by this field 
before this transform (e.g. using a Sort transform).\nThis is informational 
only\: Moving Average does not sort internally.
 GroupByDialog.FailedToGetFields.DialogMessage=Unable to get fields from 
previous transforms because of an error
 GroupByDialog.FailedToGetFields.DialogTitle=Get fields failed
 GroupByDialog.FilePrefix.Label=TMP-file prefix
@@ -57,6 +59,7 @@ GroupByMeta.Exception.UnableToLoadTransformMetaFromXML=Unable 
to load transform
 GroupByMeta.Injection.ADD_GROUP_LINENR=Add group line number?
 GroupByMeta.Injection.ADD_GROUP_LINENR_FIELD=Group line number field
 GroupByMeta.Injection.AGG_FIELD=Aggregation field
+GroupByMeta.Injection.AGG_ORDER_FIELD=Aggregation order field
 GroupByMeta.Injection.AGG_SUBJECT=Aggregation subject
 GroupByMeta.Injection.AGG_TYPE=Aggregation type
 GroupByMeta.Injection.AGG_VALUE=Aggregation value
@@ -85,6 +88,7 @@ GroupByMeta.TypeGroupLongDesc.LAST_INCL_NULL=Last value
 GroupByMeta.TypeGroupLongDesc.MAX=Maximum
 GroupByMeta.TypeGroupLongDesc.MEDIAN=Median
 GroupByMeta.TypeGroupLongDesc.MIN=Minimum
+GroupByMeta.TypeGroupLongDesc.MOVING_AVERAGE=Moving average (last N rows)
 GroupByMeta.TypeGroupLongDesc.PERCENTILE=Percentile (linear interpolation)
 GroupByMeta.TypeGroupLongDesc.PERCENTILE_NEAREST_RANK=Percentile (nearest-rank 
method)
 GroupByMeta.TypeGroupLongDesc.STANDARD_DEVIATION=Standard deviation 
(population)
diff --git 
a/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/GroupByMetaTest.java
 
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/GroupByMetaTest.java
index 65678c9bc0..d1b21f70e0 100644
--- 
a/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/GroupByMetaTest.java
+++ 
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/GroupByMetaTest.java
@@ -100,7 +100,8 @@ class GroupByMetaTest {
             new Aggregation("field21", "subject21", getDesc("STD_DEV_SAMPLE"), 
"value21"),
             new Aggregation("field22", "subject22", 
getDesc("PERCENTILE_NEAREST_RANK"), "value22"),
             new Aggregation("field23", "subject23", 
getDesc("CONCAT_STRING_CRLF"), null),
-            new Aggregation("field24", "subject23", 
getDesc("CONCAT_DISTINCT"), "value24")));
+            new Aggregation("field24", "subject23", 
getDesc("CONCAT_DISTINCT"), "value24"),
+            new Aggregation("field25", "subject25", getDesc("MOVING_AVG"), 
"5", "order_field_25")));
     meta.setPassAllRows(true);
     meta.setAlwaysGivingBackOneRow(true);
     meta.setDirectory("directory");
diff --git 
a/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/MovingAverageAggregationTest.java
 
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/MovingAverageAggregationTest.java
new file mode 100644
index 0000000000..08551fa561
--- /dev/null
+++ 
b/plugins/transforms/groupby/src/test/java/org/apache/hop/pipeline/transforms/groupby/MovingAverageAggregationTest.java
@@ -0,0 +1,193 @@
+/*
+ * 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.groupby;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.hop.core.HopEnvironment;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.row.IRowMeta;
+import org.apache.hop.core.row.RowMeta;
+import org.apache.hop.core.row.value.ValueMetaNumber;
+import org.apache.hop.pipeline.transform.TransformMeta;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for the Moving Average (Last N Events) aggregation type in the 
Group By transform.
+ *
+ * <p>Uses the same package as GroupBy so that package-private methods 
(addMovingAverages,
+ * newAggregate, getAggregateResult, setAllNullsAreZero) are accessible.
+ */
+class MovingAverageAggregationTest {
+
+  @BeforeAll
+  static void setUpClass() throws HopException {
+    HopEnvironment.init();
+  }
+
+  private GroupByMeta meta;
+  private GroupByData data;
+  private GroupBy groupBy;
+
+  /** Builds a minimal GroupBy with one MOVING_AVERAGE aggregation on a Number 
column (N=3). */
+  @BeforeEach
+  void setUp() throws HopException {
+    meta = new GroupByMeta();
+
+    // Minimal TransformMeta stub required by BaseTransform constructor
+    TransformMeta transformMeta = new TransformMeta();
+    transformMeta.setName("test-groupby");
+
+    // Input row: [amount (Number)]
+    IRowMeta inputRowMeta = new RowMeta();
+    inputRowMeta.addValueMeta(new ValueMetaNumber("amount"));
+
+    // One aggregation: MOVING_AVERAGE of "amount" with N=3
+    Aggregation agg =
+        new Aggregation(
+            "moving_avg_amount",
+            "amount",
+            
Aggregation.getTypeDescLongFromCode(Aggregation.TYPE_GROUP_MOVING_AVERAGE),
+            "3",
+            "");
+    meta.getAggregations().add(agg);
+
+    data = new GroupByData();
+    data.inputRowMeta = inputRowMeta;
+    data.subjectnrs = new int[] {0}; // "amount" is at index 0
+    data.counts = new long[1];
+    data.mean = new double[1];
+    // Cumulative-sum/avg tracking arrays (required by newAggregate's teardown 
section)
+    data.previousSums = new Object[0];
+    data.previousAvgSum = new Object[0];
+    data.previousAvgCount = new long[0];
+
+    // Initialise moving average metadata lists used by addMovingAverages
+    data.movingAvgSourceIndexes = List.of(0);
+    data.movingAvgTargetIndexes = List.of(1);
+    data.movingAvgWidths = new ArrayList<>(List.of(3));
+    data.movingAvgIndexes = List.of(0);
+
+    // Initialise the sliding window array
+    @SuppressWarnings("unchecked")
+    ArrayDeque<Double>[] windows = new ArrayDeque[1];
+    data.movingAvgWindows = windows;
+    data.movingAvgWindows[0] = new ArrayDeque<>();
+
+    // Minimal GroupBy instance - we only exercise 
addMovingAverages/newAggregate/getAggregateResult
+    groupBy =
+        new GroupBy(transformMeta, meta, data, 0, null, null) {
+          // Override resolve so variable expressions are returned as-is
+          @Override
+          public String resolve(String str) {
+            return str;
+          }
+        };
+    groupBy.setAllNullsAreZero(false);
+
+    // Call newAggregate to initialise data.agg[] and data.aggMeta
+    groupBy.newAggregate(new Object[] {null});
+  }
+
+  // Helper: feed a value through addMovingAverages and return the computed 
moving_avg value
+  private Object feedValue(Double value) throws Exception {
+    Object[] row = new Object[] {value, null};
+    groupBy.addMovingAverages(row);
+    return row[1];
+  }
+
+  /**
+   * With N=3, the first two rows should produce null (window not full), and 
the third row should
+   * produce the average of the three values.
+   */
+  @Test
+  void testPartialWindowReturnsNull() throws Exception {
+    assertNull(feedValue(10.0), "1st row: window not full -> null");
+    assertNull(feedValue(20.0), "2nd row: window not full -> null");
+    assertEquals(20.0, (Double) feedValue(30.0), 1e-9, "3rd row: full window 
-> avg(10,20,30)=20");
+  }
+
+  /** After the window fills, it slides correctly: removes oldest, adds 
newest, recomputes. */
+  @Test
+  void testSlidingWindowMovesCorrectly() throws Exception {
+    feedValue(10.0); // null
+    feedValue(20.0); // null
+    feedValue(30.0); // avg(10,20,30)=20
+    // Now add 40: window = [20,30,40], avg = 30
+    assertEquals(30.0, (Double) feedValue(40.0), 1e-9, "avg(20,30,40)=30");
+    // Add 50: window = [30,40,50], avg = 40
+    assertEquals(40.0, (Double) feedValue(50.0), 1e-9, "avg(30,40,50)=40");
+  }
+
+  /** A null subject value must be ignored: the window does not grow and 
result stays null. */
+  @Test
+  void testNullSubjectIsSkipped() throws Exception {
+    feedValue(10.0); // window=[10], size=1 -> null
+    feedValue(null); // null value skipped; window=[10], size=1 -> still null
+    assertNull(feedValue(20.0), "Window size 2 < N=3 -> null");
+    assertEquals(20.0, (Double) feedValue(30.0), 1e-9, "After null skip: 
avg(10,20,30)=20");
+  }
+
+  /**
+   * Resetting the window (simulating a new group) via newAggregate must clear 
the deque and reset
+   * to null.
+   */
+  @Test
+  void testWindowResetsOnNewGroup() throws Exception {
+    feedValue(10.0);
+    feedValue(20.0);
+    feedValue(30.0); // full window, avg=20
+
+    // Simulate start of a new group
+    groupBy.newAggregate(new Object[] {null});
+
+    // Window should be clear; first value produces null again
+    assertNull(feedValue(5.0), "After group reset, 1st value -> null");
+    assertNull(feedValue(15.0), "After group reset, 2nd value -> null");
+    assertEquals(
+        15.0, (Double) feedValue(25.0), 1e-9, "After group reset, 3rd value 
avg(5,15,25)=15");
+  }
+
+  /** With N=1, every non-null row should immediately return that row's own 
value. */
+  @Test
+  void testWindowSizeOne() throws Exception {
+    // Reconfigure with N=1
+    meta.getAggregations().get(0).setValue("1");
+    data.movingAvgWidths.set(0, 1);
+    data.movingAvgWindows[0].clear();
+    groupBy.newAggregate(new Object[] {null});
+
+    assertEquals(42.0, (Double) feedValue(42.0), 1e-9, "N=1: returns current 
value 42");
+    assertEquals(7.0, (Double) feedValue(7.0), 1e-9, "N=1: returns current 
value 7");
+    assertEquals(100.0, (Double) feedValue(100.0), 1e-9, "N=1: returns current 
value 100");
+  }
+
+  /** Verify that getAggregateResult passes through the rolling value stored 
in agg[0]. */
+  @Test
+  void testGetAggregateResultPassesThrough() throws Exception {
+    data.agg[0] = 20.0;
+    Object[] result = groupBy.getAggregateResult();
+    assertEquals(20.0, (Double) result[0], 1e-9, "getAggregateResult passes 
through moving avg");
+  }
+}


Reply via email to